Skip to content

Commit

Permalink
parse JSON response to update DA state only if needed (#324)
Browse files Browse the repository at this point in the history
* parse JSON response to update DA state only if needed
- update test to cover updating state via output
- add lazy parsing of json response

* add comment and simplify LazyJson

* move import of typing outside try:except: block related to dataclasses

Co-authored-by: GFJ138 <sebastien.dementen@engie.com>
  • Loading branch information
sdementen and sebastiendementen committed Mar 28, 2021
1 parent 5ef2d7c commit 240f62c
Show file tree
Hide file tree
Showing 4 changed files with 326 additions and 291 deletions.
28 changes: 18 additions & 10 deletions django_plotly_dash/dash_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,32 +24,28 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
'''
import inspect
import itertools
import json
import inspect
import warnings
from typing import Dict, List, Callable

import dash
from dash import Dash, dependencies
from dash._utils import split_callback_id, inputs_to_dict

from flask import Flask

from django.urls import reverse
from django.utils.text import slugify

from flask import Flask
from plotly.utils import PlotlyJSONEncoder

from .app_name import app_name, main_view_label
from .middleware import EmbeddedHolder

from .util import static_asset_path
from .util import serve_locally as serve_locally_setting
from .util import stateless_app_lookup_hook
from .util import static_asset_path

try:
from dataclasses import dataclass
from typing import Dict, List

@dataclass(frozen=True)
class CallbackContext:
Expand Down Expand Up @@ -201,7 +197,6 @@ def __init__(self, name=None, serve_locally=None,

if self._serve_locally:
# Ensure package is loaded; if not present then pip install dpd-static-support
import dpd_static_support
hard_coded_package_name = "dpd_static_support"
base_file_name = bootstrap_source.split('/')[-1]

Expand Down Expand Up @@ -705,7 +700,20 @@ def dispatch_with_args(self, body, argMap):
res = callback(*args, **argMap)

if da:
root_value = json.loads(res).get('response', {})
class LazyJson:
"""A class to allow delayed the evaluation of a dict (returned by `func`)
till the first get(...) is called on the dict."""

def __init__(self, func):
self._root_value = func

def get(self, item, default):
if isinstance(self._root_value, Callable):
self._root_value = self._root_value()
return self._root_value.get(item, default)

# wraps the json parsing of the response into LazyJson to avoid unnecessary parsing
root_value = LazyJson(lambda: json.loads(res).get('response', {}))

for output_item in outputs:
if isinstance(output_item, str):
Expand Down
98 changes: 53 additions & 45 deletions django_plotly_dash/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,33 +79,37 @@ def test_dash_stateful_app_client_contract(client):

# set the initial expected state
expected_state = {'inp1': {'n_clicks': 0, 'n_clicks_timestamp': 1611733453854},
'inp2': {'n_clicks': 5, 'n_clicks_timestamp': 1611733454354},
'inp1b': {'n_clicks': 5, 'n_clicks_timestamp': 1611733454354},
'inp2': {'n_clicks': 7, 'n_clicks_timestamp': 1611733454554},
'out1-0': {'n_clicks': 1, 'n_clicks_timestamp': 1611733453954},
'out1-1': {'n_clicks': 2, 'n_clicks_timestamp': 1611733454054},
'out1-2': {'n_clicks': 3, 'n_clicks_timestamp': 1611733454154},
'out1-3': {'n_clicks': 4, 'n_clicks_timestamp': 1611733454254},
'out2-0': {'n_clicks': 6, 'n_clicks_timestamp': 1611733454454},
'out3': {'n_clicks': 10, 'n_clicks_timestamp': 1611733454854},
'out4': {'n_clicks': 14, 'n_clicks_timestamp': 1611733455254},
'out5': {'n_clicks': 18, 'n_clicks_timestamp': 1611733455654},
'{"_id":"inp-0","_type":"btn3"}': {'n_clicks': 7,
'n_clicks_timestamp': 1611733454554},
'{"_id":"inp-0","_type":"btn4"}': {'n_clicks': 11,
'n_clicks_timestamp': 1611733454954},
'{"_id":"inp-0","_type":"btn5"}': {'n_clicks': 15,
'n_clicks_timestamp': 1611733455354},
'{"_id":"inp-1","_type":"btn3"}': {'n_clicks': 8,
'n_clicks_timestamp': 1611733454654},
'{"_id":"inp-1","_type":"btn4"}': {'n_clicks': 12,
'n_clicks_timestamp': 1611733455054},
'{"_id":"inp-1","_type":"btn5"}': {'n_clicks': 16,
'n_clicks_timestamp': 1611733455454},
'{"_id":"inp-2","_type":"btn3"}': {'n_clicks': 9,
'out1b': {'href': 'http://www.example.com/null',
'n_clicks': 6,
'n_clicks_timestamp': 1611733454454},
'out2-0': {'n_clicks': 8, 'n_clicks_timestamp': 1611733454654},
'out3': {'n_clicks': 12, 'n_clicks_timestamp': 1611733455054},
'out4': {'n_clicks': 16, 'n_clicks_timestamp': 1611733455454},
'out5': {'n_clicks': 20, 'n_clicks_timestamp': 1611733455854},
'{"_id":"inp-0","_type":"btn3"}': {'n_clicks': 9,
'n_clicks_timestamp': 1611733454754},
'{"_id":"inp-2","_type":"btn4"}': {'n_clicks': 13,
'{"_id":"inp-0","_type":"btn4"}': {'n_clicks': 13,
'n_clicks_timestamp': 1611733455154},
'{"_id":"inp-2","_type":"btn5"}': {'n_clicks': 17,
'n_clicks_timestamp': 1611733455554}}
'{"_id":"inp-0","_type":"btn5"}': {'n_clicks': 17,
'n_clicks_timestamp': 1611733455554},
'{"_id":"inp-1","_type":"btn3"}': {'n_clicks': 10,
'n_clicks_timestamp': 1611733454854},
'{"_id":"inp-1","_type":"btn4"}': {'n_clicks': 14,
'n_clicks_timestamp': 1611733455254},
'{"_id":"inp-1","_type":"btn5"}': {'n_clicks': 18,
'n_clicks_timestamp': 1611733455654},
'{"_id":"inp-2","_type":"btn3"}': {'n_clicks': 11,
'n_clicks_timestamp': 1611733454954},
'{"_id":"inp-2","_type":"btn4"}': {'n_clicks': 15,
'n_clicks_timestamp': 1611733455354},
'{"_id":"inp-2","_type":"btn5"}': {'n_clicks': 19,
'n_clicks_timestamp': 1611733455754}}

########## test state management of the app and conversion of components ids
# search for state values in dash layout
Expand All @@ -122,7 +126,7 @@ def test_dash_stateful_app_client_contract(client):

# update an existent state => update current_state
stateful_a.update_current_state('{"_id":"inp-2","_type":"btn5"}', "n_clicks", 100)
expected_state['{"_id":"inp-2","_type":"btn5"}'] = {'n_clicks': 100, 'n_clicks_timestamp': 1611733455554}
expected_state['{"_id":"inp-2","_type":"btn5"}'] = {'n_clicks': 100, 'n_clicks_timestamp': 1611733455754}
assert stateful_a.current_state() == expected_state

assert DashApp.objects.get(instance_name="Some name").current_state() == {}
Expand Down Expand Up @@ -170,34 +174,38 @@ def test_dash_stateful_app_client_contract(client):
stateful_a.handle_current_state()

# check final state has been changed accordingly
final_state = {'inp1': {'n_clicks': 1, 'n_clicks_timestamp': 1611736145932},
'inp2': {'n_clicks': 6, 'n_clicks_timestamp': 1611736146875},
final_state = {'inp1': {'n_clicks': 1, 'n_clicks_timestamp': 1615103027288},
'inp1b': {'n_clicks': 5, 'n_clicks_timestamp': 1615103033482},
'inp2': {'n_clicks': 8, 'n_clicks_timestamp': 1615103036591},
'out1-0': {'n_clicks': 1, 'n_clicks_timestamp': 1611733453954},
'out1-1': {'n_clicks': 2, 'n_clicks_timestamp': 1611733454054},
'out1-2': {'n_clicks': 3, 'n_clicks_timestamp': 1611733454154},
'out1-3': {'n_clicks': 4, 'n_clicks_timestamp': 1611733454254},
'out2-0': {'n_clicks': 6, 'n_clicks_timestamp': 1611733454454},
'out3': {'n_clicks': 10, 'n_clicks_timestamp': 1611733454854},
'out4': {'n_clicks': 14, 'n_clicks_timestamp': 1611733455254},
'out5': {'n_clicks': 18, 'n_clicks_timestamp': 1611733455654},
'{"_id":"inp-0","_type":"btn3"}': {'n_clicks': 8,
'n_clicks_timestamp': 1611736147644},
'{"_id":"inp-0","_type":"btn4"}': {'n_clicks': 12,
'n_clicks_timestamp': 1611733454954},
'{"_id":"inp-0","_type":"btn5"}': {'n_clicks': 16,
'n_clicks_timestamp': 1611733455354},
'{"_id":"inp-1","_type":"btn3"}': {'n_clicks': 9,
'n_clicks_timestamp': 1611736148172},
'{"_id":"inp-1","_type":"btn4"}': {'n_clicks': 13,
'n_clicks_timestamp': 1611733455054},
'{"_id":"inp-1","_type":"btn5"}': {'n_clicks': 18,
'n_clicks_timestamp': 1611733455454},
'{"_id":"inp-2","_type":"btn3"}': {'n_clicks': 10,
'n_clicks_timestamp': 1611736149140},
'{"_id":"inp-2","_type":"btn4"}': {'n_clicks': 13,
'out1b': {'href': 'http://www.example.com/1615103033482',
'n_clicks': 6,
'n_clicks_timestamp': 1611733454454},
'out2-0': {'n_clicks': 8, 'n_clicks_timestamp': 1611733454654},
'out3': {'n_clicks': 12, 'n_clicks_timestamp': 1611733455054},
'out4': {'n_clicks': 16, 'n_clicks_timestamp': 1611733455454},
'out5': {'n_clicks': 20, 'n_clicks_timestamp': 1611733455854},
'{"_id":"inp-0","_type":"btn3"}': {'n_clicks': 10,
'n_clicks_timestamp': 1615103039030},
'{"_id":"inp-0","_type":"btn4"}': {'n_clicks': 14,
'n_clicks_timestamp': 1611733455154},
'{"_id":"inp-2","_type":"btn5"}': {'n_clicks': 19,
'n_clicks_timestamp': 1611733455554}}
'{"_id":"inp-0","_type":"btn5"}': {'n_clicks': 18,
'n_clicks_timestamp': 1611733455554},
'{"_id":"inp-1","_type":"btn3"}': {'n_clicks': 11,
'n_clicks_timestamp': 1615103039496},
'{"_id":"inp-1","_type":"btn4"}': {'n_clicks': 15,
'n_clicks_timestamp': 1611733455254},
'{"_id":"inp-1","_type":"btn5"}': {'n_clicks': 19,
'n_clicks_timestamp': 1611733455654},
'{"_id":"inp-2","_type":"btn3"}': {'n_clicks': 12,
'n_clicks_timestamp': 1615103040528},
'{"_id":"inp-2","_type":"btn4"}': {'n_clicks': 15,
'n_clicks_timestamp': 1611733455354},
'{"_id":"inp-2","_type":"btn5"}': {'n_clicks': 20,
'n_clicks_timestamp': 1611733455754}}

assert DashApp.objects.get(instance_name="Some name").current_state() == final_state

Expand Down

0 comments on commit 240f62c

Please sign in to comment.