diff --git a/docs/cells.md b/docs/cells.md new file mode 100644 index 000000000..2def0e57e --- /dev/null +++ b/docs/cells.md @@ -0,0 +1,116 @@ +### Cells ### + +Cells represents an abstraction of the web, tightly coupled with object database, which allows you to create "reactive" interface in python.... + + +### Preamble ### + +In order to get ourselves up and running we'll need a few things: + +* Object Database installed (see the [installation docs](../INSTALLATION.md) for more details) +* an object_database engine service instance (see [here](./object_engine.md) for more information) +* build the bundle for the services landing page + * `cd object_database/object_database/web/content` + * create a node environment and source it + * run `npm install && npm run build` +* a configured and installed web service instance, which will be responsible for building and serving the web application +* and an installed instance of the service we'd like to run + +After this we can start up our web server and service. + +Lets walk through it (as in the other examples we'll use "TOKEN" for our special token): + +In a python virtual environment boot up Object Database engine like so: +``` + object_database_service_manager \ + localhost \ + localhost \ + 8000 \ + Master \ + --run_db \ + --service-token TOKEN \ + --source ./odb/source \ + --storage ./odb/storage +``` + +In another python virtual environment instance run the following: +``` +# export our special Object Database token +export ODB_AUTH_TOKEN=TOKEN + +# install the ActiveWebService +object_database_service_config install \ +--class object_database.web.ActiveWebService.ActiveWebService \ +--placement Master + +# configure ActiveWebService +object_database_service_config configure ActiveWebService \ +--port 8080 --hostname localhost --internal-port 8081 + +# check to make sure it is listed +object_database_service_config list + +# start it up +object_database_service_config start ActiveWebService + +# check to see that it is running +object_database_service_config instances +``` + +NOTE: you can always open [http://localhost:8080/](http://localhost:8080/) in your browser to see the running services and click to see what they are. Also, if you get tired of running the above commands, there is a small bash script found [here](./examples/aws_start.sh). + +#### Running a boring web app #### + +Run the following in your virtual environment: + +``` +object_database_service_config install --class object_database.service_manager.ServiceBase.ServiceBase --placement Master +# check to see it is installed +object_database_service_config list +# start +object_database_service_config start ServiceBase +# check to see it is running +object_database_service_config instances +``` + +(NOTE: you might need to change the paths to the [ServiceBase](https://github.com/APrioriInvestments/object_database/blob/dev/object_database/service_manager/ServiceBase.py) file depending on the directory you are running this from.) + +If you head to [http://localhost:8080/services/ServiceBase](http://localhost:8080/services/ServiceBase) you will see our really boring service. The simple text holder card is what [ServiceBase.serviceDisplay](https://github.com/APrioriInvestments/object_database/blob/dev/object_database/service_manager/ServiceBase.py#L67) returns. In the next example, we'll subclass `ServiceBase` and change this method to do something more interesting. + + +#### Running a more interesting web app #### + +Take a look at [cells.py]('./examples/cells.py'). You'll see we made a subclass of `ServiceBase` and overrode its `.serviceDisplay` method. There is card with a header and some buttons which all route you to the corresponding URI (in our case the list of services we have running), plus a little bit of styling. + +Lets install our more interesting app like above: +``` +object_database_service_config install --class docs.examples.cells.SomethingMoreInteresting --placement Master +object_database_service_config start SomethingMoreInteresting +``` + +Note: when you make changes to `SomethingMoreInteresting` you need to reinstall it, with the above command. If you see a message like `Cannot set codebase of locked service 'SomethingMoreInteresting'` then click the "Locked" icon in [http://localhost:8080/services]([http://localhost:8080/services) to unlock it, then reinstall. + + +You can learn more about cells by perusing the [cells](https://github.com/APrioriInvestments/object_database/tree/dev/object_database/web/cells) directory or taking a look at the [cells test example](https://github.com/APrioriInvestments/object_database/blob/dev/object_database/web/CellsTestService.py#L63) + +#### Running an ODB web app #### + +But before we wrap up, we should really build a cells examples which works with Object Database, since that's the main point here. + +I am going to skip over details of how ODB works. Please here [here](https://github.com/APrioriInvestments/object_database/blob/dev/docs/object_engine.md) for an introduction. + +We'll be building an `AnODBService` which you can find [here](https://github.com/APrioriInvestments/object_database/blob/daniel-examples/docs/examples/cells_odb.py). You'll see we need to define +* a [schema](https://github.com/APrioriInvestments/object_database/blob/daniel-examples/docs/examples/cells_odb.py#L12) +* how our app will [interact with ODB](https://github.com/APrioriInvestments/object_database/blob/daniel-examples/docs/examples/cells_odb.py#L23) +* and the [UI](https://github.com/APrioriInvestments/object_database/blob/daniel-examples/docs/examples/cells_odb.py#L40) for the app itself. + +The app will send and recieve messages from the database, and update the UI which consists largely of a Panel and Table cell. The key departure here from the previous examples is the lambda functions passed to the cells. Instead of returning something like a string (which then tells the service to route to the correponding URI), these interact with the DB via the `Message` class. + +As before we'll need to install and start the service: +Lets install our more interesting app like above: +``` +object_database_service_config install --class docs.examples.cells_db.AnODBService --placement Master +object_database_service_config start AnODBService +``` + +We'll learn more about cells and how to develop them in the upcoming `cells_dev.md` doc. diff --git a/docs/examples/__init__.py b/docs/examples/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/examples/aws_start.sh b/docs/examples/aws_start.sh new file mode 100644 index 000000000..e17134a7f --- /dev/null +++ b/docs/examples/aws_start.sh @@ -0,0 +1,22 @@ +## Active Web Service startup script + +# export our special Object Database token +export ODB_AUTH_TOKEN=TOKEN + +# install the ActiveWebService +object_database_service_config install \ +--class object_database.web.ActiveWebService.ActiveWebService \ +--placement Master + +# configure ActiveWebService +object_database_service_config configure ActiveWebService \ +--port 8080 --hostname localhost --internal-port 8081 + +# check to make sure it is listed +object_database_service_config list + +# start it up +object_database_service_config start ActiveWebService + +# check to see that it is running +object_database_service_config instances diff --git a/docs/examples/cells.py b/docs/examples/cells.py new file mode 100644 index 000000000..3cb393d8f --- /dev/null +++ b/docs/examples/cells.py @@ -0,0 +1,20 @@ +import object_database.web.cells as cells +from object_database import ServiceBase + + +class SomethingMoreInteresting(ServiceBase): + def initialize(self): + self.buttonName = "click me" + return + + @staticmethod + def serviceDisplay(serviceObject, instance=None, objType=None, queryArgs=None): + return cells.Card( + cells.Panel( + cells.Button("Reload", lambda: "") + + cells.Button("Service Base", lambda: "ServiceBase") + + cells.Button("Active Web Service", lambda: "ActiveWebService") + ), + header="This is a 'card' cell with some buttons", + padding="10px" + ) diff --git a/docs/examples/cells_odb.py b/docs/examples/cells_odb.py new file mode 100644 index 000000000..8512d90a6 --- /dev/null +++ b/docs/examples/cells_odb.py @@ -0,0 +1,81 @@ +from object_database import Schema, ServiceBase +import object_database.web.cells as cells +import time +import random + +# define a 'schema', which is a collection of classes we can subscribe to as a group +schema = Schema("cells_obd") + +# define a type of entry in odb. We'll have one instance of this class for each +# message in the database +@schema.define +class Message: + timestamp = float + message = str + lifetime = float + + +class AnODBService(ServiceBase): + def initialize(self): + # make sure we're subscribed to all objects in our schema. + self.db.subscribeToSchema(schema) + + def doWork(self, shouldStop): + # this is the main entrypoint for the service - it gets to do work here. + while not shouldStop.is_set(): + #wake up every 100ms and look at the objects in the ODB. + time.sleep(.1) + + # delete any messages more than 10 seconds old + with self.db.transaction(): + # get all the messages + messages = Message.lookupAll() + + for m in messages: + if m.timestamp < time.time() - m.lifetime: + # this will actually delete the object from the ODB. + m.delete() + + @staticmethod + def serviceDisplay(serviceObject, instance=None, objType=None, queryArgs=None): + # make sure cells has loaded these classes in the database and subscribed + # to all the objects. + cells.ensureSubscribedSchema(schema) + + def newMessage(): + # calling the constructor creates a new message object. Even though we + # orphan it immediately, we can always get it back by calling + # Message.lookupAll() + # because ODB objects have an explicit lifetime (they have to be destroyed) + Message(timestamp=time.time(), message=editBox.currentText.get(), lifetime=20) + + # reset our edit box so we can type again + editBox.currentText.set("") + + # define an 'edit box' cell. The user can type into this. + editBox = cells.SingleLineTextBox(onEnter=lambda newText: newMessage()) + + return cells.Panel( + editBox >> cells.Button( + "New Message", + newMessage + ) + ) + ( + cells.Table( + colFun=lambda: ['timestamp', 'lifetime', 'message'], + rowFun=lambda: sorted(Message.lookupAll(), key=lambda m: -m.timestamp), + headerFun=lambda x: x, + rendererFun=lambda m, col: cells.Subscribed( + lambda: + cells.Timestamp(m.timestamp) if col == 'timestamp' else + m.message if col == 'message' else + cells.Dropdown( + m.lifetime, + [1, 2, 5, 10, 20, 60, 300], + lambda val: setattr(m, 'lifetime', val) + ) + ), + maxRowsPerPage=100, + fillHeight=True + ) + ) diff --git a/docs/examples/consumer.py b/docs/examples/consumer.py index a0d1a6a43..66888c7d1 100644 --- a/docs/examples/consumer.py +++ b/docs/examples/consumer.py @@ -9,15 +9,14 @@ class Message: timestamp = float message = str - db = connect('localhost', 8000, 'TOKEN') + db.subscribeToSchema(schema) ts = 0 while True: - with db.view(): - Message(timestamp=time.time(), - message='message_5') + with db.transaction(): + Message(timestamp=time.time(), message="message_5") messages = Message.lookupAll() for m in messages: if m.timestamp > ts: diff --git a/quickstart/Dockerfile b/quickstart/Dockerfile index 5116d5ba5..46e0d3bd8 100644 --- a/quickstart/Dockerfile +++ b/quickstart/Dockerfile @@ -1,17 +1,20 @@ # Using 3.8 because typed_python doesn't seem to # compile with 3.9 (as of 2022-04-27) -FROM python:3.8-slim as builder +FROM python:3.9-slim as builder -ADD . /opt/object_database +ADD ./object_database /opt/object_database +ADD ./typed_python /opt/typed_python RUN apt update -y -qq \ && apt upgrade -y -qq \ + && apt-get update \ && apt install -y -qq curl libssl-dev build-essential git npm \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* RUN pip install --upgrade pip \ - && pip install typed_python \ + && cd /opt/typed_python \ + && pip install -e . \ && cd /opt/object_database \ && pip install -e . @@ -19,4 +22,4 @@ RUN cd /opt/object_database/object_database/web/content \ && npm install \ && npm run build -CMD ["object_database_webtest"] \ No newline at end of file +CMD ["object_database_webtest"]