From eff4d933a2fa9c146a92cfc1d3add9c449393cff Mon Sep 17 00:00:00 2001 From: manatlan Date: Fri, 29 Sep 2023 19:39:29 +0200 Subject: [PATCH 01/26] 1st implem of the new RedysServer --- __init__.py | 0 example.py | 52 +++++++- htagweb/__init__.py | 1 + htagweb/redysserver.py | 244 +++++++++++++++++++++++++++++++++++++ htagweb/server/__init__.py | 198 ++++++++++++++++++++++++++++++ htagweb/server/client.py | 122 +++++++++++++++++++ poetry.lock | 13 +- pyproject.toml | 1 + 8 files changed, 626 insertions(+), 5 deletions(-) create mode 100644 __init__.py create mode 100644 htagweb/redysserver.py create mode 100644 htagweb/server/__init__.py create mode 100644 htagweb/server/client.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example.py b/example.py index c52ce67..9f22c0b 100644 --- a/example.py +++ b/example.py @@ -1,8 +1,46 @@ from htag import Tag -class App(Tag.div): +from htag import Tag +import json + +class TagSession(Tag.div): #dynamic component (compliant htag >= 0.30) !!!! FIRST IN THE WORLD !!!! + def init(self): + self["style"]="border:1px solid black" + self.otitle = Tag.h3(_style="padding:0px;margin:0px;float:right") + self.orendu = Tag.pre(_style="padding:0px;margin:0px") + + # draw ui + self+=self.otitle + self+=self.orendu + + def render(self): + self.otitle.set( "Live Session" ) + self.orendu.set( json.dumps( dict(self.session), indent=1)) + +class App(Tag.body): + imports=[TagSession] + statics=b"window.error=alert" def init(self): - self+= "hello world" + # del self.session["apps.draw.App"] + def inc_test_session(o): + v=int(self.state.get("integer","0")) + v=v+1 + self.state["integer"]=v + def addd(o): + if "list" in self.state: + self.state["list"].append("x") # <= this workd because tag.state.save() called in interaction (before guess rendering) + else: + self.state["list"]=[] + def clllll(o): + self.state.clear() + + self <= Tag.button("inc integer",_onclick=inc_test_session) + self <= Tag.button("add list",_onclick=addd) + self <= Tag.button("clear",_onclick=clllll) + self <= TagSession() + + + # With Web http runner provided by htag #------------------------------------------------------ @@ -11,5 +49,11 @@ def init(self): # With htagweb.WebServer runner provided by htagweb #------------------------------------------------------ -from htagweb import AppServer -AppServer( App ).run(openBrowser=True) + +if __name__=="__main__": + try: + from htagweb import AppServer,RedysServer + app=RedysServer( "example:App" ,parano=False) + app.run(openBrowser=False) + except: + pass diff --git a/htagweb/__init__.py b/htagweb/__init__.py index aef9a23..ae3018f 100644 --- a/htagweb/__init__.py +++ b/htagweb/__init__.py @@ -7,6 +7,7 @@ # https://github.com/manatlan/htagweb # ############################################################################# +from .redysserver import RedysServer #NOT THE DEFINITIVE NAME !!!!!!!!!!!!!!!! from .appserver import AppServer # a completly different beast, but compatible with ^^ from .htagserver import HtagServer # a completly different beast. from .usot import Usot diff --git a/htagweb/redysserver.py b/htagweb/redysserver.py new file mode 100644 index 0000000..232cb95 --- /dev/null +++ b/htagweb/redysserver.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- +# ############################################################################# +# Copyright (C) 2023 manatlan manatlan[at]gmail(dot)com +# +# MIT licence +# +# https://github.com/manatlan/htagweb +# ############################################################################# + +# gunicorn -w 4 -k uvicorn.workers.UvicornWorker -b localhost:8000 --preload basic:app + + +import os +import sys +import json +import uuid +import logging +import uvicorn +import asyncio +from htag import Tag +from starlette.applications import Starlette +from starlette.responses import HTMLResponse +from starlette.applications import Starlette +from starlette.routing import Route,WebSocketRoute +from starlette.endpoints import WebSocketEndpoint +from starlette.middleware import Middleware +from starlette.requests import HTTPConnection +from starlette.datastructures import MutableHeaders +from starlette.types import ASGIApp, Message, Receive, Scope, Send + +from htag.runners import commons +from . import crypto,usot + +from htagweb.server import importClassFromFqn, hrserver +from htagweb.server.client import HrPilot + +logger = logging.getLogger(__name__) +#################################################### +from types import ModuleType + +from . import sessions + +def findfqn(x) -> str: + if isinstance(x,str): + if ("." not in x) and (":" not in x): + raise Exception(f"'{x}' is not a 'full qualified name' (expected 'module.name') of an App (htag.Tag class)") + return x # /!\ x is a fqn /!\ DANGEROUS /!\ + elif isinstance(x, ModuleType): + if hasattr(x,"App"): + tagClass=getattr(x,"App") + if not issubclass(tagClass,Tag): + raise Exception("The 'App' of the module is not inherited from 'htag.Tag class'") + else: + raise Exception("module should contains a 'App' (htag.Tag class)") + elif issubclass(x,Tag): + tagClass=x + else: + raise Exception(f"!!! wtf ({x}) ???") + + return tagClass.__module__+"."+tagClass.__qualname__ + + + + +class WebServerSession: # ASGI Middleware, for starlette + def __init__(self, app:ASGIApp, https_only:bool = False, sesprovider:"async method(uid)"=None ) -> None: + self.app = app + self.session_cookie = "session" + self.max_age = 0 + self.path = "/" + self.security_flags = "httponly; samesite=lax" + if https_only: # Secure flag can be used with HTTPS only + self.security_flags += "; secure" + self.cbsesprovider=sesprovider + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] not in ("http", "websocket"): # pragma: no cover + await self.app(scope, receive, send) + return + + connection = HTTPConnection(scope) + + if self.session_cookie in connection.cookies: + uid = connection.cookies[self.session_cookie] + else: + uid = str(uuid.uuid4()) + + #!!!!!!!!!!!!!!!!!!!!!!!!!!! + scope["uid"] = uid + scope["session"] = await self.cbsesprovider(uid) + #!!!!!!!!!!!!!!!!!!!!!!!!!!! + + logger.debug("request for %s, scope=%s",uid,scope) + + async def send_wrapper(message: Message) -> None: + if message["type"] == "http.response.start": + # send it back, in all cases + headers = MutableHeaders(scope=message) + header_value = "{session_cookie}={data}; path={path}; {max_age}{security_flags}".format( # noqa E501 + session_cookie=self.session_cookie, + data=uid, + path=self.path, + max_age=f"Max-Age={self.max_age}; " if self.max_age else "", + security_flags=self.security_flags, + ) + headers.append("Set-Cookie", header_value) + await send(message) + + await self.app(scope, receive, send_wrapper) + + +def normalize(fqn): + if ":" not in fqn: + # replace last "." by ":" + fqn="".join( reversed("".join(reversed(fqn)).replace(".",":",1))) + return fqn + +class HRSocket(WebSocketEndpoint): + encoding = "text" + + async def _sendback(self,ws, txt:str) -> bool: + try: + if ws.app.parano: + txt = crypto.encrypt(txt.encode(),ws.app.parano) + + await ws.send_text( txt ) + return True + except Exception as e: + logger.error("Can't send to socket, error: %s",e) + return False + + async def on_connect(self, websocket): + await websocket.accept() + + async def on_receive(self, websocket, data): + fqn=websocket.path_params.get("fqn","") + uid=websocket.scope["uid"] + + if websocket.app.parano: + data = crypto.decrypt(data.encode(),websocket.app.parano).decode() + data=json.loads(data) + + p=HrPilot(uid,fqn) + + actions=await p.interact( oid=data["id"], method_name=data["method"], args=data["args"], kargs=data["kargs"], event=data.get("event") ) + + await self._sendback( websocket, json.dumps(actions) ) + + async def on_disconnect(self, websocket, close_code): + pass + +class RedysServer(Starlette): #NOT THE DEFINITIVE NAME !!!!!!!!!!!!!!!! + def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True,ssl:bool=False,parano:bool=False,sesprovider:"htagweb.sessions.create*|None"=None): + self.ssl=ssl + #TODO: NOT GOOD (when socket disconnet panaro uid change ... so broken !!!!) + self.parano = str(uuid.uuid4()) if parano else None + if sesprovider is None: + sesprovider = sessions.createFile + + ##asyncio.ensure_future( hrserver() ) + + Starlette.__init__( self, + debug=debug, + routes=[WebSocketRoute("/_/{fqn}", HRSocket)], + middleware=[Middleware(WebServerSession,https_only=ssl,sesprovider=sesprovider)], + ) + + if obj: + async def handleHome(request): + return await self.serve(request,obj) + self.add_route( '/', handleHome ) + + async def serve(self, request, obj ) -> HTMLResponse: + uid = request.scope["uid"] + fqn=normalize(findfqn(obj)) + print(uid,fqn, "->", importClassFromFqn(fqn) ) + + protocol = "wss" if self.ssl else "ws" + + if self.parano: + jsparano = crypto.JSCRYPTO + jsparano += f"\nvar _PARANO_='{self.parano}'\n" + jsparano += "\nasync function _read_(x) {return await decrypt(x,_PARANO_)}\n" + jsparano += "\nasync function _write_(x) {return await encrypt(x,_PARANO_)}\n" + else: + jsparano = "" + jsparano += "\nasync function _read_(x) {return x}\n" + jsparano += "\nasync function _write_(x) {return x}\n" + + + js = """ +%(jsparano)s + +async function interact( o ) { + _WS_.send( await _write_(JSON.stringify(o)) ); +} + +// instanciate the WEBSOCKET +let _WS_=null; +let retryms=500; + +function connect() { + _WS_= new WebSocket("%(protocol)s://"+location.host+"/_/%(fqn)s"); + _WS_.onopen=function(evt) { + console.log("** WS connected") + document.body.classList.remove("htagoff"); + retryms=500; + start(); + + _WS_.onmessage = async function(e) { + let actions = await _read_(e.data) + action(actions) + }; + + } + + _WS_.onclose = function(evt) { + console.log("** WS disconnected, retry in (ms):",retryms); + document.body.classList.add("htagoff"); + + setTimeout( function() { + connect(); + retryms=retryms*2; + }, retryms); + }; +} +connect(); + +""" % locals() + + p = HrPilot(uid,fqn,js) + + args,kargs = commons.url2ak(str(request.url)) + html=await p.start(*args,**kargs) + return HTMLResponse(html or "no?!") + + + + def run(self, host="0.0.0.0", port=8000, openBrowser=False): # localhost, by default !! + if openBrowser: + import webbrowser + webbrowser.open_new_tab(f"http://localhost:{port}") + + uvicorn.run(self, host=host, port=port) diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py new file mode 100644 index 0000000..e2c42c4 --- /dev/null +++ b/htagweb/server/__init__.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +# ############################################################################# +# Copyright (C) 2023 manatlan manatlan[at]gmail(dot)com +# +# MIT licence +# +# https://github.com/manatlan/htagweb +# ############################################################################# + +import asyncio +import redys +import os,sys,importlib,inspect +from htag import Tag + + +from multiprocessing import Process +from htag.render import HRenderer + + +EVENT_SERVER="EVENT_SERVER" + +CMD_EXIT="EXIT" +CMD_RENDER="RENDER" + +def importClassFromFqn(fqn_norm:str) -> type: + assert ":" in fqn_norm + #--------------------------- fqn -> module, name + modulename,name = fqn_norm.split(":",1) + if modulename in sys.modules: + module=sys.modules[modulename] + try: + module=importlib.reload( module ) + except ModuleNotFoundError as e: + """ can't be (really) reloaded if the component is in the + same module as the instance htag server""" + print(e) + pass + else: + module=importlib.import_module(modulename) + #--------------------------- + klass= getattr(module,name) + if not ( inspect.isclass(klass) and issubclass(klass,Tag) ): + raise Exception(f"'{fqn_norm}' is not a htag.Tag subclass") + + if not hasattr(klass,"imports"): + # if klass doesn't declare its imports + # we prefer to set them empty + # to avoid clutering + klass.imports=[] + return klass + + +def process(hid,event_response,event_interact,fqn,js,init): + pid = os.getpid() + async def loop(): + if os.getcwd() not in sys.path: sys.path.insert(0,os.getcwd()) + klass=importClassFromFqn(fqn) + + RUNNING=True + def exit(): + RUNNING=False + + session={} + styles=Tag.style("body.htagoff * {cursor:not-allowed !important;}") + + hr=HRenderer( klass ,js, init=init, exit_callback=exit, fullerror=True, statics=[styles,],session = session) + + # register the hr.sendactions, for tag.update feature + #TODO: implement tag.update !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + #TODO: implement tag.update !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + #TODO: implement tag.update !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + #hr.sendactions=lambda actions: self._sendback(websocket,json.dumps(actions)) + #TODO: implement tag.update !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + #TODO: implement tag.update !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + #TODO: implement tag.update !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + + print(f"Process {pid} started with :",hid,init,event_response,event_interact) + + with redys.AClient() as bus: + # publish the 1st rendering + await bus.publish(event_response,str(hr)) + + # subscribe for interaction + await bus.subscribe( event_interact ) + + while RUNNING: + params = await bus.get_event( event_interact ) + if params: + if params.get("cmd") == CMD_EXIT: + print(f"Process {pid} {hid} killed") + break + elif params.get("cmd") == CMD_RENDER: + # just a false start, just need the current render + print(f"Process {pid} just a render of {hid}") + await bus.publish(event_response,str(hr)) + else: + print(f"Process {pid} interact {hid}:",params) + #-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\- UT + if params["oid"]=="ut": params["oid"]=id(hr.tag) + #-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\- + + actions = await hr.interact(**params) + await bus.publish(event_response,actions) + + await asyncio.sleep(0.1) + + # prevent the server that this process is going dead + await bus.publish( EVENT_SERVER, dict(cmd="REMOVE",hid=hid) ) + + # unsubscribe for interaction + await bus.unsubscribe( event_interact ) + + asyncio.run( loop() ) + print(f"Process {pid} ended") + +async def starters(): + print("htag starters started") + with redys.AClient() as bus: + await bus.subscribe( EVENT_SERVER ) + + ps={} + + async def killall(ps:dict): + # try to send a EXIT CMD to all running ps + for hid,infos in ps.items(): + await bus.publish(infos["event_interact"],dict(cmd=CMD_EXIT)) + + while 1: + params = await bus.get_event( EVENT_SERVER ) + if params: + if params.get("cmd") == CMD_EXIT: + print(EVENT_SERVER, params.get("cmd") ) + break + elif params.get("cmd") == "CLEAN": + print(EVENT_SERVER, params.get("cmd") ) + await killall(ps) + continue + elif params.get("cmd") == "PS": + print(EVENT_SERVER, params.get("cmd") ) + from pprint import pprint + pprint(ps) + continue + elif params.get("cmd") == "REMOVE": + hid=params.get("hid") + print(EVENT_SERVER, params.get("cmd"),hid ) + del ps[hid] # remove from pool + continue + + hid=params["hid"] + key_init=str(params["init"]) + + if hid in ps: + # process is already running + + if key_init == ps[hid]["key"]: + # it's the same initialization process + + # so ask process to send back its render + await bus.publish(params["event_interact"],dict(cmd=CMD_RENDER)) + continue + else: + # kill itself because it's not the same init params + await bus.publish(params["event_interact"],dict(cmd=CMD_EXIT)) + # and recreate another one later + + # create the process + p=Process(target=process, args=[],kwargs=params) + p.start() + + # and save it in pool ps + ps[hid]=dict( process=p, key=key_init, event_interact=params["event_interact"]) + + await asyncio.sleep(0.1) + + await bus.unsubscribe( EVENT_SERVER ) + + await killall(ps) + + + print("htag starters stopped") + +async def hrserver(): + print("HRSERVER started") + + async def delay(): + await asyncio.sleep(2) + print("go") + await starters() + + asyncio.ensure_future( delay() ) + await redys.Server() + # asyncio.ensure_future( redys.Server() ) + # await starters() + + +if __name__=="__main__": + asyncio.run( hrserver() ) diff --git a/htagweb/server/client.py b/htagweb/server/client.py new file mode 100644 index 0000000..5e35e55 --- /dev/null +++ b/htagweb/server/client.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +# ############################################################################# +# Copyright (C) 2023 manatlan manatlan[at]gmail(dot)com +# +# MIT licence +# +# https://github.com/manatlan/htagweb +# ############################################################################# + +import uuid,asyncio +import redys,time +from htagweb.server import EVENT_SERVER + +TIMEOUT=30 # sec to wait answer from redys server + +class HrPilot: + def __init__(self,uid:str,fqn:str,js:str=None): + """ !!!!!!!!!!!!!!!!!!!! if js is None : can't do a start() !!!!!!!!!!!!!!!!!!!!!!""" + self.fqn=fqn + self.js=js + self.bus = redys.AClient() + + self.hid=f"{uid}_{fqn}" + self.event_response = f"response_{self.hid}" + self.event_interact = f"interact_{self.hid}" + + async def _wait(self,s=TIMEOUT): + # wait for a response + t1=time.monotonic() + while time.monotonic() - t1 < s: + message = await self.bus.get_event( self.event_response ) + if message: + return message + + await asyncio.sleep(0.1) + + return None + + async def start(self,*a,**k) -> str: + """ Start the defined app with this params (a,k) + (dialog with server event) + """ + assert self.js, "You should define the js in HrPilot() !!!!!!" + + # subscribe for response + await self.bus.subscribe( self.event_response ) + + # start the process app + await self.bus.publish( EVENT_SERVER , dict( + hid=self.hid, + event_response=self.event_response, + event_interact=self.event_interact, + fqn=self.fqn, + js=self.js, + init= (a,k), + )) + + # wait 1st rendering + html = await self._wait() + + return html + + async def kill(self): + """ Kill the process + (dialog with process event) + """ + await self.bus.publish( self.event_interact, dict(cmd="EXIT") ) + + + async def interact(self,**params) -> dict: + """ return htag'actions or None (if process doesn't answer, after timeout) + (dialog with process event) + """ + # subscribe for response + await self.bus.subscribe( self.event_response ) + + # post the interaction + await self.bus.publish( self.event_interact, params ) + + # wait actions + return await self._wait() + + + @staticmethod + async def list(): + """ SERVER COMMAND + (dialog with server event) + """ + with redys.AClient() as bus: + await bus.publish( EVENT_SERVER, dict(cmd="PS") ) + #~ @staticmethod + #~ async def stop(): + #~ with redys.AClient() as bus: + #~ await bus.publish( EVENT_SERVER, dict(cmd="EXIT") ) + @staticmethod + async def clean(): + """ SERVER COMMAND + (dialog with server event) + """ + with redys.AClient() as bus: + await bus.publish( EVENT_SERVER, dict(cmd="CLEAN") ) + + +async def main(): + uid ="u1" + p=HrPilot(uid,"obj:App","//") + #~ html=await p.start() + #~ print(html) + + #~ actions=await p.interact( oid="ut", method_name="doit", args=[], kargs={}, event={} ) + #~ print(actions) + + await p.kill() + await p.kill() + await p.kill() + #~ await p.kill() + #~ await p.kill() + #~ await HrPilot.list() + #~ await HrPilot.clean() + +if __name__=="__main__": + asyncio.run( main() ) diff --git a/poetry.lock b/poetry.lock index 60488bb..83ca973 100644 --- a/poetry.lock +++ b/poetry.lock @@ -509,6 +509,17 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "redys" +version = "0.9.7" +description = "A simple redis-like in pure python3, fully asyncio compliant" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "redys-0.9.7-py3-none-any.whl", hash = "sha256:2fe61bcfa0cdc46d3d203a13c0129f8bd71efbaab896f00cdfa8c6ac1593803a"}, + {file = "redys-0.9.7.tar.gz", hash = "sha256:31c698d015381c5fbd5a4d167daa591d816e4613e1b3af810ecb1f3d70890dc8"}, +] + [[package]] name = "sniffio" version = "1.3.0" @@ -761,4 +772,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "7387cb56c5cffd7486c9c507c556a2797469aa508a74e48777a3e34965d4bb52" +content-hash = "13908b74471200d7eca74787a1ea33c1f91c88ab63dd4bc7f29958e92bb3bc05" diff --git a/pyproject.toml b/pyproject.toml index 11dc6cc..2cd7f02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ htag = ">= 0.41.0" starlette = ">= 0.21.0" pycryptodomex = ">= 3.0.0" uvicorn = {version = ">=0.22,<1.0", extras = ["standard"]} +redys = "^0.9.7" [tool.poetry.dev-dependencies] pytest = "^7.0" From ac8c5daca69d6e5480706a3ccc20cb3d3a10fa5c Mon Sep 17 00:00:00 2001 From: manatlan Date: Sat, 30 Sep 2023 13:02:44 +0200 Subject: [PATCH 02/26] now start the hrserver ;-) --- example.py | 9 +++------ htagweb/redysserver.py | 26 ++++++++++++++++++++++---- htagweb/server/__init__.py | 18 +++++++++--------- poetry.lock | 14 +++++++------- pyproject.toml | 2 +- 5 files changed, 42 insertions(+), 27 deletions(-) diff --git a/example.py b/example.py index 9f22c0b..798721d 100644 --- a/example.py +++ b/example.py @@ -51,9 +51,6 @@ def clllll(o): #------------------------------------------------------ if __name__=="__main__": - try: - from htagweb import AppServer,RedysServer - app=RedysServer( "example:App" ,parano=False) - app.run(openBrowser=False) - except: - pass + from htagweb import AppServer,RedysServer + app=RedysServer( "example:App" ,parano=False) + app.run(openBrowser=True) diff --git a/htagweb/redysserver.py b/htagweb/redysserver.py index 232cb95..a26f7fb 100644 --- a/htagweb/redysserver.py +++ b/htagweb/redysserver.py @@ -31,7 +31,7 @@ from htag.runners import commons from . import crypto,usot -from htagweb.server import importClassFromFqn, hrserver +from htagweb.server import importClassFromFqn, hrserver, starters from htagweb.server.client import HrPilot logger = logging.getLogger(__name__) @@ -149,6 +149,15 @@ async def on_receive(self, websocket, data): async def on_disconnect(self, websocket, close_code): pass +def process1(): + import redys + # asyncio.ensure_future( starters() ) + asyncio.run( redys.Server() ) + +def process2(): + import redys + asyncio.run( starters() ) + class RedysServer(Starlette): #NOT THE DEFINITIVE NAME !!!!!!!!!!!!!!!! def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True,ssl:bool=False,parano:bool=False,sesprovider:"htagweb.sessions.create*|None"=None): self.ssl=ssl @@ -156,9 +165,18 @@ def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True,ssl:bool=F self.parano = str(uuid.uuid4()) if parano else None if sesprovider is None: sesprovider = sessions.createFile - - ##asyncio.ensure_future( hrserver() ) - + ################################################################### + import redys + # asyncio.ensure_future( redys.Server() ) + # asyncio.ensure_future( hrserver() ) + + import multiprocessing + p=multiprocessing.Process(target=process1) + p.start() + p=multiprocessing.Process(target=process2) + p.start() + + ################################################################# Starlette.__init__( self, debug=debug, routes=[WebSocketRoute("/_/{fqn}", HRSocket)], diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index e2c42c4..cfacaa8 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -183,15 +183,15 @@ async def killall(ps:dict): async def hrserver(): print("HRSERVER started") - async def delay(): - await asyncio.sleep(2) - print("go") - await starters() - - asyncio.ensure_future( delay() ) - await redys.Server() - # asyncio.ensure_future( redys.Server() ) - # await starters() + # async def delay(): + # await asyncio.sleep(2) + # print("go") + # await starters() + + # asyncio.ensure_future( delay() ) + # await redys.Server() + asyncio.ensure_future( redys.Server() ) + await starters() if __name__=="__main__": diff --git a/poetry.lock b/poetry.lock index 83ca973..21703af 100644 --- a/poetry.lock +++ b/poetry.lock @@ -164,13 +164,13 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "htag" -version = "0.41.0" +version = "0.42.0" description = "GUI toolkit for building GUI toolkits (and create beautiful applications for mobile, web, and desktop from a single python3 codebase)" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "htag-0.41.0-py3-none-any.whl", hash = "sha256:1abe313201ed5c99fd19e91fdff059a89bca508fc5c5262bfe4f200c0e7b0686"}, - {file = "htag-0.41.0.tar.gz", hash = "sha256:a4f3f53eac49c7efae03d7fa5e1badf3f57c7d4db3ad55020504ed300b009084"}, + {file = "htag-0.42.0-py3-none-any.whl", hash = "sha256:b856b20eff5944e856bca3456f6313d2582b14287e47d746b848d14a4cd32acc"}, + {file = "htag-0.42.0.tar.gz", hash = "sha256:3c6c226af1e75fd54f3c6bb1e4a6d8ccc449a8da201a2fedc932711bcaa7ac60"}, ] [[package]] @@ -511,13 +511,13 @@ files = [ [[package]] name = "redys" -version = "0.9.7" +version = "0.9.8" description = "A simple redis-like in pure python3, fully asyncio compliant" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "redys-0.9.7-py3-none-any.whl", hash = "sha256:2fe61bcfa0cdc46d3d203a13c0129f8bd71efbaab896f00cdfa8c6ac1593803a"}, - {file = "redys-0.9.7.tar.gz", hash = "sha256:31c698d015381c5fbd5a4d167daa591d816e4613e1b3af810ecb1f3d70890dc8"}, + {file = "redys-0.9.8-py3-none-any.whl", hash = "sha256:fb27f7cc982363835089f115a66c7da56c8ff67897e556a2276e69ce043fe517"}, + {file = "redys-0.9.8.tar.gz", hash = "sha256:b87fa13b16e9d1bdbef5b43868b7652dda408b1e4c0eee9dca5e358de43d2ca8"}, ] [[package]] @@ -772,4 +772,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "13908b74471200d7eca74787a1ea33c1f91c88ab63dd4bc7f29958e92bb3bc05" +content-hash = "df8b95784ef698262c8ebef15bdc4a1259a090f433262075840ad4c7464740c1" diff --git a/pyproject.toml b/pyproject.toml index 2cd7f02..f751907 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ htag = ">= 0.41.0" starlette = ">= 0.21.0" pycryptodomex = ">= 3.0.0" uvicorn = {version = ">=0.22,<1.0", extras = ["standard"]} -redys = "^0.9.7" +redys = "^0.9.8" [tool.poetry.dev-dependencies] pytest = "^7.0" From 24cbe8d7d36b0cfaa0ffd2b470e1756f3d7a082e Mon Sep 17 00:00:00 2001 From: manatlan Date: Sat, 30 Sep 2023 13:57:37 +0200 Subject: [PATCH 03/26] style: renaming and good hrserver start --- __init__.py | 0 example.py | 4 +- examples/main.py | 4 +- examples/oauth_example.py | 4 +- htagweb/__init__.py | 4 +- htagweb/appserver.py | 245 ++++++------------- htagweb/htagserver.py | 8 +- htagweb/server/__init__.py | 28 ++- htagweb/{redysserver.py => simpleserver.py} | 255 +++++++++++++------- test_htagserver.py | 6 +- 10 files changed, 278 insertions(+), 280 deletions(-) delete mode 100644 __init__.py rename htagweb/{redysserver.py => simpleserver.py} (51%) diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/example.py b/example.py index 798721d..78315b6 100644 --- a/example.py +++ b/example.py @@ -49,8 +49,8 @@ def clllll(o): # With htagweb.WebServer runner provided by htagweb #------------------------------------------------------ +from htagweb import SimpleServer,AppServer +app=AppServer( App ,parano=False) if __name__=="__main__": - from htagweb import AppServer,RedysServer - app=RedysServer( "example:App" ,parano=False) app.run(openBrowser=True) diff --git a/examples/main.py b/examples/main.py index 29a7d4c..54c350a 100644 --- a/examples/main.py +++ b/examples/main.py @@ -1,6 +1,6 @@ import os,sys; sys.path.insert(0,os.path.realpath(os.path.dirname(os.path.dirname(__file__)))) -from htagweb import AppServer +from htagweb import SimpleServer from starlette.responses import HTMLResponse import app1 @@ -43,7 +43,7 @@ async def handlePath(request): return HTMLResponse("404",404) -app=AppServer() +app=SimpleServer() app.add_route("/{path:path}", handlePath ) if __name__=="__main__": diff --git a/examples/oauth_example.py b/examples/oauth_example.py index 804c02b..63dc8e8 100644 --- a/examples/oauth_example.py +++ b/examples/oauth_example.py @@ -1,7 +1,7 @@ import os,sys; sys.path.insert(0,os.path.realpath(os.path.dirname(os.path.dirname(__file__)))) from htag import Tag -from htagweb import AppServer +from htagweb import SimpleServer from authlib.integrations.starlette_client import OAuth from starlette.responses import Response,RedirectResponse import time @@ -92,7 +92,7 @@ def render(self): # dynamic rendering #========================================= # IT WORKS FOR THE 3 runners of htagweb ;-) (should work with old webhttp/webws runners from htag too) -app=AppServer(App) +app=SimpleServer(App) app.add_route("/oauth_{action}", oauth_request_action ) diff --git a/htagweb/__init__.py b/htagweb/__init__.py index ae3018f..5f994d5 100644 --- a/htagweb/__init__.py +++ b/htagweb/__init__.py @@ -7,8 +7,8 @@ # https://github.com/manatlan/htagweb # ############################################################################# -from .redysserver import RedysServer #NOT THE DEFINITIVE NAME !!!!!!!!!!!!!!!! -from .appserver import AppServer # a completly different beast, but compatible with ^^ +from .appserver import AppServer +from .simpleserver import SimpleServer # a completly different beast, but compatible with ^^ from .htagserver import HtagServer # a completly different beast. from .usot import Usot diff --git a/htagweb/appserver.py b/htagweb/appserver.py index 9177415..ef22c38 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -9,37 +9,15 @@ # gunicorn -w 4 -k uvicorn.workers.UvicornWorker -b localhost:8000 --preload basic:app -""" -IDEM que htagweb.AppServer -- mais sans le SHARED MEMORY DICT (donc compat py3.7) ... grace au sesprovider ! -- fichier solo -- !!! utilise crypto de htagweb !!! - - - -This thing is completly new different beast, and doesn't work as all classic runners. - -It's a runner between "WebHTTP/WebServer" & "HtagServer" : the best of two worlds - - - It's fully compatible with WebHTTP/WebServer (provide a serve method) - - it use same techs as HTagServer (WS only, parano mode, simple/#workers, etc ...) - - and the SEO trouble is faked by a pre-fake-rendering (it create a hr on http, for seo ... and recreate a real one at WS connect) - -Like HTagServer, as lifespan of htag instances is completly changed : -htag instances should base their state on "self.root.state" only! -Because a F5 will always destroy/recreate the instance. -""" import os import sys import json import uuid -import pickle -import inspect import logging import uvicorn -import importlib -import contextlib +import asyncio +import multiprocessing from htag import Tag from starlette.applications import Starlette from starlette.responses import HTMLResponse @@ -51,10 +29,12 @@ from starlette.datastructures import MutableHeaders from starlette.types import ASGIApp, Message, Receive, Scope, Send -from htag.render import HRenderer from htag.runners import commons from . import crypto,usot +from htagweb.server import importClassFromFqn, hrserver, hrserver_orchestrator +from htagweb.server.client import HrPilot + logger = logging.getLogger(__name__) #################################################### from types import ModuleType @@ -80,31 +60,7 @@ def findfqn(x) -> str: return tagClass.__module__+"."+tagClass.__qualname__ -def getClass(fqn_norm:str) -> type: - assert ":" in fqn_norm - #--------------------------- fqn -> module, name - modulename,name = fqn_norm.split(":",1) - if modulename in sys.modules: - module=sys.modules[modulename] - try: - module=importlib.reload( module ) - except ModuleNotFoundError: - """ can't be (really) reloaded if the component is in the - same module as the instance htag server""" - pass - else: - module=importlib.import_module(modulename) - #--------------------------- - klass= getattr(module,name) - if not ( inspect.isclass(klass) and issubclass(klass,Tag) ): - raise Exception(f"'{fqn_norm}' is not a htag.Tag subclass") - if not hasattr(klass,"imports"): - # if klass doesn't declare its imports - # we prefer to set them empty - # to avoid clutering - klass.imports=[] - return klass class WebServerSession: # ASGI Middleware, for starlette @@ -154,16 +110,11 @@ async def send_wrapper(message: Message) -> None: await self.app(scope, receive, send_wrapper) -def fqn2hr(fqn:str,js:str,init,session,fullerror=False): # fqn is a "full qualified name", full ! +def normalize(fqn): if ":" not in fqn: # replace last "." by ":" fqn="".join( reversed("".join(reversed(fqn)).replace(".",":",1))) - - klass=getClass(fqn) - - styles=Tag.style("body.htagoff * {cursor:not-allowed !important;}") - - return HRenderer( klass, js, init=init, session = session, fullerror=fullerror, statics=[styles,]) + return fqn class HRSocket(WebSocketEndpoint): encoding = "text" @@ -180,62 +131,42 @@ async def _sendback(self,ws, txt:str) -> bool: return False async def on_connect(self, websocket): - fqn=websocket.path_params.get("fqn","") - await websocket.accept() - js=""" -// rewrite the onmessage of the _WS_ to interpret json action now ! -_WS_.onmessage = async function(e) { - let actions = await _read_(e.data) - action(actions) -} - -// declare the interact js method to communicate thru the WS -async function interact( o ) { - _WS_.send( await _write_(JSON.stringify(o)) ); -} - -console.log("started") -""" - - try: - hr=fqn2hr(fqn,js,commons.url2ak(str(websocket.url)),websocket.session,fullerror=websocket.app.debug) - except Exception as e: - await self._sendback( websocket, str(e) ) - await websocket.close() - return - - self.hr=hr - - # send back the full rendering (1st onmessage after js connection) - await self._sendback( websocket, str(self.hr) ) - - # register the hr.sendactions, for tag.update feature - self.hr.sendactions=lambda actions: self._sendback(websocket,json.dumps(actions)) async def on_receive(self, websocket, data): + fqn=websocket.path_params.get("fqn","") + uid=websocket.scope["uid"] + if websocket.app.parano: data = crypto.decrypt(data.encode(),websocket.app.parano).decode() - data=json.loads(data) - #=================================== for UT only - if data["id"]=="ut": - data["id"]=id(self.hr.tag) - #=================================== + p=HrPilot(uid,fqn) + + actions=await p.interact( oid=data["id"], method_name=data["method"], args=data["args"], kargs=data["kargs"], event=data.get("event") ) - actions = await self.hr.interact(data["id"],data["method"],data["args"],data["kargs"],data.get("event")) await self._sendback( websocket, json.dumps(actions) ) async def on_disconnect(self, websocket, close_code): - del self.hr + pass + +def processHrServer(): + asyncio.run( hrserver() ) -class AppServer(Starlette): + +class AppServer(Starlette): #NOT THE DEFINITIVE NAME !!!!!!!!!!!!!!!! def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True,ssl:bool=False,parano:bool=False,sesprovider:"htagweb.sessions.create*|None"=None): self.ssl=ssl + #TODO: NOT GOOD (when socket disconnet panaro uid change ... so broken !!!!) self.parano = str(uuid.uuid4()) if parano else None if sesprovider is None: sesprovider = sessions.createFile + ################################################################### + + p=multiprocessing.Process(target=processHrServer) + p.start() + + ################################################################# Starlette.__init__( self, debug=debug, routes=[WebSocketRoute("/_/{fqn}", HRSocket)], @@ -248,8 +179,12 @@ async def handleHome(request): self.add_route( '/', handleHome ) async def serve(self, request, obj ) -> HTMLResponse: - fqn=findfqn(obj) + uid = request.scope["uid"] + fqn=normalize(findfqn(obj)) + print(uid,fqn, "->", importClassFromFqn(fqn) ) + protocol = "wss" if self.ssl else "ws" + if self.parano: jsparano = crypto.JSCRYPTO jsparano += f"\nvar _PARANO_='{self.parano}'\n" @@ -259,77 +194,55 @@ async def serve(self, request, obj ) -> HTMLResponse: jsparano = "" jsparano += "\nasync function _read_(x) {return x}\n" jsparano += "\nasync function _write_(x) {return x}\n" - #TODO: consider https://developer.chrome.com/blog/removing-document-write/ - - jsbootstrap=""" - %(jsparano)s - // instanciate the WEBSOCKET - let _WS_=null; - let retryms=500; - - function connect() { - _WS_= new WebSocket("%(protocol)s://"+location.host+"/_/%(fqn)s"+location.search); - _WS_.onopen=function(evt) { - console.log("** WS connected") - document.body.classList.remove("htagoff"); - retryms=500; - - _WS_.onmessage = async function(e) { - // when connected -> the full HTML page is returned, installed & start'ed !!! - - let html = await _read_(e.data); - html = html.replace(" - # - # - # - # - # loading - # - # """ % locals() - - # return HTMLResponse( bootstrapHtmlPage ) + retryms=retryms*2; + }, retryms); + }; +} +connect(); + +""" % locals() + + p = HrPilot(uid,fqn,js) + + args,kargs = commons.url2ak(str(request.url)) + html=await p.start(*args,**kargs) + return HTMLResponse(html or "no?!") + + def run(self, host="0.0.0.0", port=8000, openBrowser=False): # localhost, by default !! if openBrowser: diff --git a/htagweb/htagserver.py b/htagweb/htagserver.py index 4ac9a58..d82a02c 100644 --- a/htagweb/htagserver.py +++ b/htagweb/htagserver.py @@ -10,7 +10,7 @@ # gunicorn -w 4 -k uvicorn.workers.UvicornWorker -b localhost:8000 --preload basic:app """ -This thing is AppServer, but with 2 majors behaviour +This thing is HtagServer, but with 2 majors behaviour - If "no klass"(None) is defined -> will hook "/" on IndexApp (a browser of folders/files) - every others routes -> will try to instanciate an htag app @@ -23,7 +23,7 @@ from starlette.responses import HTMLResponse,Response from htag import Tag -from .appserver import AppServer,getClass +from .simpleserver import SimpleServer,getClass #################################################### class IndexApp(Tag.body): @@ -60,11 +60,11 @@ def init(self,path="."): #################################################### -class HtagServer(AppServer): +class HtagServer(SimpleServer): def __init__(self,obj:"htag.Tag class|fqn|None"=None, *a,**k): if obj is None: obj = IndexApp - AppServer.__init__(self,obj,*a,**k) + SimpleServer.__init__(self,obj,*a,**k) self.add_route('/{path}', self._serve) diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index cfacaa8..0fecf56 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -114,9 +114,18 @@ def exit(): asyncio.run( loop() ) print(f"Process {pid} ended") -async def starters(): - print("htag starters started") +async def hrserver_orchestrator(): with redys.AClient() as bus: + + # prevent multi orchestrators + if await bus.get("hrserver_orchestrator_running")==True: + print("hrserver_orchestrator is already running") + return + else: + print("hrserver_orchestrator started") + await bus.set("hrserver_orchestrator_running",True) + + # register its main event await bus.subscribe( EVENT_SERVER ) ps={} @@ -180,18 +189,17 @@ async def killall(ps:dict): print("htag starters stopped") + + async def hrserver(): print("HRSERVER started") - # async def delay(): - # await asyncio.sleep(2) - # print("go") - # await starters() + async def delay(): + await asyncio.sleep(0.5) + await hrserver_orchestrator() - # asyncio.ensure_future( delay() ) - # await redys.Server() - asyncio.ensure_future( redys.Server() ) - await starters() + asyncio.ensure_future( delay() ) + await redys.Server() if __name__=="__main__": diff --git a/htagweb/redysserver.py b/htagweb/simpleserver.py similarity index 51% rename from htagweb/redysserver.py rename to htagweb/simpleserver.py index a26f7fb..270d21c 100644 --- a/htagweb/redysserver.py +++ b/htagweb/simpleserver.py @@ -9,14 +9,37 @@ # gunicorn -w 4 -k uvicorn.workers.UvicornWorker -b localhost:8000 --preload basic:app +""" +IDEM que htagweb.AppServer +- mais sans le SHARED MEMORY DICT (donc compat py3.7) ... grace au sesprovider ! +- fichier solo +- !!! utilise crypto de htagweb !!! + + + +This thing is completly new different beast, and doesn't work as all classic runners. + +It's a runner between "WebHTTP/WebServer" & "HtagServer" : the best of two worlds + + - It's fully compatible with WebHTTP/WebServer (provide a serve method) + - it use same techs as HTagServer (WS only, parano mode, simple/#workers, etc ...) + - and the SEO trouble is faked by a pre-fake-rendering (it create a hr on http, for seo ... and recreate a real one at WS connect) + +Like HTagServer, as lifespan of htag instances is completly changed : +htag instances should base their state on "self.root.state" only! +Because a F5 will always destroy/recreate the instance. +""" import os import sys import json import uuid +import pickle +import inspect import logging import uvicorn -import asyncio +import importlib +import contextlib from htag import Tag from starlette.applications import Starlette from starlette.responses import HTMLResponse @@ -28,12 +51,10 @@ from starlette.datastructures import MutableHeaders from starlette.types import ASGIApp, Message, Receive, Scope, Send +from htag.render import HRenderer from htag.runners import commons from . import crypto,usot -from htagweb.server import importClassFromFqn, hrserver, starters -from htagweb.server.client import HrPilot - logger = logging.getLogger(__name__) #################################################### from types import ModuleType @@ -59,7 +80,31 @@ def findfqn(x) -> str: return tagClass.__module__+"."+tagClass.__qualname__ +def getClass(fqn_norm:str) -> type: + assert ":" in fqn_norm + #--------------------------- fqn -> module, name + modulename,name = fqn_norm.split(":",1) + if modulename in sys.modules: + module=sys.modules[modulename] + try: + module=importlib.reload( module ) + except ModuleNotFoundError: + """ can't be (really) reloaded if the component is in the + same module as the instance htag server""" + pass + else: + module=importlib.import_module(modulename) + #--------------------------- + klass= getattr(module,name) + if not ( inspect.isclass(klass) and issubclass(klass,Tag) ): + raise Exception(f"'{fqn_norm}' is not a htag.Tag subclass") + if not hasattr(klass,"imports"): + # if klass doesn't declare its imports + # we prefer to set them empty + # to avoid clutering + klass.imports=[] + return klass class WebServerSession: # ASGI Middleware, for starlette @@ -109,11 +154,16 @@ async def send_wrapper(message: Message) -> None: await self.app(scope, receive, send_wrapper) -def normalize(fqn): +def fqn2hr(fqn:str,js:str,init,session,fullerror=False): # fqn is a "full qualified name", full ! if ":" not in fqn: # replace last "." by ":" fqn="".join( reversed("".join(reversed(fqn)).replace(".",":",1))) - return fqn + + klass=getClass(fqn) + + styles=Tag.style("body.htagoff * {cursor:not-allowed !important;}") + + return HRenderer( klass, js, init=init, session = session, fullerror=fullerror, statics=[styles,]) class HRSocket(WebSocketEndpoint): encoding = "text" @@ -130,53 +180,62 @@ async def _sendback(self,ws, txt:str) -> bool: return False async def on_connect(self, websocket): + fqn=websocket.path_params.get("fqn","") + await websocket.accept() + js=""" +// rewrite the onmessage of the _WS_ to interpret json action now ! +_WS_.onmessage = async function(e) { + let actions = await _read_(e.data) + action(actions) +} - async def on_receive(self, websocket, data): - fqn=websocket.path_params.get("fqn","") - uid=websocket.scope["uid"] +// declare the interact js method to communicate thru the WS +async function interact( o ) { + _WS_.send( await _write_(JSON.stringify(o)) ); +} + +console.log("started") +""" + + try: + hr=fqn2hr(fqn,js,commons.url2ak(str(websocket.url)),websocket.session,fullerror=websocket.app.debug) + except Exception as e: + await self._sendback( websocket, str(e) ) + await websocket.close() + return + + self.hr=hr + + # send back the full rendering (1st onmessage after js connection) + await self._sendback( websocket, str(self.hr) ) + # register the hr.sendactions, for tag.update feature + self.hr.sendactions=lambda actions: self._sendback(websocket,json.dumps(actions)) + + async def on_receive(self, websocket, data): if websocket.app.parano: data = crypto.decrypt(data.encode(),websocket.app.parano).decode() - data=json.loads(data) - p=HrPilot(uid,fqn) + data=json.loads(data) - actions=await p.interact( oid=data["id"], method_name=data["method"], args=data["args"], kargs=data["kargs"], event=data.get("event") ) + #=================================== for UT only + if data["id"]=="ut": + data["id"]=id(self.hr.tag) + #=================================== + actions = await self.hr.interact(data["id"],data["method"],data["args"],data["kargs"],data.get("event")) await self._sendback( websocket, json.dumps(actions) ) async def on_disconnect(self, websocket, close_code): - pass + del self.hr -def process1(): - import redys - # asyncio.ensure_future( starters() ) - asyncio.run( redys.Server() ) - -def process2(): - import redys - asyncio.run( starters() ) - -class RedysServer(Starlette): #NOT THE DEFINITIVE NAME !!!!!!!!!!!!!!!! +class SimpleServer(Starlette): def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True,ssl:bool=False,parano:bool=False,sesprovider:"htagweb.sessions.create*|None"=None): self.ssl=ssl - #TODO: NOT GOOD (when socket disconnet panaro uid change ... so broken !!!!) self.parano = str(uuid.uuid4()) if parano else None if sesprovider is None: sesprovider = sessions.createFile - ################################################################### - import redys - # asyncio.ensure_future( redys.Server() ) - # asyncio.ensure_future( hrserver() ) - - import multiprocessing - p=multiprocessing.Process(target=process1) - p.start() - p=multiprocessing.Process(target=process2) - p.start() - - ################################################################# Starlette.__init__( self, debug=debug, routes=[WebSocketRoute("/_/{fqn}", HRSocket)], @@ -189,12 +248,8 @@ async def handleHome(request): self.add_route( '/', handleHome ) async def serve(self, request, obj ) -> HTMLResponse: - uid = request.scope["uid"] - fqn=normalize(findfqn(obj)) - print(uid,fqn, "->", importClassFromFqn(fqn) ) - + fqn=findfqn(obj) protocol = "wss" if self.ssl else "ws" - if self.parano: jsparano = crypto.JSCRYPTO jsparano += f"\nvar _PARANO_='{self.parano}'\n" @@ -204,55 +259,77 @@ async def serve(self, request, obj ) -> HTMLResponse: jsparano = "" jsparano += "\nasync function _read_(x) {return x}\n" jsparano += "\nasync function _write_(x) {return x}\n" - - - js = """ -%(jsparano)s - -async function interact( o ) { - _WS_.send( await _write_(JSON.stringify(o)) ); -} - -// instanciate the WEBSOCKET -let _WS_=null; -let retryms=500; - -function connect() { - _WS_= new WebSocket("%(protocol)s://"+location.host+"/_/%(fqn)s"); - _WS_.onopen=function(evt) { - console.log("** WS connected") - document.body.classList.remove("htagoff"); - retryms=500; - start(); - - _WS_.onmessage = async function(e) { - let actions = await _read_(e.data) - action(actions) - }; - - } - - _WS_.onclose = function(evt) { - console.log("** WS disconnected, retry in (ms):",retryms); - document.body.classList.add("htagoff"); - - setTimeout( function() { + #TODO: consider https://developer.chrome.com/blog/removing-document-write/ + + jsbootstrap=""" + %(jsparano)s + // instanciate the WEBSOCKET + let _WS_=null; + let retryms=500; + + function connect() { + _WS_= new WebSocket("%(protocol)s://"+location.host+"/_/%(fqn)s"+location.search); + _WS_.onopen=function(evt) { + console.log("** WS connected") + document.body.classList.remove("htagoff"); + retryms=500; + + _WS_.onmessage = async function(e) { + // when connected -> the full HTML page is returned, installed & start'ed !!! + + let html = await _read_(e.data); + html = html.replace(" + # + # + # + # + # loading + # + # """ % locals() + + # return HTMLResponse( bootstrapHtmlPage ) def run(self, host="0.0.0.0", port=8000, openBrowser=False): # localhost, by default !! if openBrowser: diff --git a/test_htagserver.py b/test_htagserver.py index 60a3a8f..6748d86 100644 --- a/test_htagserver.py +++ b/test_htagserver.py @@ -1,6 +1,6 @@ import pytest from htag import Tag -from htagweb import HtagServer,AppServer +from htagweb import HtagServer,SimpleServer import sys,json from starlette.testclient import TestClient @@ -54,8 +54,8 @@ def do_tests(): do_tests() -def test_appserver(): - app=AppServer( "test_hr:App" ) +def test_simpleserver(): + app=SimpleServer( "test_hr:App" ) with TestClient(app) as client: response = client.get('/') From 74123d6b6dd9fd4ca12b7a41f4ac99d75c81e3d0 Mon Sep 17 00:00:00 2001 From: manatlan Date: Sat, 30 Sep 2023 13:58:36 +0200 Subject: [PATCH 04/26] test: fix --- test_htagserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_htagserver.py b/test_htagserver.py index 6748d86..49e5d87 100644 --- a/test_htagserver.py +++ b/test_htagserver.py @@ -94,4 +94,4 @@ def test_parano(): if __name__=="__main__": # test_basic() # test_a_full_fqn() - test_appserver() \ No newline at end of file + test_simpleserver() \ No newline at end of file From 38055d7bc70eebc47cdce31254a10cb98fe4e5eb Mon Sep 17 00:00:00 2001 From: manatlan Date: Sat, 30 Sep 2023 15:55:35 +0200 Subject: [PATCH 05/26] sessions seems to work as expected --- example.py | 7 ++-- htagweb/__init__.py | 6 ++-- htagweb/appserver.py | 39 ++++++++++++--------- htagweb/server/__init__.py | 32 +++++++++-------- htagweb/server/client.py | 6 ++-- htagweb/sessions/__init__.py | 6 +--- htagweb/sessions/memory.py | 67 ------------------------------------ memserver.py | 9 ----- test_hr.py | 2 ++ test_server.py | 43 +++++++++++++++++++++++ test_sessions.py | 34 +++++++++++------- test_usot.py | 2 +- 12 files changed, 121 insertions(+), 132 deletions(-) delete mode 100644 htagweb/sessions/memory.py delete mode 100644 memserver.py create mode 100644 test_server.py diff --git a/example.py b/example.py index 78315b6..62a8a64 100644 --- a/example.py +++ b/example.py @@ -15,7 +15,7 @@ def init(self): def render(self): self.otitle.set( "Live Session" ) - self.orendu.set( json.dumps( dict(self.session), indent=1)) + self.orendu.set( json.dumps( dict(self.session.items()), indent=1)) class App(Tag.body): imports=[TagSession] @@ -39,6 +39,9 @@ def clllll(o): self <= Tag.button("clear",_onclick=clllll) self <= TagSession() + self+=Tag.li(Tag.a("t0",_href="/")) + self+=Tag.li(Tag.a("t1",_href="/?a=43")) + self+=Tag.li(Tag.a("t2",_href="/?z=bb")) @@ -53,4 +56,4 @@ def clllll(o): app=AppServer( App ,parano=False) if __name__=="__main__": - app.run(openBrowser=True) + app.run(openBrowser=False) diff --git a/htagweb/__init__.py b/htagweb/__init__.py index 5f994d5..7942e18 100644 --- a/htagweb/__init__.py +++ b/htagweb/__init__.py @@ -7,11 +7,11 @@ # https://github.com/manatlan/htagweb # ############################################################################# +__version__ = "0.0.0" # auto updated + from .appserver import AppServer from .simpleserver import SimpleServer # a completly different beast, but compatible with ^^ from .htagserver import HtagServer # a completly different beast. -from .usot import Usot -__all__= ["AppServer"] +__all__= ["AppServer","SimpleServer"] -__version__ = "0.0.0" # auto updated diff --git a/htagweb/appserver.py b/htagweb/appserver.py index ef22c38..9260ea4 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -17,6 +17,7 @@ import logging import uvicorn import asyncio +import hashlib import multiprocessing from htag import Tag from starlette.applications import Starlette @@ -61,8 +62,6 @@ def findfqn(x) -> str: return tagClass.__module__+"."+tagClass.__qualname__ - - class WebServerSession: # ASGI Middleware, for starlette def __init__(self, app:ASGIApp, https_only:bool = False, sesprovider:"async method(uid)"=None ) -> None: self.app = app @@ -88,6 +87,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: #!!!!!!!!!!!!!!!!!!!!!!!!!!! scope["uid"] = uid + scope["parano"] = hashlib.md5(uid.encode()).hexdigest() scope["session"] = await self.cbsesprovider(uid) #!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -119,12 +119,13 @@ def normalize(fqn): class HRSocket(WebSocketEndpoint): encoding = "text" - async def _sendback(self,ws, txt:str) -> bool: + async def _sendback(self,websocket, txt:str) -> bool: + parano_seed = websocket.scope["parano"] if websocket.app.parano else None try: - if ws.app.parano: - txt = crypto.encrypt(txt.encode(),ws.app.parano) + if parano_seed: + txt = crypto.encrypt(txt.encode(),parano_seed) - await ws.send_text( txt ) + await websocket.send_text( txt ) return True except Exception as e: logger.error("Can't send to socket, error: %s",e) @@ -136,9 +137,10 @@ async def on_connect(self, websocket): async def on_receive(self, websocket, data): fqn=websocket.path_params.get("fqn","") uid=websocket.scope["uid"] + parano_seed = websocket.scope["parano"] if websocket.app.parano else None - if websocket.app.parano: - data = crypto.decrypt(data.encode(),websocket.app.parano).decode() + if parano_seed: + data = crypto.decrypt(data.encode(),parano_seed).decode() data=json.loads(data) p=HrPilot(uid,fqn) @@ -157,10 +159,14 @@ def processHrServer(): class AppServer(Starlette): #NOT THE DEFINITIVE NAME !!!!!!!!!!!!!!!! def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True,ssl:bool=False,parano:bool=False,sesprovider:"htagweb.sessions.create*|None"=None): self.ssl=ssl - #TODO: NOT GOOD (when socket disconnet panaro uid change ... so broken !!!!) - self.parano = str(uuid.uuid4()) if parano else None + self.parano=parano + if sesprovider is None: - sesprovider = sessions.createFile + self.sesprovider = sessions.createFile + else: + self.sesprovider = sesprovider + + print("Session with:",self.sesprovider.__name__) ################################################################### p=multiprocessing.Process(target=processHrServer) @@ -170,7 +176,7 @@ def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True,ssl:bool=F Starlette.__init__( self, debug=debug, routes=[WebSocketRoute("/_/{fqn}", HRSocket)], - middleware=[Middleware(WebServerSession,https_only=ssl,sesprovider=sesprovider)], + middleware=[Middleware(WebServerSession,https_only=ssl,sesprovider=self.sesprovider)], ) if obj: @@ -180,14 +186,15 @@ async def handleHome(request): async def serve(self, request, obj ) -> HTMLResponse: uid = request.scope["uid"] + parano_seed = request.scope["parano"] if self.parano else None + fqn=normalize(findfqn(obj)) - print(uid,fqn, "->", importClassFromFqn(fqn) ) protocol = "wss" if self.ssl else "ws" - if self.parano: + if parano_seed: jsparano = crypto.JSCRYPTO - jsparano += f"\nvar _PARANO_='{self.parano}'\n" + jsparano += f"\nvar _PARANO_='{parano_seed}'\n" jsparano += "\nasync function _read_(x) {return await decrypt(x,_PARANO_)}\n" jsparano += "\nasync function _write_(x) {return await encrypt(x,_PARANO_)}\n" else: @@ -236,7 +243,7 @@ async def serve(self, request, obj ) -> HTMLResponse: """ % locals() - p = HrPilot(uid,fqn,js) + p = HrPilot(uid,fqn,js,self.sesprovider.__name__) args,kargs = commons.url2ak(str(request.url)) html=await p.start(*args,**kargs) diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index 0fecf56..5babbcd 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -49,8 +49,15 @@ def importClassFromFqn(fqn_norm:str) -> type: klass.imports=[] return klass +import htagweb.sessions + +def process(hid,event_response,event_interact,fqn,js,init,sesprovidername): + if sesprovidername is None: + sesprovidername="createFile" + createSession=getattr(htagweb.sessions,sesprovidername) + + uid=hid.split("_")[0] -def process(hid,event_response,event_interact,fqn,js,init): pid = os.getpid() async def loop(): if os.getcwd() not in sys.path: sys.path.insert(0,os.getcwd()) @@ -60,7 +67,10 @@ async def loop(): def exit(): RUNNING=False - session={} + print(uid) + session = await createSession(uid) + #session={} + styles=Tag.style("body.htagoff * {cursor:not-allowed !important;}") hr=HRenderer( klass ,js, init=init, exit_callback=exit, fullerror=True, statics=[styles,],session = session) @@ -75,7 +85,7 @@ def exit(): #TODO: implement tag.update !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - print(f"Process {pid} started with :",hid,init,event_response,event_interact) + print(f">Process {pid} started with :",hid,init,event_response,event_interact) with redys.AClient() as bus: # publish the 1st rendering @@ -88,14 +98,14 @@ def exit(): params = await bus.get_event( event_interact ) if params: if params.get("cmd") == CMD_EXIT: - print(f"Process {pid} {hid} killed") + print(f">Process {pid} {hid} killed") break elif params.get("cmd") == CMD_RENDER: # just a false start, just need the current render - print(f"Process {pid} just a render of {hid}") + print(f">Process {pid} just a render of {hid}") await bus.publish(event_response,str(hr)) else: - print(f"Process {pid} interact {hid}:",params) + print(f">Process {pid} interact {hid}:",list(params.keys())) #-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\- UT if params["oid"]=="ut": params["oid"]=id(hr.tag) #-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\- @@ -105,14 +115,11 @@ def exit(): await asyncio.sleep(0.1) - # prevent the server that this process is going dead - await bus.publish( EVENT_SERVER, dict(cmd="REMOVE",hid=hid) ) - # unsubscribe for interaction await bus.unsubscribe( event_interact ) asyncio.run( loop() ) - print(f"Process {pid} ended") + print(f">Process {pid} ended") async def hrserver_orchestrator(): with redys.AClient() as bus: @@ -150,11 +157,6 @@ async def killall(ps:dict): from pprint import pprint pprint(ps) continue - elif params.get("cmd") == "REMOVE": - hid=params.get("hid") - print(EVENT_SERVER, params.get("cmd"),hid ) - del ps[hid] # remove from pool - continue hid=params["hid"] key_init=str(params["init"]) diff --git a/htagweb/server/client.py b/htagweb/server/client.py index 5e35e55..65bc6b7 100644 --- a/htagweb/server/client.py +++ b/htagweb/server/client.py @@ -14,11 +14,12 @@ TIMEOUT=30 # sec to wait answer from redys server class HrPilot: - def __init__(self,uid:str,fqn:str,js:str=None): - """ !!!!!!!!!!!!!!!!!!!! if js is None : can't do a start() !!!!!!!!!!!!!!!!!!!!!!""" + def __init__(self,uid:str,fqn:str,js:str=None,sesprovidername=None): + """ !!!!!!!!!!!!!!!!!!!! if js|sesprovidername is None : can't do a start() !!!!!!!!!!!!!!!!!!!!!!""" self.fqn=fqn self.js=js self.bus = redys.AClient() + self.sesprovidername=sesprovidername self.hid=f"{uid}_{fqn}" self.event_response = f"response_{self.hid}" @@ -53,6 +54,7 @@ async def start(self,*a,**k) -> str: fqn=self.fqn, js=self.js, init= (a,k), + sesprovidername=self.sesprovidername, )) # wait 1st rendering diff --git a/htagweb/sessions/__init__.py b/htagweb/sessions/__init__.py index f1235d2..9bff45f 100644 --- a/htagweb/sessions/__init__.py +++ b/htagweb/sessions/__init__.py @@ -19,8 +19,4 @@ async def createShm(uid): from . import shm return await shm.create(uid) -async def createMem(uid): - from . import memory - return await memory.create(uid) - -__all__= ["createFile","createFilePersistent","createShm","createMem"] \ No newline at end of file +__all__= ["createFile","createFilePersistent","createShm"] \ No newline at end of file diff --git a/htagweb/sessions/memory.py b/htagweb/sessions/memory.py deleted file mode 100644 index a923554..0000000 --- a/htagweb/sessions/memory.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -# ############################################################################# -# Copyright (C) 2023 manatlan manatlan[at]gmail(dot)com -# -# MIT licence -# -# https://github.com/manatlan/htagweb -# ############################################################################# -from ..usot import Usot - - -class ServerMemDict: # proxy between app and ServerUnique - def __init__(self,uid,dico:dict): - self._uid=uid - self._dico=dico - - def __len__(self): - return len(self._dico.keys()) - - def __contains__(self,key): - return key in self._dico.keys() - - def items(self): - return self._dico.items() - - def get(self,k:str,default=None): # could be inplemented in SessionMem - return self._dico.get(k,default) - - def __getitem__(self,k:str): # could be inplemented in SessionMem - return self._dico[k] - - def __setitem__(self,k:str,v): # could be inplemented in SessionMem - self._dico[k]=v - PX.clientsync.set( self._uid, self._dico) - - def __delitem__(self,k:str): - del self._dico[k] - PX.clientsync.set( self._uid, self._dico) - - def clear(self): - self._dico.clear() - PX.clientsync.set( self._uid, {}) - - def __repr__(self): - return f"" - - -class SessionMemory: # unique source of truth handled by ServerUnique - def __init__(self): - self.SESSIONS={} - def get(self,uid:str) -> ServerMemDict: - if uid not in self.SESSIONS: - self.SESSIONS[uid] = {} - return ServerMemDict( uid, self.SESSIONS[uid] ) - def set(self,uid:str,dico): - self.SESSIONS[uid] = dico - - - -PX=Usot( SessionMemory, port=19999 ) - -def startServer(): - PX.start() - -async def create(uid) -> ServerMemDict: - return PX.clientsync.get( uid ) - diff --git a/memserver.py b/memserver.py deleted file mode 100644 index 987a16b..0000000 --- a/memserver.py +++ /dev/null @@ -1,9 +0,0 @@ -import asyncio -from htagweb.sessions.memory import startServer - -async def server(): - startServer() - while 1: - await asyncio.sleep(1) - -asyncio.run( server() ) diff --git a/test_hr.py b/test_hr.py index 8d799cc..6f09f0b 100644 --- a/test_hr.py +++ b/test_hr.py @@ -19,6 +19,8 @@ async def test_hr(): ses=dict(cpt=1) hr=HRenderer( App ,"//",session = ses) + assert str(hr).startswith("") + assert ses["created"] assert ses["cpt"]==1 diff --git a/test_server.py b/test_server.py new file mode 100644 index 0000000..3eb2e5c --- /dev/null +++ b/test_server.py @@ -0,0 +1,43 @@ +from htag import Tag +import asyncio +import pytest,sys,io +import multiprocessing,threading +import time +from htagweb.appserver import processHrServer +from htagweb.server.client import HrPilot + + +@pytest.fixture() +def server(): + p=multiprocessing.Process(target=processHrServer) + p.start() + + time.sleep(1) + yield "x" + p.terminate() + + +@pytest.mark.asyncio +async def test_base( server ): + uid ="u1" + p=HrPilot(uid,"test_hr:App","//") + html=await p.start() + assert html.startswith("") + + actions=await p.interact( oid="ut", method_name="doit", args=[], kargs={}, event={} ) + assert "update" in actions + + # await p.kill() + # await p.kill() + # await p.kill() + + +if __name__=="__main__": + p=multiprocessing.Process(target=processHrServer) + try: + p.start() + time.sleep(1) + + asyncio.run( test_base(42) ) + finally: + p.terminate() \ No newline at end of file diff --git a/test_sessions.py b/test_sessions.py index 54ae5aa..279ada6 100644 --- a/test_sessions.py +++ b/test_sessions.py @@ -1,18 +1,38 @@ import pytest,asyncio,sys -from htagweb.sessions import createFile, createFilePersistent, createShm, createMem +from htagweb.sessions import createFile, createFilePersistent, createShm async def session_test(method_session): session = await method_session("uid") try: + # bad way to clone + with pytest.raises(Exception): + dict(session) + + # good way to clone + dict(session.items()) + + assert "nb" not in session + session["nb"]=session.get("nb",0) + 1 + assert "nb" in session + assert session + assert len(session)==1 + + # ensure persistance is present session = await method_session("uid") assert session["nb"]==1 - assert len(session.items())>0 + session["x"]=42 + + assert len(session)==2 + + del session["x"] + assert len(session)==1 session.clear() + assert len(session)==0 session = await method_session("uid") assert len(session.items())==0 @@ -30,16 +50,6 @@ async def test_sessions_file(): async def test_sessions_filepersitent(): await session_test( createFilePersistent ) -# def test_sessions_memory(): -# async def doit(): -# from htagweb.sessions.memory import startServer,PX -# startServer() - -# await session_test( createMem ) -# @pytest.mark.asyncio - -# asyncio.run( doit()) - @pytest.mark.asyncio async def test_sessions_shm(): diff --git a/test_usot.py b/test_usot.py index 6459a1b..ccc89ba 100644 --- a/test_usot.py +++ b/test_usot.py @@ -1,7 +1,7 @@ import pytest,asyncio import logging -from htagweb import Usot +from htagweb.usot import Usot class SessionMemory: def __init__(self): From a77ffc11b9883c8b52cf5dff98c1ddd45d011eea Mon Sep 17 00:00:00 2001 From: manatlan Date: Sat, 30 Sep 2023 17:55:07 +0200 Subject: [PATCH 06/26] tag.update commented, coz NOT STABLE !!! --- example.py | 16 ++- examples/main.py | 5 +- htagweb/appserver.py | 34 +++++- htagweb/server/__init__.py | 53 +++++---- htagweb/server/client.py | 10 +- htagweb/simpleserver.py | 2 +- htagweb/usot.py | 230 ------------------------------------- test_usot.py | 157 ------------------------- 8 files changed, 82 insertions(+), 425 deletions(-) delete mode 100644 htagweb/usot.py delete mode 100644 test_usot.py diff --git a/example.py b/example.py index 62a8a64..1379b89 100644 --- a/example.py +++ b/example.py @@ -1,7 +1,5 @@ from htag import Tag - -from htag import Tag -import json +import json,asyncio,time class TagSession(Tag.div): #dynamic component (compliant htag >= 0.30) !!!! FIRST IN THE WORLD !!!! def init(self): @@ -21,7 +19,9 @@ class App(Tag.body): imports=[TagSession] statics=b"window.error=alert" def init(self): - # del self.session["apps.draw.App"] + self.place = Tag.div(js="console.log('I update myself')") + asyncio.ensure_future( self.loop_timer() ) + def inc_test_session(o): v=int(self.state.get("integer","0")) v=v+1 @@ -42,7 +42,15 @@ def clllll(o): self+=Tag.li(Tag.a("t0",_href="/")) self+=Tag.li(Tag.a("t1",_href="/?a=43")) self+=Tag.li(Tag.a("t2",_href="/?z=bb")) + self+=self.place + async def loop_timer(self): + while 1: + await asyncio.sleep(0.5) + self.place.set(time.time() ) + if not await self.place.update(): # update component using current websocket + # break if can't (<- good practice to kill this asyncio/loop) + break # With Web http runner provided by htag diff --git a/examples/main.py b/examples/main.py index 54c350a..bcd3fd1 100644 --- a/examples/main.py +++ b/examples/main.py @@ -1,6 +1,6 @@ import os,sys; sys.path.insert(0,os.path.realpath(os.path.dirname(os.path.dirname(__file__)))) -from htagweb import SimpleServer +from htagweb import SimpleServer,AppServer from starlette.responses import HTMLResponse import app1 @@ -43,7 +43,8 @@ async def handlePath(request): return HTMLResponse("404",404) -app=SimpleServer() +#app=SimpleServer() +app=AppServer() app.add_route("/{path:path}", handlePath ) if __name__=="__main__": diff --git a/htagweb/appserver.py b/htagweb/appserver.py index 9260ea4..bef7b5c 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -31,7 +31,8 @@ from starlette.types import ASGIApp, Message, Receive, Scope, Send from htag.runners import commons -from . import crypto,usot +from . import crypto +import redys from htagweb.server import importClassFromFqn, hrserver, hrserver_orchestrator from htagweb.server.client import HrPilot @@ -116,6 +117,8 @@ def normalize(fqn): fqn="".join( reversed("".join(reversed(fqn)).replace(".",":",1))) return fqn + + class HRSocket(WebSocketEndpoint): encoding = "text" @@ -131,7 +134,26 @@ async def _sendback(self,websocket, txt:str) -> bool: logger.error("Can't send to socket, error: %s",e) return False + async def loop_tag_update(self, event, websocket): + with redys.AClient() as bus: + await bus.subscribe(event) + + ok=True + while ok: + actions = await bus.get_event( event ) + if actions: + ok=await self._sendback(websocket,json.dumps(actions)) + await asyncio.sleep(0.1) + async def on_connect(self, websocket): + #====================================================== get the event + fqn=websocket.path_params.get("fqn","") + uid=websocket.scope["uid"] + event=HrPilot(uid,fqn).event_response+"_update" + #====================================================== + + # asyncio.ensure_future(self.loop_tag_update(event,websocket)) + await websocket.accept() async def on_receive(self, websocket, data): @@ -150,7 +172,15 @@ async def on_receive(self, websocket, data): await self._sendback( websocket, json.dumps(actions) ) async def on_disconnect(self, websocket, close_code): - pass + #====================================================== get the event + fqn=websocket.path_params.get("fqn","") + uid=websocket.scope["uid"] + event=HrPilot(uid,fqn).event_response+"_update" + #====================================================== + + with redys.AClient() as bus: + await bus.unsubscribe(event) + def processHrServer(): asyncio.run( hrserver() ) diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index 5babbcd..83e91ec 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -10,13 +10,13 @@ import asyncio import redys import os,sys,importlib,inspect +import multiprocessing from htag import Tag - - -from multiprocessing import Process +import htagweb.sessions from htag.render import HRenderer + EVENT_SERVER="EVENT_SERVER" CMD_EXIT="EXIT" @@ -49,7 +49,7 @@ def importClassFromFqn(fqn_norm:str) -> type: klass.imports=[] return klass -import htagweb.sessions + def process(hid,event_response,event_interact,fqn,js,init,sesprovidername): if sesprovidername is None: @@ -67,51 +67,56 @@ async def loop(): def exit(): RUNNING=False - print(uid) session = await createSession(uid) - #session={} styles=Tag.style("body.htagoff * {cursor:not-allowed !important;}") hr=HRenderer( klass ,js, init=init, exit_callback=exit, fullerror=True, statics=[styles,],session = session) - # register the hr.sendactions, for tag.update feature - #TODO: implement tag.update !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - #TODO: implement tag.update !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - #TODO: implement tag.update !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - #hr.sendactions=lambda actions: self._sendback(websocket,json.dumps(actions)) - #TODO: implement tag.update !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - #TODO: implement tag.update !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - #TODO: implement tag.update !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - - - print(f">Process {pid} started with :",hid,init,event_response,event_interact) + print(f">Process {pid} started with :",hid,init) with redys.AClient() as bus: # publish the 1st rendering await bus.publish(event_response,str(hr)) + # register tag.update feature + #====================================== + # async def update(actions): + # try: + # await bus.publish(event_response+"_update",actions) + # except: + # print("!!! concurrent write/read on redys !!!") + # return True + + # hr.sendactions=update + #====================================== + + # subscribe for interaction await bus.subscribe( event_interact ) while RUNNING: params = await bus.get_event( event_interact ) - if params: + # try: #TODO: but fail the test_server.py ?!?!? + # except: # some times it crash ;-( + # print("!!! concurrent sockets reads !!!") + # params = None + if params and isinstance(params,dict): # sometimes it's not a dict ?!? (bool ?!) if params.get("cmd") == CMD_EXIT: print(f">Process {pid} {hid} killed") break elif params.get("cmd") == CMD_RENDER: # just a false start, just need the current render - print(f">Process {pid} just a render of {hid}") + print(f">Process {pid} render {hid}") await bus.publish(event_response,str(hr)) else: - print(f">Process {pid} interact {hid}:",list(params.keys())) + print(f">Process {pid} interact {hid}:") #-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\- UT if params["oid"]=="ut": params["oid"]=id(hr.tag) #-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\- actions = await hr.interact(**params) - await bus.publish(event_response,actions) + await bus.publish(event_response+"_interact",actions) await asyncio.sleep(0.1) @@ -176,7 +181,7 @@ async def killall(ps:dict): # and recreate another one later # create the process - p=Process(target=process, args=[],kwargs=params) + p=multiprocessing.Process(target=process, args=[],kwargs=params) p.start() # and save it in pool ps @@ -189,7 +194,7 @@ async def killall(ps:dict): await killall(ps) - print("htag starters stopped") + print("hrserver_orchestrator stopped") @@ -197,7 +202,7 @@ async def hrserver(): print("HRSERVER started") async def delay(): - await asyncio.sleep(0.5) + await asyncio.sleep(0.3) await hrserver_orchestrator() asyncio.ensure_future( delay() ) diff --git a/htagweb/server/client.py b/htagweb/server/client.py index 65bc6b7..f6b4e4d 100644 --- a/htagweb/server/client.py +++ b/htagweb/server/client.py @@ -25,11 +25,11 @@ def __init__(self,uid:str,fqn:str,js:str=None,sesprovidername=None): self.event_response = f"response_{self.hid}" self.event_interact = f"interact_{self.hid}" - async def _wait(self,s=TIMEOUT): + async def _wait(self,event, s=TIMEOUT): # wait for a response t1=time.monotonic() while time.monotonic() - t1 < s: - message = await self.bus.get_event( self.event_response ) + message = await self.bus.get_event( event ) if message: return message @@ -58,7 +58,7 @@ async def start(self,*a,**k) -> str: )) # wait 1st rendering - html = await self._wait() + html = await self._wait(self.event_response) return html @@ -74,13 +74,13 @@ async def interact(self,**params) -> dict: (dialog with process event) """ # subscribe for response - await self.bus.subscribe( self.event_response ) + await self.bus.subscribe( self.event_response+"_interact" ) # post the interaction await self.bus.publish( self.event_interact, params ) # wait actions - return await self._wait() + return await self._wait(self.event_response+"_interact") @staticmethod diff --git a/htagweb/simpleserver.py b/htagweb/simpleserver.py index 270d21c..f75a16c 100644 --- a/htagweb/simpleserver.py +++ b/htagweb/simpleserver.py @@ -53,7 +53,7 @@ from htag.render import HRenderer from htag.runners import commons -from . import crypto,usot +from . import crypto logger = logging.getLogger(__name__) #################################################### diff --git a/htagweb/usot.py b/htagweb/usot.py deleted file mode 100644 index 0eac280..0000000 --- a/htagweb/usot.py +++ /dev/null @@ -1,230 +0,0 @@ -# -*- coding: utf-8 -*- -# ############################################################################# -# Copyright (C) 2023 manatlan manatlan[at]gmail(dot)com -# -# MIT licence -# -# https://github.com/manatlan/htagweb -# ############################################################################# -import asyncio,sys -import logging,pickle -from typing import Callable -logger = logging.getLogger(__name__) - -class SessionMemory: - def __init__(self): - self.SESSIONS={} - def get(self,uid:str): - return self.SESSIONS.get(uid,{}) - def set(self,uid:str,value:dict): - assert isinstance(value,dict) - self.SESSIONS[uid]=value - - - -import multiprocessing,threading,time -from concurrent.futures import ThreadPoolExecutor - -class Usot: - """ Unique Source Of Truth - Helper to create server and client for a RPC - """ - def __init__(self,klass:Callable,host:str="127.0.0.1",port:int=17788): - self._klass=klass - self._host=host - self._port=int(port) - - self._task=None - self._p=None - - def start(self): - """ start an async task in the current loop, which spawn a tcp server - wich will expose methods of the 'klass' on 'host:port' - """ - def task(): - ########################################################################### - instance = self._klass() - - ########################################################################### - async def serve(reader, writer): - - question = await reader.read() - - #~ logger.debug("Received from %s, size: %s",writer.get_extra_info('peername'),len(question)) - - trunc = lambda x,limit=100: str(x)[:limit-3]+"..." if len(str(x))>limit else str(x) - fmt=lambda a,k: f"""{trunc(a)[1:-1]}{','.join([f"{k}={trunc(v)}" for k,v in k.items()])}""" - try: - name,a,k = pickle.loads(question) - method = getattr(instance, name) - logger.debug(">>> %s.%s( %s )", instance.__class__.__name__,name, fmt(a,k)) - if asyncio.iscoroutinefunction(method): - reponse = await method(*a,**k) - else: - reponse = method(*a,**k) - logger.debug("<<< %s", trunc(reponse)) - except Exception as e: - logger.error("Error calling %s(...) : %s" % (name,e)) - reponse=e - - data=pickle.dumps(reponse) - #~ logger.debug("Send size: %s",len(data)) - writer.write(data) - await writer.drain() - writer.write_eof() - - writer.close() - await writer.wait_closed() - - ########################################################################### - - return asyncio.start_server( serve, self._host, self._port) - - def callback(task): - try: - error=task.exception() - except asyncio.exceptions.CancelledError as e: - error=e - if not error: - logger.info("Usot: %s started on %s:%s !",self._klass.__name__,self._host,self._port) - elif isinstance(error,OSError): - logger.warning("Usot: %s exists on %s:%s !",self._klass.__name__,self._host,self._port) - elif isinstance(error, asyncio.exceptions.CancelledError): - logger.warning("Usot: %s cancelled !",self._klass.__name__) - else: - raise error - - self._task= asyncio.create_task( task() ) - self._task.add_done_callback(callback) - - def _instanciate(self): - self._running=True - async def loop(): - self.start() - while self._running: - await asyncio.sleep(0.1) - #run its own loop - asyncio.run(loop()) - - def start_process(self): - ''' start a process, with own loop to run the task ^^ ''' - self._p=multiprocessing.Process(target=self._instanciate) - self._p.start() - - def start_thread(self): - ''' start a thread, with own loop to run the task ^^ ''' - self._p=threading.Thread(target=self._instanciate) - self._p.start() - - def stop(self): - ''' will try to stop the server ''' - logger.info("try to stop server") - if self._task: self._task.cancel() - self._running=False # stop the loop in process/thread - if self._p: - if isinstance(self._p, multiprocessing.Process): - self._p.terminate() # process mode - self._p.join() - - - @property - def clientsync(self): - class ProxySync: - def __getattr__(this,name:str): - def _(*a,**k): - am=self.clientasync.__getattr__(name) - coro = am(*a,**k) - - sideloop=asyncio.new_event_loop() - with ThreadPoolExecutor(max_workers=1) as exe: - r=exe.submit(sideloop.run_until_complete, coro ) - retour= r.result() - sideloop.close() - return retour - return _ - return ProxySync() - - @property - def clientasync(self): - class ProxyASync: - def __getattr__(this,name:str): - async def _(*a,**k): - try: # ensure server was started - await self._task - except: - pass - - reader, writer = await asyncio.open_connection(self._host,self._port) - question = pickle.dumps( (name,a,k) ) - # logger.debug('Sending data of size: %s',len(question)) - writer.write(question) - await writer.drain() - writer.write_eof() - data = await reader.read() - # logger.debug('recept data of size: %s',len(data)) - reponse = pickle.loads( data ) - writer.close() - await writer.wait_closed() - if isinstance(reponse,Exception): - raise reponse - else: - return reponse - return _ - - return ProxyASync() - - -def test_sync(client): - client.set("uid1", dict(a=43)) - assert client.get("uid1") == dict(a=43) - -async def test_async(client): - await client.set("uid1", dict(a=42)) - assert await client.get("uid1") == dict(a=42) - - - -if __name__=="__main__": - import pytest - import logging,multiprocessing,threading - logging.basicConfig(format='[%(levelname)-5s] %(name)s: %(message)s',level=logging.DEBUG) - - - async def test_thread(): - print(">>> THREAD") - p=Usot(SessionMemory,port=19999) - p.start_thread() - await asyncio.sleep(0.2) # slowdown to let server start ;-( - try: - await test_async( p.clientasync ) - test_sync( p.clientsync ) - finally: - p.stop() - - async def test_process(): - print(">>> PROCESS") - p=Usot(SessionMemory,port=19999) - p.start_process() - await asyncio.sleep(0.2) # slowdown to let server start ;-( - try: - await test_async( p.clientasync ) - test_sync( p.clientsync ) - finally: - p.stop() - - async def test_task(): - print(">>> TASK") - p=Usot(SessionMemory,port=19999) - p.start() - try: - await test_async( p.clientasync ) - #~ test_sync( p.clientsync ) # sync not possible - finally: - p.stop() - - # asyncio.run( test_process() ) - # asyncio.run( test_thread() ) - asyncio.run( test_task() ) - # asyncio.run( test_task() ) - # asyncio.run( test_thread() ) - # asyncio.run( test_process() ) diff --git a/test_usot.py b/test_usot.py deleted file mode 100644 index ccc89ba..0000000 --- a/test_usot.py +++ /dev/null @@ -1,157 +0,0 @@ -import pytest,asyncio -import logging - -from htagweb.usot import Usot - -class SessionMemory: - def __init__(self): - self.SESSIONS={} - def get(self,uid:str): - return self.SESSIONS.get(uid,{}) - def set(self,uid:str,value:dict): - assert isinstance(value,dict) - self.SESSIONS[uid]=value - -# class BuggedObject: -# def testko(self): -# return 42/0 # runtime error ;-) -# def testok(self): -# return 42 - -# class ObjectTest: -# async def test(self): -# await asyncio.sleep(0.1) -# return 42 -# def testsize(self,buf): -# return buf - -# def f(klass,port): -# async def loop(): -# px = ProxySingleton( klass, port=port ) -# d=await px.get("uid") -# d["nb"]+=1 -# await px.set("uid",d) - -# asyncio.run(loop()) - - -# @pytest.mark.asyncio -# async def test_base(): -# m=ProxySingleton( SessionMemory, port=19999 ) -# assert await m.get("uid1") == {} -# await m.set("uid1", dict(a=42)) -# assert await m.get("uid1") == dict(a=42) - -# m2=ProxySingleton( SessionMemory, port=19999 ) -# assert await m2.get("uid1") == dict(a=42) - -# m2=ProxySingleton( SessionMemory, port=19999 ) -# assert await m2.get("uid1") == dict(a=42) - -# assert await m.get("uid1") == dict(a=42) - - -# @pytest.mark.asyncio -# async def test_compatibility_in_multiprocessing(): -# # ensure is compatible in different process -# import multiprocessing - -# m=ProxySingleton( SessionMemory, port=19999 ) -# await m.set("uid",dict(nb=0)) - -# p=multiprocessing.Process(target=f,args=(SessionMemory,19999,)) -# p.start() -# while p.is_alive(): -# await asyncio.sleep(0.2) - -# xx=await m.get("uid") -# assert {"nb":1} == xx - -# @pytest.mark.asyncio -# async def test_classical_use(): -# # ensure the classic use works -# async with ServeUnique( SessionMemory, port=19999 ) as m: -# assert m.is_server() -# print(m) -# assert await m.get("uid1") == {} -# await m.set("uid1", dict(a=42)) -# assert await m.get("uid1") == dict(a=42) - -# assert not m.is_server() # and it's closed - -# async with ServeUnique( SessionMemory, port=21213 ) as m: -# assert {} == await m.get("uid") # previous was closed, so new one - -# @pytest.mark.asyncio -# async def test_exception_are_well_managed(): -# # ensure exception are well managed -# async with ServeUnique( BuggedObject, port=19999 ) as m: -# with pytest.raises(ZeroDivisionError): -# await m.testko() - -# # it works after crash -# assert 42==await m.testok() - -# @pytest.mark.asyncio -# async def test_async_methods_on_object(): -# # ensure object can have async methods -# async with ServeUnique( ObjectTest, port=19999 ) as m: -# assert 42==await m.test() -# buf=500_000*"x" -# assert buf==await m.testsize(buf) # work better than redys - -# @pytest.mark.asyncio -# async def test_compatibility_in_inner_thread(): -# # ensure is compatible in same thread -# async with ServeUnique( SessionMemory, port=21213 ) as m: -# assert m.is_server() -# await m.set("uid",dict(nb=0)) - -# async with ServeUnique( SessionMemory, port=21213 ) as m2: -# assert not m2.is_server() # m is not the real server -# d=await m2.get("uid") -# d["nb"]+=1 -# await m2.set("uid",d) - -# assert {"nb":1} == await m.get("uid") - -def sync_test(client): - client.set("uid1", dict(a=43)) - assert client.get("uid1") == dict(a=43) - -async def async_test(client): - await client.set("uid1", dict(a=42)) - assert await client.get("uid1") == dict(a=42) - - - -@pytest.mark.asyncio -async def test_task(): - print(">>> TASK") - p=Usot(SessionMemory,port=19999) - p.start() - try: - await async_test( p.clientasync ) - ##sync_test( p.clientsync ) # sync not possible - finally: - p.stop() - -# def test_thread(): -# print(">>> THREAD") -# p=Usot(SessionMemory,port=19919) -# p.start_thread() -# try: -# #await async_test( p.clientasync ) -# sync_test( p.clientsync ) -# finally: -# p.stop() - -if __name__=="__main__": - logging.basicConfig(format='[%(levelname)-5s] %(name)s: %(message)s',level=logging.DEBUG) - - # asyncio.run( test_base() ) - # asyncio.run( test_classical_use() ) - # asyncio.run( test_exception_are_well_managed() ) - # asyncio.run( test_async_methods_on_object() ) - # asyncio.run( test_compatibility_in_inner_thread() ) - # asyncio.run( test_compatibility_in_multiprocessing() ) From aad27ccadef4597a25eff81e4503fdcf9edbe5ad Mon Sep 17 00:00:00 2001 From: manatlan Date: Sun, 1 Oct 2023 10:38:47 +0200 Subject: [PATCH 07/26] use "redys.v2" : but not stable ;-( --- htagweb/appserver.py | 12 ++++---- htagweb/server/__init__.py | 23 +++++++++++--- htagweb/server/client.py | 11 ++++--- poetry.lock | 63 +++----------------------------------- pyproject.toml | 25 ++++++++------- 5 files changed, 47 insertions(+), 87 deletions(-) diff --git a/htagweb/appserver.py b/htagweb/appserver.py index bef7b5c..cf8526b 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -32,9 +32,9 @@ from htag.runners import commons from . import crypto -import redys +import redys.v2 -from htagweb.server import importClassFromFqn, hrserver, hrserver_orchestrator +from htagweb.server import hrserver2, importClassFromFqn, hrserver, hrserver_orchestrator from htagweb.server.client import HrPilot logger = logging.getLogger(__name__) @@ -135,7 +135,7 @@ async def _sendback(self,websocket, txt:str) -> bool: return False async def loop_tag_update(self, event, websocket): - with redys.AClient() as bus: + with redys.v2.AClient() as bus: await bus.subscribe(event) ok=True @@ -178,12 +178,12 @@ async def on_disconnect(self, websocket, close_code): event=HrPilot(uid,fqn).event_response+"_update" #====================================================== - with redys.AClient() as bus: - await bus.unsubscribe(event) + # with redys.v2.AClient() as bus: + # await bus.unsubscribe(event) def processHrServer(): - asyncio.run( hrserver() ) + asyncio.run( hrserver2() ) class AppServer(Starlette): #NOT THE DEFINITIVE NAME !!!!!!!!!!!!!!!! diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index 83e91ec..4a3a942 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -9,10 +9,10 @@ import asyncio import redys +import redys.v2 import os,sys,importlib,inspect import multiprocessing from htag import Tag -import htagweb.sessions from htag.render import HRenderer @@ -54,6 +54,7 @@ def importClassFromFqn(fqn_norm:str) -> type: def process(hid,event_response,event_interact,fqn,js,init,sesprovidername): if sesprovidername is None: sesprovidername="createFile" + import htagweb.sessions createSession=getattr(htagweb.sessions,sesprovidername) uid=hid.split("_")[0] @@ -75,7 +76,7 @@ def exit(): print(f">Process {pid} started with :",hid,init) - with redys.AClient() as bus: + with redys.v2.AClient() as bus: # publish the 1st rendering await bus.publish(event_response,str(hr)) @@ -127,7 +128,7 @@ def exit(): print(f">Process {pid} ended") async def hrserver_orchestrator(): - with redys.AClient() as bus: + with redys.v2.AClient() as bus: # prevent multi orchestrators if await bus.get("hrserver_orchestrator_running")==True: @@ -209,5 +210,19 @@ async def delay(): await redys.Server() +async def hrserver2(): + print("HRSERVER2 started") + + async def delay(): + await asyncio.sleep(0.3) + await hrserver_orchestrator() + + asyncio.ensure_future( delay() ) + + s=redys.v2.Server() + s.start() + while 1: + await asyncio.sleep(1) + if __name__=="__main__": - asyncio.run( hrserver() ) + asyncio.run( hrserver2() ) diff --git a/htagweb/server/client.py b/htagweb/server/client.py index f6b4e4d..bee631b 100644 --- a/htagweb/server/client.py +++ b/htagweb/server/client.py @@ -7,8 +7,9 @@ # https://github.com/manatlan/htagweb # ############################################################################# -import uuid,asyncio -import redys,time +import uuid,asyncio,time +import redys +import redys.v2 from htagweb.server import EVENT_SERVER TIMEOUT=30 # sec to wait answer from redys server @@ -18,7 +19,7 @@ def __init__(self,uid:str,fqn:str,js:str=None,sesprovidername=None): """ !!!!!!!!!!!!!!!!!!!! if js|sesprovidername is None : can't do a start() !!!!!!!!!!!!!!!!!!!!!!""" self.fqn=fqn self.js=js - self.bus = redys.AClient() + self.bus = redys.v2.AClient() self.sesprovidername=sesprovidername self.hid=f"{uid}_{fqn}" @@ -88,7 +89,7 @@ async def list(): """ SERVER COMMAND (dialog with server event) """ - with redys.AClient() as bus: + with redys.v2.AClient() as bus: await bus.publish( EVENT_SERVER, dict(cmd="PS") ) #~ @staticmethod #~ async def stop(): @@ -99,7 +100,7 @@ async def clean(): """ SERVER COMMAND (dialog with server event) """ - with redys.AClient() as bus: + with redys.v2.AClient() as bus: await bus.publish( EVENT_SERVER, dict(cmd="CLEAN") ) diff --git a/poetry.lock b/poetry.lock index 21703af..965b952 100644 --- a/poetry.lock +++ b/poetry.lock @@ -22,17 +22,6 @@ doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd- test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (<0.22)"] -[[package]] -name = "certifi" -version = "2023.7.22" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, -] - [[package]] name = "click" version = "8.1.7" @@ -173,27 +162,6 @@ files = [ {file = "htag-0.42.0.tar.gz", hash = "sha256:3c6c226af1e75fd54f3c6bb1e4a6d8ccc449a8da201a2fedc932711bcaa7ac60"}, ] -[[package]] -name = "httpcore" -version = "0.17.3" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.7" -files = [ - {file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"}, - {file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"}, -] - -[package.dependencies] -anyio = ">=3.0,<5.0" -certifi = "*" -h11 = ">=0.13,<0.15" -sniffio = "==1.*" - -[package.extras] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] - [[package]] name = "httptools" version = "0.6.0" @@ -241,29 +209,6 @@ files = [ [package.extras] test = ["Cython (>=0.29.24,<0.30.0)"] -[[package]] -name = "httpx" -version = "0.24.1" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.7" -files = [ - {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, - {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, -] - -[package.dependencies] -certifi = "*" -httpcore = ">=0.15.0,<0.18.0" -idna = "*" -sniffio = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] - [[package]] name = "idna" version = "3.4" @@ -511,13 +456,13 @@ files = [ [[package]] name = "redys" -version = "0.9.8" +version = "0.9.9" description = "A simple redis-like in pure python3, fully asyncio compliant" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "redys-0.9.8-py3-none-any.whl", hash = "sha256:fb27f7cc982363835089f115a66c7da56c8ff67897e556a2276e69ce043fe517"}, - {file = "redys-0.9.8.tar.gz", hash = "sha256:b87fa13b16e9d1bdbef5b43868b7652dda408b1e4c0eee9dca5e358de43d2ca8"}, + {file = "redys-0.9.9-py3-none-any.whl", hash = "sha256:4de11b75e30c588361c6abe923afcd5a32670ee18e3883b8c09f06a9c3455031"}, + {file = "redys-0.9.9.tar.gz", hash = "sha256:cc30a61e5c9cb98e519876c31d389b3df86fa88dc1b629dcc22e32d95b232486"}, ] [[package]] @@ -772,4 +717,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "df8b95784ef698262c8ebef15bdc4a1259a090f433262075840ad4c7464740c1" +content-hash = "2f1f79ef448c45e47a5fb22a4982917bacd4c9c0d58ecc40dd334b09332c10cc" diff --git a/pyproject.toml b/pyproject.toml index f751907..881bb24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,20 +17,19 @@ classifiers = [ ] [tool.poetry.dependencies] -# https://python-poetry.org/docs/dependency-specification/ python = "^3.7" -htag = ">= 0.41.0" -starlette = ">= 0.21.0" -pycryptodomex = ">= 3.0.0" -uvicorn = {version = ">=0.22,<1.0", extras = ["standard"]} -redys = "^0.9.8" +htag = "^0.42.0" +starlette = "0.29.0" +pycryptodomex = "^3.19.0" +uvicorn = {version = "0.22.0", extras = ["standard"]} +redys = "0.9.9" -[tool.poetry.dev-dependencies] -pytest = "^7.0" -pytest-cov = "^4.1" -pytest-asyncio = "^0.21" -httpx = "^0.24" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.2" +pytest-asyncio = "^0.21.1" +pytest-cov = "^4.1.0" [build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" From 531c455a72084dcf1bec5b1b3552a894e1d3235e Mon Sep 17 00:00:00 2001 From: manatlan Date: Sun, 1 Oct 2023 10:53:48 +0200 Subject: [PATCH 08/26] code: assert every publish --- htagweb/server/__init__.py | 12 ++++++------ htagweb/server/client.py | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index 4a3a942..1f7afd5 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -78,7 +78,7 @@ def exit(): with redys.v2.AClient() as bus: # publish the 1st rendering - await bus.publish(event_response,str(hr)) + assert await bus.publish(event_response,str(hr)) # register tag.update feature #====================================== @@ -109,7 +109,7 @@ def exit(): elif params.get("cmd") == CMD_RENDER: # just a false start, just need the current render print(f">Process {pid} render {hid}") - await bus.publish(event_response,str(hr)) + assert await bus.publish(event_response,str(hr)) else: print(f">Process {pid} interact {hid}:") #-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\- UT @@ -117,7 +117,7 @@ def exit(): #-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\- actions = await hr.interact(**params) - await bus.publish(event_response+"_interact",actions) + assert await bus.publish(event_response+"_interact",actions) await asyncio.sleep(0.1) @@ -146,7 +146,7 @@ async def hrserver_orchestrator(): async def killall(ps:dict): # try to send a EXIT CMD to all running ps for hid,infos in ps.items(): - await bus.publish(infos["event_interact"],dict(cmd=CMD_EXIT)) + assert await bus.publish(infos["event_interact"],dict(cmd=CMD_EXIT)) while 1: params = await bus.get_event( EVENT_SERVER ) @@ -174,11 +174,11 @@ async def killall(ps:dict): # it's the same initialization process # so ask process to send back its render - await bus.publish(params["event_interact"],dict(cmd=CMD_RENDER)) + assert await bus.publish(params["event_interact"],dict(cmd=CMD_RENDER)) continue else: # kill itself because it's not the same init params - await bus.publish(params["event_interact"],dict(cmd=CMD_EXIT)) + assert await bus.publish(params["event_interact"],dict(cmd=CMD_EXIT)) # and recreate another one later # create the process diff --git a/htagweb/server/client.py b/htagweb/server/client.py index bee631b..b077c9e 100644 --- a/htagweb/server/client.py +++ b/htagweb/server/client.py @@ -48,7 +48,7 @@ async def start(self,*a,**k) -> str: await self.bus.subscribe( self.event_response ) # start the process app - await self.bus.publish( EVENT_SERVER , dict( + assert await self.bus.publish( EVENT_SERVER , dict( hid=self.hid, event_response=self.event_response, event_interact=self.event_interact, @@ -67,7 +67,7 @@ async def kill(self): """ Kill the process (dialog with process event) """ - await self.bus.publish( self.event_interact, dict(cmd="EXIT") ) + assert await self.bus.publish( self.event_interact, dict(cmd="EXIT") ) async def interact(self,**params) -> dict: @@ -78,7 +78,7 @@ async def interact(self,**params) -> dict: await self.bus.subscribe( self.event_response+"_interact" ) # post the interaction - await self.bus.publish( self.event_interact, params ) + assert await self.bus.publish( self.event_interact, params ) # wait actions return await self._wait(self.event_response+"_interact") @@ -90,7 +90,7 @@ async def list(): (dialog with server event) """ with redys.v2.AClient() as bus: - await bus.publish( EVENT_SERVER, dict(cmd="PS") ) + assert await bus.publish( EVENT_SERVER, dict(cmd="PS") ) #~ @staticmethod #~ async def stop(): #~ with redys.AClient() as bus: @@ -101,7 +101,7 @@ async def clean(): (dialog with server event) """ with redys.v2.AClient() as bus: - await bus.publish( EVENT_SERVER, dict(cmd="CLEAN") ) + assert await bus.publish( EVENT_SERVER, dict(cmd="CLEAN") ) async def main(): From d33dc1ca515bbe2cb84936b4e1e7103567f163fe Mon Sep 17 00:00:00 2001 From: manatlan Date: Sun, 1 Oct 2023 11:14:45 +0200 Subject: [PATCH 09/26] better, but not stable yet --- htagweb/server/__init__.py | 7 +++++-- htagweb/server/client.py | 10 ++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index 1f7afd5..6661b22 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -117,6 +117,7 @@ def exit(): #-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\- actions = await hr.interact(**params) + assert await bus.publish(event_response+"_interact",actions) await asyncio.sleep(0.1) @@ -174,11 +175,13 @@ async def killall(ps:dict): # it's the same initialization process # so ask process to send back its render - assert await bus.publish(params["event_interact"],dict(cmd=CMD_RENDER)) + # (TODO:sometimes it's not possible to assert it) + await bus.publish(params["event_interact"],dict(cmd=CMD_RENDER)) continue else: # kill itself because it's not the same init params - assert await bus.publish(params["event_interact"],dict(cmd=CMD_EXIT)) + # (TODO:sometimes it's not possible to assert it) + await bus.publish(params["event_interact"],dict(cmd=CMD_EXIT)) # and recreate another one later # create the process diff --git a/htagweb/server/client.py b/htagweb/server/client.py index b077c9e..1aa080a 100644 --- a/htagweb/server/client.py +++ b/htagweb/server/client.py @@ -12,7 +12,7 @@ import redys.v2 from htagweb.server import EVENT_SERVER -TIMEOUT=30 # sec to wait answer from redys server +TIMEOUT=5 # sec to wait answer from redys server #TODO: set better class HrPilot: def __init__(self,uid:str,fqn:str,js:str=None,sesprovidername=None): @@ -31,11 +31,9 @@ async def _wait(self,event, s=TIMEOUT): t1=time.monotonic() while time.monotonic() - t1 < s: message = await self.bus.get_event( event ) - if message: + if message is not None: return message - await asyncio.sleep(0.1) - return None async def start(self,*a,**k) -> str: @@ -59,7 +57,7 @@ async def start(self,*a,**k) -> str: )) # wait 1st rendering - html = await self._wait(self.event_response) + html = await self._wait(self.event_response) or "?!" return html @@ -81,7 +79,7 @@ async def interact(self,**params) -> dict: assert await self.bus.publish( self.event_interact, params ) # wait actions - return await self._wait(self.event_response+"_interact") + return await self._wait(self.event_response+"_interact") or {} @staticmethod From 6bd6bafae484985cfba03ed93c0af0f098bcaf6e Mon Sep 17 00:00:00 2001 From: manatlan Date: Sun, 1 Oct 2023 11:48:15 +0200 Subject: [PATCH 10/26] more coherence, but not stable yet --- htagweb/appserver.py | 2 +- htagweb/server/__init__.py | 11 ++++------- htagweb/server/client.py | 14 ++++++-------- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/htagweb/appserver.py b/htagweb/appserver.py index cf8526b..78a1aed 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -277,7 +277,7 @@ async def serve(self, request, obj ) -> HTMLResponse: args,kargs = commons.url2ak(str(request.url)) html=await p.start(*args,**kargs) - return HTMLResponse(html or "no?!") + return HTMLResponse(html) diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index 6661b22..a4c59a4 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -76,7 +76,11 @@ def exit(): print(f">Process {pid} started with :",hid,init) + with redys.v2.AClient() as bus: + # subscribe for interaction + await bus.subscribe( event_interact ) + # publish the 1st rendering assert await bus.publish(event_response,str(hr)) @@ -92,10 +96,6 @@ def exit(): # hr.sendactions=update #====================================== - - # subscribe for interaction - await bus.subscribe( event_interact ) - while RUNNING: params = await bus.get_event( event_interact ) # try: #TODO: but fail the test_server.py ?!?!? @@ -122,9 +122,6 @@ def exit(): await asyncio.sleep(0.1) - # unsubscribe for interaction - await bus.unsubscribe( event_interact ) - asyncio.run( loop() ) print(f">Process {pid} ended") diff --git a/htagweb/server/client.py b/htagweb/server/client.py index 1aa080a..ae3e09a 100644 --- a/htagweb/server/client.py +++ b/htagweb/server/client.py @@ -57,15 +57,13 @@ async def start(self,*a,**k) -> str: )) # wait 1st rendering - html = await self._wait(self.event_response) or "?!" + return await self._wait(self.event_response) or "?!" - return html - - async def kill(self): - """ Kill the process - (dialog with process event) - """ - assert await self.bus.publish( self.event_interact, dict(cmd="EXIT") ) + # async def kill(self): + # """ Kill the process + # (dialog with process event) + # """ + # assert await self.bus.publish( self.event_interact, dict(cmd="EXIT") ) async def interact(self,**params) -> dict: From 969faff4ad93eaafce9f64e9d7b26c9bb7942d9c Mon Sep 17 00:00:00 2001 From: manatlan Date: Sun, 1 Oct 2023 12:33:49 +0200 Subject: [PATCH 11/26] stability is better ! --- example.py | 7 ++++--- htagweb/appserver.py | 6 +++--- htagweb/server/__init__.py | 30 ++++++++++++++---------------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/example.py b/example.py index 1379b89..e414a3c 100644 --- a/example.py +++ b/example.py @@ -18,7 +18,7 @@ def render(self): class App(Tag.body): imports=[TagSession] statics=b"window.error=alert" - def init(self): + def init(self,v="0"): self.place = Tag.div(js="console.log('I update myself')") asyncio.ensure_future( self.loop_timer() ) @@ -34,14 +34,15 @@ def addd(o): def clllll(o): self.state.clear() + self <= Tag.div(v) self <= Tag.button("inc integer",_onclick=inc_test_session) self <= Tag.button("add list",_onclick=addd) self <= Tag.button("clear",_onclick=clllll) self <= TagSession() self+=Tag.li(Tag.a("t0",_href="/")) - self+=Tag.li(Tag.a("t1",_href="/?a=43")) - self+=Tag.li(Tag.a("t2",_href="/?z=bb")) + self+=Tag.li(Tag.a("t1",_href="/?v=1")) + self+=Tag.li(Tag.a("t2",_href="/?v=2")) self+=self.place async def loop_timer(self): diff --git a/htagweb/appserver.py b/htagweb/appserver.py index 78a1aed..5c494f7 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -152,7 +152,7 @@ async def on_connect(self, websocket): event=HrPilot(uid,fqn).event_response+"_update" #====================================================== - # asyncio.ensure_future(self.loop_tag_update(event,websocket)) + asyncio.ensure_future(self.loop_tag_update(event,websocket)) await websocket.accept() @@ -178,8 +178,8 @@ async def on_disconnect(self, websocket, close_code): event=HrPilot(uid,fqn).event_response+"_update" #====================================================== - # with redys.v2.AClient() as bus: - # await bus.unsubscribe(event) + with redys.v2.AClient() as bus: + await bus.unsubscribe(event) def processHrServer(): diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index a4c59a4..20b7370 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -86,14 +86,14 @@ def exit(): # register tag.update feature #====================================== - # async def update(actions): - # try: - # await bus.publish(event_response+"_update",actions) - # except: - # print("!!! concurrent write/read on redys !!!") - # return True - - # hr.sendactions=update + async def update(actions): + try: + await bus.publish(event_response+"_update",actions) + except: + print("!!! concurrent write/read on redys !!!") + return True + + hr.sendactions=update #====================================== while RUNNING: @@ -103,10 +103,7 @@ def exit(): # print("!!! concurrent sockets reads !!!") # params = None if params and isinstance(params,dict): # sometimes it's not a dict ?!? (bool ?!) - if params.get("cmd") == CMD_EXIT: - print(f">Process {pid} {hid} killed") - break - elif params.get("cmd") == CMD_RENDER: + if params.get("cmd") == CMD_RENDER: # just a false start, just need the current render print(f">Process {pid} render {hid}") assert await bus.publish(event_response,str(hr)) @@ -122,6 +119,9 @@ def exit(): await asyncio.sleep(0.1) + #consume all pending events + assert await bus.unsubscribe( event_interact ) + asyncio.run( loop() ) print(f">Process {pid} ended") @@ -172,13 +172,11 @@ async def killall(ps:dict): # it's the same initialization process # so ask process to send back its render - # (TODO:sometimes it's not possible to assert it) - await bus.publish(params["event_interact"],dict(cmd=CMD_RENDER)) + assert await bus.publish(params["event_interact"],dict(cmd=CMD_RENDER)) continue else: # kill itself because it's not the same init params - # (TODO:sometimes it's not possible to assert it) - await bus.publish(params["event_interact"],dict(cmd=CMD_EXIT)) + ps[hid]["process"].terminate() # and recreate another one later # create the process From a3abb546569a756def920947a0656b74a9cb5a84 Mon Sep 17 00:00:00 2001 From: manatlan Date: Sun, 1 Oct 2023 12:47:16 +0200 Subject: [PATCH 12/26] more stable ! --- example.py | 17 +++++++++++++++++ htagweb/server/__init__.py | 10 +++++----- htagweb/server/client.py | 5 +---- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/example.py b/example.py index e414a3c..68fd93a 100644 --- a/example.py +++ b/example.py @@ -1,6 +1,16 @@ from htag import Tag import json,asyncio,time +""" +Complex htag's app to test: + + - a dynamic object (TagSession), which got a render method (new way) + - using tag.state (in session) + - using tag.update with a task in a loop + - can recreate itself (when init params change) + +""" + class TagSession(Tag.div): #dynamic component (compliant htag >= 0.30) !!!! FIRST IN THE WORLD !!!! def init(self): self["style"]="border:1px solid black" @@ -34,10 +44,12 @@ def addd(o): def clllll(o): self.state.clear() + self <= Tag.div(v) self <= Tag.button("inc integer",_onclick=inc_test_session) self <= Tag.button("add list",_onclick=addd) self <= Tag.button("clear",_onclick=clllll) + #~ self <= Tag.button("yield",_onclick=self.yielder) self <= TagSession() self+=Tag.li(Tag.a("t0",_href="/")) @@ -45,6 +57,11 @@ def clllll(o): self+=Tag.li(Tag.a("t2",_href="/?v=2")) self+=self.place + #~ async def yielder(self,o): + #~ for i in "ABCDEF": + #~ await asyncio.sleep(0.3) + #~ self+=i + async def loop_timer(self): while 1: await asyncio.sleep(0.5) diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index 20b7370..d31c09a 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -141,10 +141,10 @@ async def hrserver_orchestrator(): ps={} - async def killall(ps:dict): + def killall(ps:dict): # try to send a EXIT CMD to all running ps for hid,infos in ps.items(): - assert await bus.publish(infos["event_interact"],dict(cmd=CMD_EXIT)) + ps[hid]["process"].terminate() while 1: params = await bus.get_event( EVENT_SERVER ) @@ -154,7 +154,7 @@ async def killall(ps:dict): break elif params.get("cmd") == "CLEAN": print(EVENT_SERVER, params.get("cmd") ) - await killall(ps) + killall(ps) continue elif params.get("cmd") == "PS": print(EVENT_SERVER, params.get("cmd") ) @@ -188,9 +188,9 @@ async def killall(ps:dict): await asyncio.sleep(0.1) - await bus.unsubscribe( EVENT_SERVER ) + assert await bus.unsubscribe( EVENT_SERVER ) - await killall(ps) + killall(ps) print("hrserver_orchestrator stopped") diff --git a/htagweb/server/client.py b/htagweb/server/client.py index ae3e09a..6859234 100644 --- a/htagweb/server/client.py +++ b/htagweb/server/client.py @@ -87,10 +87,7 @@ async def list(): """ with redys.v2.AClient() as bus: assert await bus.publish( EVENT_SERVER, dict(cmd="PS") ) - #~ @staticmethod - #~ async def stop(): - #~ with redys.AClient() as bus: - #~ await bus.publish( EVENT_SERVER, dict(cmd="EXIT") ) + @staticmethod async def clean(): """ SERVER COMMAND From 6b5462afd045c9b635816507e8992e370a3c762f Mon Sep 17 00:00:00 2001 From: manatlan Date: Sun, 1 Oct 2023 14:05:34 +0200 Subject: [PATCH 13/26] Update appserver.py --- htagweb/appserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htagweb/appserver.py b/htagweb/appserver.py index 5c494f7..a413f89 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -141,7 +141,7 @@ async def loop_tag_update(self, event, websocket): ok=True while ok: actions = await bus.get_event( event ) - if actions: + if actions is not None: ok=await self._sendback(websocket,json.dumps(actions)) await asyncio.sleep(0.1) From 3d2cb7fcf5892f3911971e1b853ecfbe73e586df Mon Sep 17 00:00:00 2001 From: manatlan Date: Sun, 1 Oct 2023 15:45:36 +0200 Subject: [PATCH 14/26] Update __init__.py --- htagweb/server/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index d31c09a..3ae718d 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -148,7 +148,7 @@ def killall(ps:dict): while 1: params = await bus.get_event( EVENT_SERVER ) - if params: + if params is not None: if params.get("cmd") == CMD_EXIT: print(EVENT_SERVER, params.get("cmd") ) break From 9b5a7086b04cd5ac16dacd99f401ceaece9cf1bc Mon Sep 17 00:00:00 2001 From: manatlan Date: Sun, 1 Oct 2023 18:11:57 +0200 Subject: [PATCH 15/26] better --- example.py | 2 +- htagweb/appserver.py | 34 +++++++++++++++++----------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/example.py b/example.py index 68fd93a..1117c55 100644 --- a/example.py +++ b/example.py @@ -79,7 +79,7 @@ async def loop_timer(self): # With htagweb.WebServer runner provided by htagweb #------------------------------------------------------ from htagweb import SimpleServer,AppServer -app=AppServer( App ,parano=False) +app=AppServer( "App:ppp" ,parano=True) if __name__=="__main__": app.run(openBrowser=False) diff --git a/htagweb/appserver.py b/htagweb/appserver.py index a413f89..69760c3 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -62,6 +62,7 @@ def findfqn(x) -> str: return tagClass.__module__+"."+tagClass.__qualname__ +parano_seed = lambda uid: hashlib.md5(uid.encode()).hexdigest() class WebServerSession: # ASGI Middleware, for starlette def __init__(self, app:ASGIApp, https_only:bool = False, sesprovider:"async method(uid)"=None ) -> None: @@ -88,7 +89,6 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: #!!!!!!!!!!!!!!!!!!!!!!!!!!! scope["uid"] = uid - scope["parano"] = hashlib.md5(uid.encode()).hexdigest() scope["session"] = await self.cbsesprovider(uid) #!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -123,10 +123,10 @@ class HRSocket(WebSocketEndpoint): encoding = "text" async def _sendback(self,websocket, txt:str) -> bool: - parano_seed = websocket.scope["parano"] if websocket.app.parano else None try: - if parano_seed: - txt = crypto.encrypt(txt.encode(),parano_seed) + if websocket.app.parano: + seed = parano_seed( websocket.scope["uid"]) + txt = crypto.encrypt(txt.encode(),seed) await websocket.send_text( txt ) return True @@ -159,10 +159,9 @@ async def on_connect(self, websocket): async def on_receive(self, websocket, data): fqn=websocket.path_params.get("fqn","") uid=websocket.scope["uid"] - parano_seed = websocket.scope["parano"] if websocket.app.parano else None - if parano_seed: - data = crypto.decrypt(data.encode(),parano_seed).decode() + if websocket.app.parano: + data = crypto.decrypt(data.encode(),parano_seed( uid )).decode() data=json.loads(data) p=HrPilot(uid,fqn) @@ -216,25 +215,26 @@ async def handleHome(request): async def serve(self, request, obj ) -> HTMLResponse: uid = request.scope["uid"] - parano_seed = request.scope["parano"] if self.parano else None fqn=normalize(findfqn(obj)) protocol = "wss" if self.ssl else "ws" - if parano_seed: - jsparano = crypto.JSCRYPTO - jsparano += f"\nvar _PARANO_='{parano_seed}'\n" - jsparano += "\nasync function _read_(x) {return await decrypt(x,_PARANO_)}\n" - jsparano += "\nasync function _write_(x) {return await encrypt(x,_PARANO_)}\n" + if self.parano: + seed = parano_seed( uid ) + + jstunnel = crypto.JSCRYPTO + jstunnel += f"\nvar _PARANO_='{seed}'\n" + jstunnel += "\nasync function _read_(x) {return await decrypt(x,_PARANO_)}\n" + jstunnel += "\nasync function _write_(x) {return await encrypt(x,_PARANO_)}\n" else: - jsparano = "" - jsparano += "\nasync function _read_(x) {return x}\n" - jsparano += "\nasync function _write_(x) {return x}\n" + jstunnel = "" + jstunnel += "\nasync function _read_(x) {return x}\n" + jstunnel += "\nasync function _write_(x) {return x}\n" js = """ -%(jsparano)s +%(jstunnel)s async function interact( o ) { _WS_.send( await _write_(JSON.stringify(o)) ); From e4a8dc34a6d4d12167cbb4f159e9a234b2e33db6 Mon Sep 17 00:00:00 2001 From: manatlan Date: Sun, 1 Oct 2023 18:43:34 +0200 Subject: [PATCH 16/26] code: a lot better (but tag.update crash sometime) --- example.py | 4 +-- htagweb/appserver.py | 9 ++++--- htagweb/server/__init__.py | 55 +++++++++++++++----------------------- 3 files changed, 28 insertions(+), 40 deletions(-) diff --git a/example.py b/example.py index 1117c55..8752d87 100644 --- a/example.py +++ b/example.py @@ -79,7 +79,7 @@ async def loop_timer(self): # With htagweb.WebServer runner provided by htagweb #------------------------------------------------------ from htagweb import SimpleServer,AppServer -app=AppServer( "App:ppp" ,parano=True) +app=AppServer( "example:App" ,parano=True) if __name__=="__main__": - app.run(openBrowser=False) + app.run(openBrowser=True) diff --git a/htagweb/appserver.py b/htagweb/appserver.py index 69760c3..e5f64a3 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -34,7 +34,7 @@ from . import crypto import redys.v2 -from htagweb.server import hrserver2, importClassFromFqn, hrserver, hrserver_orchestrator +from htagweb.server import hrserver from htagweb.server.client import HrPilot logger = logging.getLogger(__name__) @@ -152,10 +152,11 @@ async def on_connect(self, websocket): event=HrPilot(uid,fqn).event_response+"_update" #====================================================== - asyncio.ensure_future(self.loop_tag_update(event,websocket)) - await websocket.accept() + # add the loop to tag.update feature + asyncio.ensure_future(self.loop_tag_update(event,websocket)) + async def on_receive(self, websocket, data): fqn=websocket.path_params.get("fqn","") uid=websocket.scope["uid"] @@ -182,7 +183,7 @@ async def on_disconnect(self, websocket, close_code): def processHrServer(): - asyncio.run( hrserver2() ) + asyncio.run( hrserver() ) class AppServer(Starlette): #NOT THE DEFINITIVE NAME !!!!!!!!!!!!!!!! diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index 3ae718d..def43bf 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -52,32 +52,39 @@ def importClassFromFqn(fqn_norm:str) -> type: def process(hid,event_response,event_interact,fqn,js,init,sesprovidername): + #'''''''''''''''''''''''''''''''''''''''''''''''''''' if sesprovidername is None: sesprovidername="createFile" import htagweb.sessions createSession=getattr(htagweb.sessions,sesprovidername) + #'''''''''''''''''''''''''''''''''''''''''''''''''''' uid=hid.split("_")[0] pid = os.getpid() async def loop(): - if os.getcwd() not in sys.path: sys.path.insert(0,os.getcwd()) - klass=importClassFromFqn(fqn) + with redys.v2.AClient() as bus: + try: + if os.getcwd() not in sys.path: sys.path.insert(0,os.getcwd()) + klass=importClassFromFqn(fqn) + except Exception as e: + print(f">Process {pid} ERROR :",hid,e) + assert await bus.publish(event_response,str(e)) + return - RUNNING=True - def exit(): - RUNNING=False + RUNNING=True + def exit(): + RUNNING=False - session = await createSession(uid) + session = await createSession(uid) - styles=Tag.style("body.htagoff * {cursor:not-allowed !important;}") + styles=Tag.style("body.htagoff * {cursor:not-allowed !important;}") - hr=HRenderer( klass ,js, init=init, exit_callback=exit, fullerror=True, statics=[styles,],session = session) + hr=HRenderer( klass ,js, init=init, exit_callback=exit, fullerror=True, statics=[styles,],session = session) - print(f">Process {pid} started with :",hid,init) + print(f">Process {pid} started with :",hid,init) - with redys.v2.AClient() as bus: # subscribe for interaction await bus.subscribe( event_interact ) @@ -165,7 +172,7 @@ def killall(ps:dict): hid=params["hid"] key_init=str(params["init"]) - if hid in ps: + if hid in ps and ps[hid]["process"].is_alive(): # process is already running if key_init == ps[hid]["key"]: @@ -195,32 +202,12 @@ def killall(ps:dict): print("hrserver_orchestrator stopped") - - async def hrserver(): - print("HRSERVER started") - - async def delay(): - await asyncio.sleep(0.3) - await hrserver_orchestrator() - - asyncio.ensure_future( delay() ) - await redys.Server() - - -async def hrserver2(): print("HRSERVER2 started") - - async def delay(): - await asyncio.sleep(0.3) - await hrserver_orchestrator() - - asyncio.ensure_future( delay() ) - s=redys.v2.Server() s.start() - while 1: - await asyncio.sleep(1) + await asyncio.sleep(0.5) #TODO: can do better + await hrserver_orchestrator() if __name__=="__main__": - asyncio.run( hrserver2() ) + asyncio.run( hrserver() ) From 419b921427e71daffde968ecdd0c8f5f904e5c96 Mon Sep 17 00:00:00 2001 From: manatlan Date: Sun, 1 Oct 2023 18:44:34 +0200 Subject: [PATCH 17/26] code: add todo --- htagweb/server/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index def43bf..5321b66 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -69,6 +69,7 @@ async def loop(): klass=importClassFromFqn(fqn) except Exception as e: print(f">Process {pid} ERROR :",hid,e) + #TODO: do better here assert await bus.publish(event_response,str(e)) return From f6174746abc54113eaca0ad5b9cf4ea9fc514ce8 Mon Sep 17 00:00:00 2001 From: manatlan Date: Mon, 2 Oct 2023 08:08:08 +0200 Subject: [PATCH 18/26] code: add session MemDict with redys --- htagweb/sessions/__init__.py | 15 +++++++---- htagweb/sessions/file.py | 7 +++--- htagweb/sessions/mem.py | 49 ++++++++++++++++++++++++++++++++++++ htagweb/sessions/shm.py | 1 + test_sessions.py | 7 ++++-- 5 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 htagweb/sessions/mem.py diff --git a/htagweb/sessions/__init__.py b/htagweb/sessions/__init__.py index 9bff45f..beea6d6 100644 --- a/htagweb/sessions/__init__.py +++ b/htagweb/sessions/__init__.py @@ -8,15 +8,20 @@ # ############################################################################# async def createFile(uid): - from . import file - return await file.create(uid) + from .file import FileDict + return FileDict(uid) async def createFilePersistent(uid): # <- persistent after server reboot - from . import file - return await file.create(uid,True) + from .file import FilePersistentDict + return FilePersistentDict(uid) +#DEPRECATED async def createShm(uid): from . import shm return await shm.create(uid) -__all__= ["createFile","createFilePersistent","createShm"] \ No newline at end of file +async def createMem(uid): + from .mem import MemDict + return MemDict(uid) + +__all__= ["createFile","createFilePersistent","createShm","createMem"] \ No newline at end of file diff --git a/htagweb/sessions/file.py b/htagweb/sessions/file.py index c5a43d5..571239d 100644 --- a/htagweb/sessions/file.py +++ b/htagweb/sessions/file.py @@ -12,7 +12,7 @@ class FileDict: # default """ mimic a dict (with minimal methods), unique source of truth, based on FS""" - def __init__(self,uid:str,persistent:bool): + def __init__(self,uid:str,persistent:bool=False): self._uid=uid if persistent: name="" @@ -61,5 +61,6 @@ def clear(self): if os.path.isfile(self._file): os.unlink(self._file) -async def create(uid,persistent=False) -> FileDict: - return FileDict(uid,persistent) +class FilePersistentDict(FileDict): # default + def __init__(self,uid): + FileDict.__init__(self,uid,persistent=True) diff --git a/htagweb/sessions/mem.py b/htagweb/sessions/mem.py new file mode 100644 index 0000000..10dabaa --- /dev/null +++ b/htagweb/sessions/mem.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# ############################################################################# +# Copyright (C) 2023 manatlan manatlan[at]gmail(dot)com +# +# MIT licence +# +# https://github.com/manatlan/htagweb +# ############################################################################# + +import os,pickle,tempfile +import redys.v2 + +class MemDict: # 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,{}) + + 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] + + def __delitem__(self,k:str): + """ save session """ + del self._d[k] + self._bus.set(self._uid, self._d) + + def __setitem__(self,k:str,v): + """ save session """ + self._d[k]=v + self._bus.set(self._uid, self._d) + + def clear(self): + """ save session """ + self._d.clear() + self._bus.remove(self._uid) + diff --git a/htagweb/sessions/shm.py b/htagweb/sessions/shm.py index 45d0c50..d4a4ba5 100644 --- a/htagweb/sessions/shm.py +++ b/htagweb/sessions/shm.py @@ -7,6 +7,7 @@ # https://github.com/manatlan/htagweb # ############################################################################# +# DEPRECATED async def create(uid,size=10240): # need to install "shared_memory_dict" (py>=3.8) from shared_memory_dict import SharedMemoryDict diff --git a/test_sessions.py b/test_sessions.py index 279ada6..6532291 100644 --- a/test_sessions.py +++ b/test_sessions.py @@ -1,5 +1,6 @@ import pytest,asyncio,sys -from htagweb.sessions import createFile, createFilePersistent, createShm +from htagweb.sessions import createFile, createFilePersistent, createShm, createMem +from test_server import server async def session_test(method_session): @@ -40,7 +41,9 @@ async def session_test(method_session): finally: session.clear() - +@pytest.mark.asyncio +async def test_sessions_mem( server ): # need redys.v2 runned + await session_test( createMem ) @pytest.mark.asyncio async def test_sessions_file(): From 5358024dec35986c5843722edfe89ac975d04533 Mon Sep 17 00:00:00 2001 From: manatlan Date: Mon, 2 Oct 2023 08:22:23 +0200 Subject: [PATCH 19/26] code: sessions use class now --- htagweb/appserver.py | 6 +++--- htagweb/server/__init__.py | 2 +- htagweb/sessions/__init__.py | 22 ++++------------------ htagweb/simpleserver.py | 8 +++++--- test_sessions.py | 30 ++++++++++-------------------- 5 files changed, 23 insertions(+), 45 deletions(-) diff --git a/htagweb/appserver.py b/htagweb/appserver.py index e5f64a3..0654b25 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -89,7 +89,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: #!!!!!!!!!!!!!!!!!!!!!!!!!!! scope["uid"] = uid - scope["session"] = await self.cbsesprovider(uid) + scope["session"] = self.cbsesprovider(uid) #!!!!!!!!!!!!!!!!!!!!!!!!!!! logger.debug("request for %s, scope=%s",uid,scope) @@ -187,12 +187,12 @@ def processHrServer(): class AppServer(Starlette): #NOT THE DEFINITIVE NAME !!!!!!!!!!!!!!!! - def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True,ssl:bool=False,parano:bool=False,sesprovider:"htagweb.sessions.create*|None"=None): + def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True,ssl:bool=False,parano:bool=False,sesprovider:"htagweb.sessions.class*|None"=None): self.ssl=ssl self.parano=parano if sesprovider is None: - self.sesprovider = sessions.createFile + self.sesprovider = sessions.FileDict else: self.sesprovider = sesprovider diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index 5321b66..ca4b8c8 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -54,7 +54,7 @@ def importClassFromFqn(fqn_norm:str) -> type: def process(hid,event_response,event_interact,fqn,js,init,sesprovidername): #'''''''''''''''''''''''''''''''''''''''''''''''''''' if sesprovidername is None: - sesprovidername="createFile" + sesprovidername="FileDict" import htagweb.sessions createSession=getattr(htagweb.sessions,sesprovidername) #'''''''''''''''''''''''''''''''''''''''''''''''''''' diff --git a/htagweb/sessions/__init__.py b/htagweb/sessions/__init__.py index beea6d6..9c1dbd4 100644 --- a/htagweb/sessions/__init__.py +++ b/htagweb/sessions/__init__.py @@ -6,22 +6,8 @@ # # https://github.com/manatlan/htagweb # ############################################################################# +from .file import FileDict +from .file import FilePersistentDict +from .mem import MemDict -async def createFile(uid): - from .file import FileDict - return FileDict(uid) - -async def createFilePersistent(uid): # <- persistent after server reboot - from .file import FilePersistentDict - return FilePersistentDict(uid) - -#DEPRECATED -async def createShm(uid): - from . import shm - return await shm.create(uid) - -async def createMem(uid): - from .mem import MemDict - return MemDict(uid) - -__all__= ["createFile","createFilePersistent","createShm","createMem"] \ No newline at end of file +__all__= ["FileDict","FilePersistentDict","MemDict"] \ No newline at end of file diff --git a/htagweb/simpleserver.py b/htagweb/simpleserver.py index f75a16c..44b60b7 100644 --- a/htagweb/simpleserver.py +++ b/htagweb/simpleserver.py @@ -132,7 +132,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: #!!!!!!!!!!!!!!!!!!!!!!!!!!! scope["uid"] = uid - scope["session"] = await self.cbsesprovider(uid) + scope["session"] = self.cbsesprovider(uid) #!!!!!!!!!!!!!!!!!!!!!!!!!!! logger.debug("request for %s, scope=%s",uid,scope) @@ -231,11 +231,13 @@ async def on_disconnect(self, websocket, close_code): del self.hr class SimpleServer(Starlette): - def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True,ssl:bool=False,parano:bool=False,sesprovider:"htagweb.sessions.create*|None"=None): + def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True,ssl:bool=False,parano:bool=False,sesprovider:"htagweb.sessions.class*|None"=None): self.ssl=ssl self.parano = str(uuid.uuid4()) if parano else None + if sesprovider is None: - sesprovider = sessions.createFile + sesprovider = sessions.FileDict + Starlette.__init__( self, debug=debug, routes=[WebSocketRoute("/_/{fqn}", HRSocket)], diff --git a/test_sessions.py b/test_sessions.py index 6532291..a4f3911 100644 --- a/test_sessions.py +++ b/test_sessions.py @@ -1,10 +1,10 @@ import pytest,asyncio,sys -from htagweb.sessions import createFile, createFilePersistent, createShm, createMem +from htagweb.sessions import FileDict,FilePersistentDict,MemDict from test_server import server -async def session_test(method_session): - session = await method_session("uid") +def session_test(factory): + session = factory("uid") try: # bad way to clone with pytest.raises(Exception): @@ -23,7 +23,7 @@ async def session_test(method_session): # ensure persistance is present - session = await method_session("uid") + session = factory("uid") assert session["nb"]==1 session["x"]=42 @@ -35,7 +35,7 @@ async def session_test(method_session): session.clear() assert len(session)==0 - session = await method_session("uid") + session = factory("uid") assert len(session.items())==0 finally: @@ -43,24 +43,14 @@ async def session_test(method_session): @pytest.mark.asyncio async def test_sessions_mem( server ): # need redys.v2 runned - await session_test( createMem ) + session_test( MemDict ) -@pytest.mark.asyncio -async def test_sessions_file(): - await session_test( createFile ) +def test_sessions_file(): + session_test( FileDict ) @pytest.mark.asyncio -async def test_sessions_filepersitent(): - await session_test( createFilePersistent ) - - -@pytest.mark.asyncio -async def test_sessions_shm(): - try: - import shared_memory_dict - await session_test( createShm ) - except: - pass +def test_sessions_filepersitent(): + session_test( FilePersistentDict ) From 6be21ac32fdaff55dfe5e0f89e3d5918253cb53c Mon Sep 17 00:00:00 2001 From: manatlan Date: Mon, 2 Oct 2023 08:24:05 +0200 Subject: [PATCH 20/26] feat: default session as MemDict (redys) --- htagweb/appserver.py | 2 +- htagweb/server/__init__.py | 2 +- htagweb/simpleserver.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/htagweb/appserver.py b/htagweb/appserver.py index 0654b25..55994d0 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -192,7 +192,7 @@ def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True,ssl:bool=F self.parano=parano if sesprovider is None: - self.sesprovider = sessions.FileDict + self.sesprovider = sessions.MemDict else: self.sesprovider = sesprovider diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index ca4b8c8..ad0b0ff 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -54,7 +54,7 @@ def importClassFromFqn(fqn_norm:str) -> type: def process(hid,event_response,event_interact,fqn,js,init,sesprovidername): #'''''''''''''''''''''''''''''''''''''''''''''''''''' if sesprovidername is None: - sesprovidername="FileDict" + sesprovidername="MemDict" import htagweb.sessions createSession=getattr(htagweb.sessions,sesprovidername) #'''''''''''''''''''''''''''''''''''''''''''''''''''' diff --git a/htagweb/simpleserver.py b/htagweb/simpleserver.py index 44b60b7..f3c8811 100644 --- a/htagweb/simpleserver.py +++ b/htagweb/simpleserver.py @@ -234,9 +234,9 @@ class SimpleServer(Starlette): def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True,ssl:bool=False,parano:bool=False,sesprovider:"htagweb.sessions.class*|None"=None): self.ssl=ssl self.parano = str(uuid.uuid4()) if parano else None - + if sesprovider is None: - sesprovider = sessions.FileDict + sesprovider = sessions.MemDict Starlette.__init__( self, debug=debug, From d0d8de2f70be3e09a46b379829887cbbdfc0c86c Mon Sep 17 00:00:00 2001 From: manatlan Date: Mon, 2 Oct 2023 16:47:06 +0200 Subject: [PATCH 21/26] Update appserver.py --- htagweb/appserver.py | 143 +++++++++++++++++++++++++++++++------------ 1 file changed, 103 insertions(+), 40 deletions(-) diff --git a/htagweb/appserver.py b/htagweb/appserver.py index 55994d0..5009720 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -21,7 +21,7 @@ import multiprocessing from htag import Tag from starlette.applications import Starlette -from starlette.responses import HTMLResponse +from starlette.responses import HTMLResponse,PlainTextResponse from starlette.applications import Starlette from starlette.routing import Route,WebSocketRoute from starlette.endpoints import WebSocketEndpoint @@ -186,27 +186,57 @@ def processHrServer(): asyncio.run( hrserver() ) + +async def lifespan(app): + import redys.v2 + from htagweb.server import hrserver_orchestrator + s=redys.v2.Server() + s.start() + + asyncio.ensure_future(hrserver_orchestrator()) + + bus=redys.v2.AClient() + while await bus.get("hrserver_orchestrator_running"): + await asyncio.sleep(0.1) + + print("hrserver ok") + + yield + + s.terminate() + class AppServer(Starlette): #NOT THE DEFINITIVE NAME !!!!!!!!!!!!!!!! - def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True,ssl:bool=False,parano:bool=False,sesprovider:"htagweb.sessions.class*|None"=None): + def __init__(self, + obj:"htag.Tag class|fqn|None"=None, + debug:bool=True, + ssl:bool=False, + parano:bool=False, + httponly:bool=False, + sesprovider:"htagweb.sessions.class*|None"=None, + ): self.ssl=ssl self.parano=parano + self.httponly=httponly if sesprovider is None: - self.sesprovider = sessions.MemDict + self.sesprovider = sessions.FileDict else: - self.sesprovider = sesprovider + self.sesprovider = sesprovider print("Session with:",self.sesprovider.__name__) ################################################################### - p=multiprocessing.Process(target=processHrServer) - p.start() + if httponly: + routes=[Route("/_/{fqn}", self.HRHttp, methods=["POST"])] + else: + routes=[WebSocketRoute("/_/{fqn}", HRSocket)] ################################################################# Starlette.__init__( self, debug=debug, - routes=[WebSocketRoute("/_/{fqn}", HRSocket)], + routes=routes, middleware=[Middleware(WebServerSession,https_only=ssl,sesprovider=self.sesprovider)], + lifespan=lifespan, ) if obj: @@ -216,11 +246,8 @@ async def handleHome(request): async def serve(self, request, obj ) -> HTMLResponse: uid = request.scope["uid"] - fqn=normalize(findfqn(obj)) - protocol = "wss" if self.ssl else "ws" - if self.parano: seed = parano_seed( uid ) @@ -234,45 +261,63 @@ async def serve(self, request, obj ) -> HTMLResponse: jstunnel += "\nasync function _write_(x) {return x}\n" - js = """ + if self.httponly: + # interactions use HTTP POST + js = """ %(jstunnel)s async function interact( o ) { - _WS_.send( await _write_(JSON.stringify(o)) ); + let body = await _write_(JSON.stringify(o)); + let req=await window.fetch("/_/%(fqn)s",{method:"POST", body: body}); + let actions=await req.text(); + action( await _read_(actions) ); } -// instanciate the WEBSOCKET -let _WS_=null; -let retryms=500; - -function connect() { - _WS_= new WebSocket("%(protocol)s://"+location.host+"/_/%(fqn)s"); - _WS_.onopen=function(evt) { - console.log("** WS connected") - document.body.classList.remove("htagoff"); - retryms=500; - start(); - - _WS_.onmessage = async function(e) { - let actions = await _read_(e.data) - action(actions) - }; +window.addEventListener('DOMContentLoaded', start ); +""" % locals() + else: + # interactions use WS + protocol = "wss" if self.ssl else "ws" - } + js = """ + %(jstunnel)s - _WS_.onclose = function(evt) { - console.log("** WS disconnected, retry in (ms):",retryms); - document.body.classList.add("htagoff"); + async function interact( o ) { + _WS_.send( await _write_(JSON.stringify(o)) ); + } - setTimeout( function() { - connect(); - retryms=retryms*2; - }, retryms); - }; -} -connect(); + // instanciate the WEBSOCKET + let _WS_=null; + let retryms=500; + + function connect() { + _WS_= new WebSocket("%(protocol)s://"+location.host+"/_/%(fqn)s"); + _WS_.onopen=function(evt) { + console.log("** WS connected") + document.body.classList.remove("htagoff"); + retryms=500; + start(); + + _WS_.onmessage = async function(e) { + let actions = await _read_(e.data) + action(actions) + }; + + } + + _WS_.onclose = function(evt) { + console.log("** WS disconnected, retry in (ms):",retryms); + document.body.classList.add("htagoff"); + + setTimeout( function() { + connect(); + retryms=retryms*2; + }, retryms); + }; + } + connect(); -""" % locals() + """ % locals() p = HrPilot(uid,fqn,js,self.sesprovider.__name__) @@ -280,7 +325,25 @@ async def serve(self, request, obj ) -> HTMLResponse: html=await p.start(*args,**kargs) return HTMLResponse(html) + async def HRHttp(self,request) -> PlainTextResponse: + uid = request.scope["uid"] + fqn = request.path_params.get("fqn","") + seed = parano_seed( uid ) + + p=HrPilot(uid,fqn) + data = await request.body() + + if self.parano: + data = crypto.decrypt(data,seed).decode() + + data=json.loads(data) + actions=await p.interact( oid=data["id"], method_name=data["method"], args=data["args"], kargs=data["kargs"], event=data.get("event") ) + txt=json.dumps(actions) + + if self.parano: + txt = crypto.encrypt(txt.encode(),seed) + return PlainTextResponse(txt) def run(self, host="0.0.0.0", port=8000, openBrowser=False): # localhost, by default !! if openBrowser: From 6887f06d6490c02f8dd665d02e5ce68d86a1e76e Mon Sep 17 00:00:00 2001 From: manatlan Date: Mon, 2 Oct 2023 18:50:07 +0200 Subject: [PATCH 22/26] code: use lifespan --- example.py | 7 ++++++- htagweb/appserver.py | 32 ++++++++++---------------------- htagweb/server/__init__.py | 19 +++++++++++++------ htagweb/sessions/mem.py | 4 ++-- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/example.py b/example.py index 8752d87..d82768b 100644 --- a/example.py +++ b/example.py @@ -79,7 +79,12 @@ async def loop_timer(self): # With htagweb.WebServer runner provided by htagweb #------------------------------------------------------ from htagweb import SimpleServer,AppServer -app=AppServer( "example:App" ,parano=True) +app=AppServer( App ,parano=False,httponly=True) if __name__=="__main__": + #~ import logging + #~ logging.basicConfig(format='[%(levelname)-5s] %(name)s: %(message)s',level=logging.DEBUG) + #~ logging.getLogger("redys.servone").setLevel( logging.INFO ) + + app.run(openBrowser=True) diff --git a/htagweb/appserver.py b/htagweb/appserver.py index 5009720..7bb5400 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -186,40 +186,28 @@ def processHrServer(): asyncio.run( hrserver() ) - async def lifespan(app): - import redys.v2 - from htagweb.server import hrserver_orchestrator - s=redys.v2.Server() - s.start() - - asyncio.ensure_future(hrserver_orchestrator()) - - bus=redys.v2.AClient() - while await bus.get("hrserver_orchestrator_running"): - await asyncio.sleep(0.1) - - print("hrserver ok") - + process_hrserver=multiprocessing.Process(target=processHrServer) + process_hrserver.start() yield - - s.terminate() + process_hrserver.terminate() + class AppServer(Starlette): #NOT THE DEFINITIVE NAME !!!!!!!!!!!!!!!! def __init__(self, - obj:"htag.Tag class|fqn|None"=None, + obj:"htag.Tag class|fqn|None"=None, debug:bool=True, ssl:bool=False, parano:bool=False, httponly:bool=False, - sesprovider:"htagweb.sessions.class*|None"=None, + sesprovider:"sessions.MemDict|sessions.FileDict|sessions.FilePersistentDict|None"=None, ): self.ssl=ssl self.parano=parano self.httponly=httponly if sesprovider is None: - self.sesprovider = sessions.FileDict + self.sesprovider = sessions.MemDict else: self.sesprovider = sesprovider @@ -332,14 +320,14 @@ async def HRHttp(self,request) -> PlainTextResponse: p=HrPilot(uid,fqn) data = await request.body() - + if self.parano: data = crypto.decrypt(data,seed).decode() - + data=json.loads(data) actions=await p.interact( oid=data["id"], method_name=data["method"], args=data["args"], kargs=data["kargs"], event=data.get("event") ) txt=json.dumps(actions) - + if self.parano: txt = crypto.encrypt(txt.encode(),seed) diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index ad0b0ff..413e5b8 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -33,8 +33,7 @@ def importClassFromFqn(fqn_norm:str) -> type: except ModuleNotFoundError as e: """ can't be (really) reloaded if the component is in the same module as the instance htag server""" - print(e) - pass + print("*WARNING* can't force module reload:",e) else: module=importlib.import_module(modulename) #--------------------------- @@ -56,7 +55,7 @@ def process(hid,event_response,event_interact,fqn,js,init,sesprovidername): if sesprovidername is None: sesprovidername="MemDict" import htagweb.sessions - createSession=getattr(htagweb.sessions,sesprovidername) + FactorySession=getattr(htagweb.sessions,sesprovidername) #'''''''''''''''''''''''''''''''''''''''''''''''''''' uid=hid.split("_")[0] @@ -77,7 +76,7 @@ async def loop(): def exit(): RUNNING=False - session = await createSession(uid) + session = FactorySession(uid) styles=Tag.style("body.htagoff * {cursor:not-allowed !important;}") @@ -204,10 +203,18 @@ def killall(ps:dict): print("hrserver_orchestrator stopped") async def hrserver(): - print("HRSERVER2 started") s=redys.v2.Server() s.start() - await asyncio.sleep(0.5) #TODO: can do better + + bus=redys.v2.AClient() + while 1: + try: + if await bus.ping()=="pong": + break + except: + pass + await asyncio.sleep(0.1) + await hrserver_orchestrator() if __name__=="__main__": diff --git a/htagweb/sessions/mem.py b/htagweb/sessions/mem.py index 10dabaa..8d9abaf 100644 --- a/htagweb/sessions/mem.py +++ b/htagweb/sessions/mem.py @@ -15,7 +15,7 @@ class MemDict: # default def __init__(self,uid:str): self._uid=uid self._bus=redys.v2.Client() - self._d=self._bus.get(self._uid,{}) + self._d=self._bus.get(self._uid) or {} def __len__(self): return len(self._d.keys()) @@ -45,5 +45,5 @@ def __setitem__(self,k:str,v): def clear(self): """ save session """ self._d.clear() - self._bus.remove(self._uid) + self._bus.delete(self._uid) From f579bed227d0e27d14a6c1de906677830c387b90 Mon Sep 17 00:00:00 2001 From: manatlan Date: Mon, 2 Oct 2023 19:10:21 +0200 Subject: [PATCH 23/26] nothing --- htagweb/server/__init__.py | 7 ++----- htagweb/server/client.py | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index 413e5b8..13fffcb 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -105,11 +105,7 @@ async def update(actions): while RUNNING: params = await bus.get_event( event_interact ) - # try: #TODO: but fail the test_server.py ?!?!? - # except: # some times it crash ;-( - # print("!!! concurrent sockets reads !!!") - # params = None - if params and isinstance(params,dict): # sometimes it's not a dict ?!? (bool ?!) + if params is not None: # sometimes it's not a dict ?!? (bool ?!) if params.get("cmd") == CMD_RENDER: # just a false start, just need the current render print(f">Process {pid} render {hid}") @@ -184,6 +180,7 @@ def killall(ps:dict): else: # kill itself because it's not the same init params ps[hid]["process"].terminate() + # and recreate another one later # create the process diff --git a/htagweb/server/client.py b/htagweb/server/client.py index 6859234..e716f82 100644 --- a/htagweb/server/client.py +++ b/htagweb/server/client.py @@ -12,7 +12,7 @@ import redys.v2 from htagweb.server import EVENT_SERVER -TIMEOUT=5 # sec to wait answer from redys server #TODO: set better +TIMEOUT=20 # sec to wait answer from redys server #TODO: set better class HrPilot: def __init__(self,uid:str,fqn:str,js:str=None,sesprovidername=None): From 40ea75ad2a5a8247ee37cd39246e9566dcf35680 Mon Sep 17 00:00:00 2001 From: manatlan Date: Mon, 2 Oct 2023 19:47:55 +0200 Subject: [PATCH 24/26] working ;-) --- example.py | 6 +++--- htagweb/server/__init__.py | 5 +++-- htagweb/simpleserver.py | 5 ++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/example.py b/example.py index d82768b..0d22f61 100644 --- a/example.py +++ b/example.py @@ -1,5 +1,5 @@ from htag import Tag -import json,asyncio,time +import json,asyncio,time,os """ Complex htag's app to test: @@ -45,7 +45,7 @@ def clllll(o): self.state.clear() - self <= Tag.div(v) + self <= Tag.div(f"V{v} (pid:{os.getpid()})") self <= Tag.button("inc integer",_onclick=inc_test_session) self <= Tag.button("add list",_onclick=addd) self <= Tag.button("clear",_onclick=clllll) @@ -79,7 +79,7 @@ async def loop_timer(self): # With htagweb.WebServer runner provided by htagweb #------------------------------------------------------ from htagweb import SimpleServer,AppServer -app=AppServer( App ,parano=False,httponly=True) +app=AppServer( App ,parano=False,httponly=False) if __name__=="__main__": #~ import logging diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index 13fffcb..6e3fd0c 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -147,7 +147,7 @@ async def hrserver_orchestrator(): def killall(ps:dict): # try to send a EXIT CMD to all running ps for hid,infos in ps.items(): - ps[hid]["process"].terminate() + ps[hid]["process"].kill() while 1: params = await bus.get_event( EVENT_SERVER ) @@ -179,7 +179,8 @@ def killall(ps:dict): continue else: # kill itself because it's not the same init params - ps[hid]["process"].terminate() + print("Reload a new process",hid) + ps[hid]["process"].kill() # and recreate another one later diff --git a/htagweb/simpleserver.py b/htagweb/simpleserver.py index f3c8811..0e2eba0 100644 --- a/htagweb/simpleserver.py +++ b/htagweb/simpleserver.py @@ -231,12 +231,11 @@ async def on_disconnect(self, websocket, close_code): del self.hr class SimpleServer(Starlette): - def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True,ssl:bool=False,parano:bool=False,sesprovider:"htagweb.sessions.class*|None"=None): + def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True,ssl:bool=False,parano:bool=False): self.ssl=ssl self.parano = str(uuid.uuid4()) if parano else None - if sesprovider is None: - sesprovider = sessions.MemDict + sesprovider = sessions.FileDict Starlette.__init__( self, debug=debug, From 23d7b2eb30e61bf00691be8c8817b72d9a5707fa Mon Sep 17 00:00:00 2001 From: manatlan Date: Mon, 2 Oct 2023 20:40:39 +0200 Subject: [PATCH 25/26] all seems work --- htagweb/__main__.py | 4 +- htagweb/appserver.py | 2 +- htagweb/htagserver.py | 7 +- htagweb/server/__init__.py | 3 + htagweb/simpleserver.py | 156 +++---------------------------------- test_htagserver.py | 12 --- test_server.py | 26 +++++-- 7 files changed, 39 insertions(+), 171 deletions(-) diff --git a/htagweb/__main__.py b/htagweb/__main__.py index e464c76..f993ca1 100644 --- a/htagweb/__main__.py +++ b/htagweb/__main__.py @@ -13,9 +13,9 @@ if __name__=="__main__": if len(sys.argv)==1: - app=HtagServer(None, debug=True,ssl=False) + app=HtagServer(None, debug=True) elif len(sys.argv)==2: - app=HtagServer(sys.argv[1], debug=True,ssl=False) + app=HtagServer(sys.argv[1], debug=True) else: print("bad call (only one paremeter is possible (a fqn, ex: 'main:App'))") sys.exit(-1) diff --git a/htagweb/appserver.py b/htagweb/appserver.py index 7bb5400..78a143b 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -193,7 +193,7 @@ async def lifespan(app): process_hrserver.terminate() -class AppServer(Starlette): #NOT THE DEFINITIVE NAME !!!!!!!!!!!!!!!! +class AppServer(Starlette): def __init__(self, obj:"htag.Tag class|fqn|None"=None, debug:bool=True, diff --git a/htagweb/htagserver.py b/htagweb/htagserver.py index d82a02c..76e85a3 100644 --- a/htagweb/htagserver.py +++ b/htagweb/htagserver.py @@ -23,7 +23,8 @@ from starlette.responses import HTMLResponse,Response from htag import Tag -from .simpleserver import SimpleServer,getClass +from .simpleserver import SimpleServer +from .server import importClassFromFqn #################################################### class IndexApp(Tag.body): @@ -74,10 +75,10 @@ async def _serve(self, request) -> HTMLResponse: fqn_norm="".join( reversed("".join(reversed(fqn)).replace(".",":",1))) try: - klass=getClass(fqn_norm) + klass=importClassFromFqn(fqn_norm) except: try: - klass=getClass(fqn+":App") + klass=importClassFromFqn(fqn+":App") except ModuleNotFoundError: return HTMLResponse("Not Found (%s)" % fqn,404,media_type="text/plain") diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index 6e3fd0c..2333abf 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -215,5 +215,8 @@ async def hrserver(): await hrserver_orchestrator() + + + if __name__=="__main__": asyncio.run( hrserver() ) diff --git a/htagweb/simpleserver.py b/htagweb/simpleserver.py index 0e2eba0..a04140c 100644 --- a/htagweb/simpleserver.py +++ b/htagweb/simpleserver.py @@ -33,13 +33,8 @@ import os import sys import json -import uuid -import pickle -import inspect import logging import uvicorn -import importlib -import contextlib from htag import Tag from starlette.applications import Starlette from starlette.responses import HTMLResponse @@ -53,105 +48,13 @@ from htag.render import HRenderer from htag.runners import commons -from . import crypto logger = logging.getLogger(__name__) #################################################### -from types import ModuleType from . import sessions - -def findfqn(x) -> str: - if isinstance(x,str): - if ("." not in x) and (":" not in x): - raise Exception(f"'{x}' is not a 'full qualified name' (expected 'module.name') of an App (htag.Tag class)") - return x # /!\ x is a fqn /!\ DANGEROUS /!\ - elif isinstance(x, ModuleType): - if hasattr(x,"App"): - tagClass=getattr(x,"App") - if not issubclass(tagClass,Tag): - raise Exception("The 'App' of the module is not inherited from 'htag.Tag class'") - else: - raise Exception("module should contains a 'App' (htag.Tag class)") - elif issubclass(x,Tag): - tagClass=x - else: - raise Exception(f"!!! wtf ({x}) ???") - - return tagClass.__module__+"."+tagClass.__qualname__ - -def getClass(fqn_norm:str) -> type: - assert ":" in fqn_norm - #--------------------------- fqn -> module, name - modulename,name = fqn_norm.split(":",1) - if modulename in sys.modules: - module=sys.modules[modulename] - try: - module=importlib.reload( module ) - except ModuleNotFoundError: - """ can't be (really) reloaded if the component is in the - same module as the instance htag server""" - pass - else: - module=importlib.import_module(modulename) - #--------------------------- - klass= getattr(module,name) - if not ( inspect.isclass(klass) and issubclass(klass,Tag) ): - raise Exception(f"'{fqn_norm}' is not a htag.Tag subclass") - - if not hasattr(klass,"imports"): - # if klass doesn't declare its imports - # we prefer to set them empty - # to avoid clutering - klass.imports=[] - return klass - - -class WebServerSession: # ASGI Middleware, for starlette - def __init__(self, app:ASGIApp, https_only:bool = False, sesprovider:"async method(uid)"=None ) -> None: - self.app = app - self.session_cookie = "session" - self.max_age = 0 - self.path = "/" - self.security_flags = "httponly; samesite=lax" - if https_only: # Secure flag can be used with HTTPS only - self.security_flags += "; secure" - self.cbsesprovider=sesprovider - - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - if scope["type"] not in ("http", "websocket"): # pragma: no cover - await self.app(scope, receive, send) - return - - connection = HTTPConnection(scope) - - if self.session_cookie in connection.cookies: - uid = connection.cookies[self.session_cookie] - else: - uid = str(uuid.uuid4()) - - #!!!!!!!!!!!!!!!!!!!!!!!!!!! - scope["uid"] = uid - scope["session"] = self.cbsesprovider(uid) - #!!!!!!!!!!!!!!!!!!!!!!!!!!! - - logger.debug("request for %s, scope=%s",uid,scope) - - async def send_wrapper(message: Message) -> None: - if message["type"] == "http.response.start": - # send it back, in all cases - headers = MutableHeaders(scope=message) - header_value = "{session_cookie}={data}; path={path}; {max_age}{security_flags}".format( # noqa E501 - session_cookie=self.session_cookie, - data=uid, - path=self.path, - max_age=f"Max-Age={self.max_age}; " if self.max_age else "", - security_flags=self.security_flags, - ) - headers.append("Set-Cookie", header_value) - await send(message) - - await self.app(scope, receive, send_wrapper) +from .appserver import WebServerSession,findfqn +from .server import importClassFromFqn def fqn2hr(fqn:str,js:str,init,session,fullerror=False): # fqn is a "full qualified name", full ! @@ -159,7 +62,7 @@ def fqn2hr(fqn:str,js:str,init,session,fullerror=False): # fqn is a "full qualif # replace last "." by ":" fqn="".join( reversed("".join(reversed(fqn)).replace(".",":",1))) - klass=getClass(fqn) + klass=importClassFromFqn(fqn) styles=Tag.style("body.htagoff * {cursor:not-allowed !important;}") @@ -170,9 +73,6 @@ class HRSocket(WebSocketEndpoint): async def _sendback(self,ws, txt:str) -> bool: try: - if ws.app.parano: - txt = crypto.encrypt(txt.encode(),ws.app.parano) - await ws.send_text( txt ) return True except Exception as e: @@ -214,8 +114,6 @@ async def on_connect(self, websocket): self.hr.sendactions=lambda actions: self._sendback(websocket,json.dumps(actions)) async def on_receive(self, websocket, data): - if websocket.app.parano: - data = crypto.decrypt(data.encode(),websocket.app.parano).decode() data=json.loads(data) @@ -231,16 +129,13 @@ async def on_disconnect(self, websocket, close_code): del self.hr class SimpleServer(Starlette): - def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True,ssl:bool=False,parano:bool=False): - self.ssl=ssl - self.parano = str(uuid.uuid4()) if parano else None - + def __init__(self,obj:"htag.Tag class|fqn|None"=None, debug:bool=True): sesprovider = sessions.FileDict Starlette.__init__( self, debug=debug, routes=[WebSocketRoute("/_/{fqn}", HRSocket)], - middleware=[Middleware(WebServerSession,https_only=ssl,sesprovider=sesprovider)], + middleware=[Middleware(WebServerSession,https_only=False,sesprovider=sesprovider)], ) if obj: @@ -250,20 +145,15 @@ async def handleHome(request): async def serve(self, request, obj ) -> HTMLResponse: fqn=findfqn(obj) - protocol = "wss" if self.ssl else "ws" - if self.parano: - jsparano = crypto.JSCRYPTO - jsparano += f"\nvar _PARANO_='{self.parano}'\n" - jsparano += "\nasync function _read_(x) {return await decrypt(x,_PARANO_)}\n" - jsparano += "\nasync function _write_(x) {return await encrypt(x,_PARANO_)}\n" - else: - jsparano = "" - jsparano += "\nasync function _read_(x) {return x}\n" - jsparano += "\nasync function _write_(x) {return x}\n" + protocol = "ws" + + jsinc = "" + jsinc += "\nasync function _read_(x) {return x}\n" + jsinc += "\nasync function _write_(x) {return x}\n" #TODO: consider https://developer.chrome.com/blog/removing-document-write/ jsbootstrap=""" - %(jsparano)s + %(jsinc)s // instanciate the WEBSOCKET let _WS_=null; let retryms=500; @@ -307,30 +197,6 @@ async def serve(self, request, obj ) -> HTMLResponse: return HTMLResponse( str(hr) ) - # bootstrapHtmlPage=""" - # - # - # - # - # loading - # - # """ % locals() - - # return HTMLResponse( bootstrapHtmlPage ) def run(self, host="0.0.0.0", port=8000, openBrowser=False): # localhost, by default !! if openBrowser: diff --git a/test_htagserver.py b/test_htagserver.py index 49e5d87..b6f7f3f 100644 --- a/test_htagserver.py +++ b/test_htagserver.py @@ -78,18 +78,6 @@ def test_simpleserver(): -def test_parano(): - app=HtagServer(parano=True) - with TestClient(app) as client: - response = client.get('/') - - # assert that get bootstrap page - assert response.status_code == 200 - assert "document.write(html)" in response.text - assert "_PARANO_" in response.text - assert "encrypt" in response.text - - # the rest will be encrypted ;-) if __name__=="__main__": # test_basic() diff --git a/test_server.py b/test_server.py index 3eb2e5c..0dd619c 100644 --- a/test_server.py +++ b/test_server.py @@ -5,6 +5,8 @@ import time from htagweb.appserver import processHrServer from htagweb.server.client import HrPilot +import redys.v2 +import threading @pytest.fixture() @@ -12,24 +14,32 @@ def server(): p=multiprocessing.Process(target=processHrServer) p.start() - time.sleep(1) yield "x" + p.terminate() @pytest.mark.asyncio async def test_base( server ): + + # while 1: + # try: + # if await redys.v2.AClient().get("hrserver_orchestrator_running"): + # break + # except Exception as e: + # print(e) + # await asyncio.sleep(0.5) + + uid ="u1" p=HrPilot(uid,"test_hr:App","//") - html=await p.start() - assert html.startswith("") + # html=await p.start() + # assert html.startswith("") + + # actions=await p.interact( oid="ut", method_name="doit", args=[], kargs={}, event={} ) + # assert "update" in actions - actions=await p.interact( oid="ut", method_name="doit", args=[], kargs={}, event={} ) - assert "update" in actions - # await p.kill() - # await p.kill() - # await p.kill() if __name__=="__main__": From 1b26254581324cd1d8bd0807177e81cef45c7bbe Mon Sep 17 00:00:00 2001 From: manatlan Date: Tue, 3 Oct 2023 08:12:53 +0200 Subject: [PATCH 26/26] seems really good (except ws/update) --- htagweb/appserver.py | 13 ++++++------ htagweb/server/__init__.py | 33 ++++++++++++++++++++++------- htagweb/server/client.py | 6 ++++-- htagweb/simpleserver.py | 1 - test_server.py | 43 +++++++++++++++++--------------------- 5 files changed, 56 insertions(+), 40 deletions(-) diff --git a/htagweb/appserver.py b/htagweb/appserver.py index 78a143b..5f3a3e5 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -35,7 +35,7 @@ import redys.v2 from htagweb.server import hrserver -from htagweb.server.client import HrPilot +from htagweb.server.client import HrClient logger = logging.getLogger(__name__) #################################################### @@ -135,6 +135,7 @@ async def _sendback(self,websocket, txt:str) -> bool: return False async def loop_tag_update(self, event, websocket): + #TODO: there is trouble here sometimes ... to fix ! with redys.v2.AClient() as bus: await bus.subscribe(event) @@ -149,7 +150,7 @@ async def on_connect(self, websocket): #====================================================== get the event fqn=websocket.path_params.get("fqn","") uid=websocket.scope["uid"] - event=HrPilot(uid,fqn).event_response+"_update" + event=HrClient(uid,fqn).event_response+"_update" #====================================================== await websocket.accept() @@ -165,7 +166,7 @@ async def on_receive(self, websocket, data): data = crypto.decrypt(data.encode(),parano_seed( uid )).decode() data=json.loads(data) - p=HrPilot(uid,fqn) + p=HrClient(uid,fqn) actions=await p.interact( oid=data["id"], method_name=data["method"], args=data["args"], kargs=data["kargs"], event=data.get("event") ) @@ -175,7 +176,7 @@ async def on_disconnect(self, websocket, close_code): #====================================================== get the event fqn=websocket.path_params.get("fqn","") uid=websocket.scope["uid"] - event=HrPilot(uid,fqn).event_response+"_update" + event=HrClient(uid,fqn).event_response+"_update" #====================================================== with redys.v2.AClient() as bus: @@ -307,7 +308,7 @@ async def serve(self, request, obj ) -> HTMLResponse: """ % locals() - p = HrPilot(uid,fqn,js,self.sesprovider.__name__) + p = HrClient(uid,fqn,js,self.sesprovider.__name__) args,kargs = commons.url2ak(str(request.url)) html=await p.start(*args,**kargs) @@ -318,7 +319,7 @@ async def HRHttp(self,request) -> PlainTextResponse: fqn = request.path_params.get("fqn","") seed = parano_seed( uid ) - p=HrPilot(uid,fqn) + p=HrClient(uid,fqn) data = await request.body() if self.parano: diff --git a/htagweb/server/__init__.py b/htagweb/server/__init__.py index 2333abf..84ba67a 100644 --- a/htagweb/server/__init__.py +++ b/htagweb/server/__init__.py @@ -50,7 +50,7 @@ def importClassFromFqn(fqn_norm:str) -> type: -def process(hid,event_response,event_interact,fqn,js,init,sesprovidername): +def process(uid,hid,event_response,event_interact,fqn,js,init,sesprovidername): #'''''''''''''''''''''''''''''''''''''''''''''''''''' if sesprovidername is None: sesprovidername="MemDict" @@ -58,8 +58,6 @@ def process(hid,event_response,event_interact,fqn,js,init,sesprovidername): FactorySession=getattr(htagweb.sessions,sesprovidername) #'''''''''''''''''''''''''''''''''''''''''''''''''''' - uid=hid.split("_")[0] - pid = os.getpid() async def loop(): with redys.v2.AClient() as bus: @@ -200,10 +198,7 @@ def killall(ps:dict): print("hrserver_orchestrator stopped") -async def hrserver(): - s=redys.v2.Server() - s.start() - +async def wait_redys(): bus=redys.v2.AClient() while 1: try: @@ -213,8 +208,32 @@ async def hrserver(): pass await asyncio.sleep(0.1) +async def wait_hrserver(): + bus=redys.v2.AClient() + while 1: + try: + if await bus.get("hrserver_orchestrator_running"): + break + except Exception as e: + print(e) + await asyncio.sleep(0.5) + + +async def kill_hrserver(): + bus=redys.v2.AClient() + await bus.publish( EVENT_SERVER, dict(cmd=CMD_EXIT) ) # kill orchestrator loop + + +async def hrserver(): + s=redys.v2.Server() + s.start() + + await wait_redys() + await hrserver_orchestrator() + s.stop() + diff --git a/htagweb/server/client.py b/htagweb/server/client.py index e716f82..b7dff15 100644 --- a/htagweb/server/client.py +++ b/htagweb/server/client.py @@ -14,9 +14,10 @@ TIMEOUT=20 # sec to wait answer from redys server #TODO: set better -class HrPilot: +class HrClient: def __init__(self,uid:str,fqn:str,js:str=None,sesprovidername=None): """ !!!!!!!!!!!!!!!!!!!! if js|sesprovidername is None : can't do a start() !!!!!!!!!!!!!!!!!!!!!!""" + self.uid=uid self.fqn=fqn self.js=js self.bus = redys.v2.AClient() @@ -47,6 +48,7 @@ async def start(self,*a,**k) -> str: # start the process app assert await self.bus.publish( EVENT_SERVER , dict( + uid=self.uid, hid=self.hid, event_response=self.event_response, event_interact=self.event_interact, @@ -99,7 +101,7 @@ async def clean(): async def main(): uid ="u1" - p=HrPilot(uid,"obj:App","//") + p=HrClient(uid,"obj:App","//") #~ html=await p.start() #~ print(html) diff --git a/htagweb/simpleserver.py b/htagweb/simpleserver.py index a04140c..eae617c 100644 --- a/htagweb/simpleserver.py +++ b/htagweb/simpleserver.py @@ -150,7 +150,6 @@ async def serve(self, request, obj ) -> HTMLResponse: jsinc = "" jsinc += "\nasync function _read_(x) {return x}\n" jsinc += "\nasync function _write_(x) {return x}\n" - #TODO: consider https://developer.chrome.com/blog/removing-document-write/ jsbootstrap=""" %(jsinc)s diff --git a/test_server.py b/test_server.py index 0dd619c..648a52d 100644 --- a/test_server.py +++ b/test_server.py @@ -4,7 +4,8 @@ import multiprocessing,threading import time from htagweb.appserver import processHrServer -from htagweb.server.client import HrPilot +from htagweb.server import kill_hrserver, wait_hrserver +from htagweb.server.client import HrClient import redys.v2 import threading @@ -14,40 +15,34 @@ def server(): p=multiprocessing.Process(target=processHrServer) p.start() + asyncio.run( wait_hrserver() ) + yield "x" - p.terminate() + asyncio.run( kill_hrserver() ) @pytest.mark.asyncio async def test_base( server ): - # while 1: - # try: - # if await redys.v2.AClient().get("hrserver_orchestrator_running"): - # break - # except Exception as e: - # print(e) - # await asyncio.sleep(0.5) - - uid ="u1" - p=HrPilot(uid,"test_hr:App","//") - # html=await p.start() - # assert html.startswith("") + p=HrClient(uid,"test_hr:App","//") + html=await p.start() + assert html.startswith("") - # actions=await p.interact( oid="ut", method_name="doit", args=[], kargs={}, event={} ) - # assert "update" in actions + actions=await p.interact( oid="ut", method_name="doit", args=[], kargs={}, event={} ) + assert "update" in actions if __name__=="__main__": - p=multiprocessing.Process(target=processHrServer) - try: - p.start() - time.sleep(1) - - asyncio.run( test_base(42) ) - finally: - p.terminate() \ No newline at end of file + pass + # p=multiprocessing.Process(target=processHrServer) + # try: + # p.start() + # time.sleep(1) + + # asyncio.run( test_base(42) ) + # finally: + # asyncio.run( kill_hrserver() ) \ No newline at end of file