<reponame>nbyoung/nuPy<filename>web.py
from collections import namedtuple
import _thread

try:
    import uasyncio as asyncio
except ImportError:
    import asyncio

from configuration import Status
import service

from MicroWebSrv2 import MicroWebSrv2, RegisterRoute

class HTTP:
    GET     = 'GET'
    HEAD    = 'HEAD'
    POST    = 'POST'
    PUT     = 'PUT'
    DELETE  = 'DELETE'
    OPTIONS = 'OPTIONS'
    PATCH   = 'PATCH'

Route = namedtuple('Route', ('name', 'method', 'path', 'callback', 'data'))

class API:

    class _Endpoint:

        def __init__(self, name, path, status, isWritable):
            self._name = name
            self._status = status
            method = HTTP.GET
            RegisterRoute(self._get, method, path, name + method)
            if isWritable:
                method = HTTP.POST
                RegisterRoute(self._post, method, path, name + method)

        def _get(self, mws2, request):
            async def sendJSON():
                async with self._status.getter as getter:
                    request.Response.ContentType = 'application/json'
                    request.Response.ReturnOk(
                        getter.getInitialJSON() if 'initial' in request.QueryParams
                        else getter.getJSON()
                    )
            asyncio.run(sendJSON())

        def _post(self, mws2, request):
            async def receiveJSON():
                async with self._status.setter as setter:
                    respond = request.Response.ReturnBadRequest
                    if request.ContentType.lower() == 'application/json' :
                        try:
                            jsonString = bytes(request.Content).decode('UTF-8')
                            setter.updateJSON(jsonString)
                            respond = request.Response.ReturnOk
                        except Exception as exception:
                            pass
                    respond()
            asyncio.run(receiveJSON())

    _webSocketModule = MicroWebSrv2.LoadModule('WebSockets')

    def __init__(self, basePath):
        self._basePath = basePath
        self._webSocketClients = []
        self._webSocketClientsLock = _thread.allocate_lock()
        def onWebSocketTextMessage(webSocket, textMessage):
            pass
        def onWebSocketBinaryMessage(webSocket, binaryMessage):
            pass
        def onWebSocketClosed(webSocket):
            with self._webSocketClientsLock:
                if webSocket in self._webSocketClients:
                    self._webSocketClients.remove(webSocket)
        def onWebSocketAccepted(mws2, webSocket):
            with self._webSocketClientsLock:
                self._webSocketClients.append(webSocket)
            webSocket.OnTextMessage = onWebSocketTextMessage
            webSocket.OnBinaryMessage = onWebSocketBinaryMessage
            webSocket.OnClosed = onWebSocketClosed
        API._webSocketModule.OnWebSocketAccepted = onWebSocketAccepted

    def _notifyClients(self, name):
        with self._webSocketClientsLock:
            for client in self._webSocketClients:
                client.SendTextMessage(name)

    def _close(self):
        with self._webSocketClientsLock:
            for client in self._webSocketClients:
                client.Close()

    def add(self, name, jsonStore, isWritable=True):
        path = '%s/%s' % (self._basePath, name)
        status = Status(jsonStore, onSet=Status.OnSet(self._notifyClients, name))
        endpoint = API._Endpoint(name, path, status, isWritable)
        return status

_service = None

class ServiceError(OSError): pass

class Service(service.Service):

    # TODO serverStatus: client address, bytes received

    def __init__(self, log, root='/flash/www', routeList=(), api=API('/api'), port=80):
        self._api = api
        for r in routeList:
            RegisterRoute(
                lambda mws2, request: r.callback(request, r.data),
                r.method, r.path, r.name,
            )
        self._mws2 = MicroWebSrv2()
        self._mws2.BindAddress = ('0.0.0.0', port)
        self._mws2.SetEmbeddedConfig()
        self._mws2.RootPath = root
        def onLogging(_, message, type):
            if type == MicroWebSrv2.DEBUG:
                log.debug(message)
            elif type == MicroWebSrv2.INFO:
                log.info(message)
            elif type == MicroWebSrv2.WARNING:
                log.warning(message)
            elif type == MicroWebSrv2.ERROR:
                log.error(message)
            else:
                log.critical(message)
        self._mws2.OnLogging = onLogging
        HOME = '/'
        if not self._mws2.ResolvePhysicalPath(HOME):
            raise MicroWebSrv2Exception(
                "RootPath '%s' does not resolve with URL '%s'"
                % (self._mws2.RootPath, HOME)
            )
        self._mws2.NotFoundURL = HOME
        self._mws2.StartManaged()

    @property
    def microwebsrv2(self): return self._mws2
    
    async def onLoop(self, stopCallback):
        if not self._mws2.IsRunning: stopCallback()
        await asyncio.sleep(1)

    async def onStop(self):
        self._api._close()
        self._mws2.Stop()
