Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Refactor] Snapshot controllers #141

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .github/workflows/irc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on: [push]

jobs:
notification:
if: github.repository == 'moddevices/mod-ui'
runs-on: ubuntu-latest
name: IRC notification
steps:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ jobs:
pip3 install pylint
- name: Run pylint
run: |
export MOD_DEV_HOST=1
export MOD_DEV_ENVIRONMENT=0

virtualenv modui-env
source modui-env/bin/activate
pylint -E mod/*.py
Expand Down
58 changes: 58 additions & 0 deletions .github/workflows/unittest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: unittest

on: [push, pull_request]

jobs:
unittest:
runs-on: ubuntu-22.04
name: unittest
steps:
- uses: actions/checkout@v3
- name: Install virtualenv
run: |
sudo apt install python3-virtualenv
- name: Setup utils
run: |
sudo apt install python3-dev build-essential libasound2-dev libjack-jackd2-dev liblilv-dev libjpeg-dev zlib1g-dev
make -C utils
- name: Setup unittest
run: |
virtualenv modui-env
source modui-env/bin/activate
pip3 install -r requirements.txt
# pytest
pip3 install pytest pytest-cov
sed -i -e 's/collections.MutableMapping/collections.abc.MutableMapping/' modui-env/lib/python3.10/site-packages/tornado/httputil.py
# mod
python3 setup.py develop
- name: Run unittest
run: |
virtualenv modui-env
source modui-env/bin/activate
pytest --cov=mod --cov-report=html --cov-report=term --cov-report=xml
- name: Archive coverage files
uses: actions/upload-artifact@v3
with:
name: coverage
path: |
.coverage
htmlcov/**
cobertura.xml
- name: Code Coverage Report
uses: irongut/[email protected]
with:
filename: coverage.xml
badge: true
fail_below_min: true
format: markdown
hide_branch_rate: false
hide_complexity: true
indicators: true
output: both
thresholds: '15 30'
# - name: Add Coverage PR Comment
# uses: marocchino/sticky-pull-request-comment@v2
# if: github.event_name == 'pull_request'
# with:
# recreate: true
# path: code-coverage-results.md
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ package-lock.json
*.tar.xz
mod-env/
.env/
host/
/host/
*.crf
*.axf
*.hex
Expand Down Expand Up @@ -37,3 +37,7 @@ utils/test
*node_modules/
.idea
*.egg-info
# Coverage
.coverage
coverage.xml
htmlcov/
30 changes: 30 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,33 @@ And now you are ready to start the webserver::

Setting the environment variables is needed when developing on a PC.
Open your browser and point to http://localhost:8888/.

Development
-----------

The source code is expected to be ran on a Python 3.4 version.

The source code is organized with the following structure

* ``html``: Frontend source code
* ``mod``: Backend source code
* ``controller/``: Tornado related source code
* ``rest/``: REST classes
* ``websocket/``: web-socket classes
* ``dto/``: Utility model representation
* ``handler/``: Common tornado handlers
* ``file_receiver/``: Common tornado file receivers
* ``model/``: Model abstraction (snapshot, pedalboard, bank)
* ``service/``: High level layer for ``mod-host`` usage
* ``util/``: Common utility source code
* ``settings.py``: Application parameters
* ``host.py``: ``mod-host`` interface for communication with application
* ``webserver.py``: Web-server initialization
* ``modtools``:
* ``test``: Python unit tests
* ``utils``: C++ utility code


Some IDEs can improve the code completion if you install mod-ui as an local packaged on virtualenv::

$ python3 setup.py develop
3 changes: 3 additions & 0 deletions mod/controller/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG
# SPDX-License-Identifier: AGPL-3.0-or-later
3 changes: 3 additions & 0 deletions mod/controller/handler/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG
# SPDX-License-Identifier: AGPL-3.0-or-later
41 changes: 41 additions & 0 deletions mod/controller/handler/json_request_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG
# SPDX-License-Identifier: AGPL-3.0-or-later
import json

from tornado.util import unicode_type

from mod.controller.handler.timeless_request_handler import TimelessRequestHandler


class JsonRequestHandler(TimelessRequestHandler):
def write(self, data):
# FIXME: something is sending strings out, need to investigate what later..
# it's likely something using write(json.dumps(...))
# we want to prevent that as it causes issues under Mac OS

if isinstance(data, (bytes, unicode_type, dict)):
TimelessRequestHandler.write(self, data)
self.finish()
return

elif data is True:
data = "true"
self.set_header("Content-Type", "application/json; charset=UTF-8")

elif data is False:
data = "false"
self.set_header("Content-Type", "application/json; charset=UTF-8")

# TESTING for data types, remove this later
#elif not isinstance(data, list):
#print("=== TESTING: Got new data type for RequestHandler.write():", type(data), "msg:", data)
#data = json.dumps(data)
#self.set_header('Content-type', 'application/json')

else:
data = json.dumps(data)
self.set_header("Content-Type", "application/json; charset=UTF-8")

TimelessRequestHandler.write(self, data)
self.finish()
15 changes: 15 additions & 0 deletions mod/controller/handler/timeless_request_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG
# SPDX-License-Identifier: AGPL-3.0-or-later
from tornado import web


class TimelessRequestHandler(web.RequestHandler):
def compute_etag(self):
return None

def set_default_headers(self):
self._headers.pop("Date")

def should_return_304(self):
return False
3 changes: 3 additions & 0 deletions mod/controller/rest/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG
# SPDX-License-Identifier: AGPL-3.0-or-later
170 changes: 170 additions & 0 deletions mod/controller/rest/snapshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG
# SPDX-License-Identifier: AGPL-3.0-or-later
from tornado import gen, web

from mod.controller.handler.json_request_handler import JsonRequestHandler
from mod.session import SESSION
from mod.settings import DEFAULT_SNAPSHOT_NAME


class SnapshotName(JsonRequestHandler):

# TODO: Replace GET /snapshot/name
# to GET /pedalboards/<pedalboard id>/snapshots/
def get(self):
"""
Remove the snapshot name of identifier ``id`` of the loaded pedalboard;
If is requested by an invalid ``id``, will be returned the default snapshot name.

.. code-block:: json

{
"ok": true,
"name": "Pedalboard name"
}

:return: Snapshot name
"""
idx = int(self.get_argument('id'))
name = SESSION.host.snapshot_name(idx) or DEFAULT_SNAPSHOT_NAME
self.write({
'ok': bool(name), # FIXME: Always true
'name': name
})


class SnapshotList(JsonRequestHandler):

# TODO: Replace GET /snapshot/list
# to GET /pedalboards/<pedalboard id>/snapshots/
def get(self):
"""
Get snapshots name of the loaded pedalboard

.. code-block:: json

{
0: "First snapshot",
1: "Second snapshot"
}

:return: names of the current pedalboard snapshots
"""
snapshots = SESSION.host.pedalboard_snapshots
snapshots = dict((i, snapshots[i]['name']) for i in range(len(snapshots)) if snapshots[i] is not None)
self.write(snapshots)


class SnapshotSave(JsonRequestHandler):

# TODO: Replace POST /snapshot/save
# to POST /pedalboards/current/snapshots/current
def post(self):
"""
Update the current snapshot status

:return: `true` if it was successfully updated
"""
ok = SESSION.host.snapshot_save()
self.write(ok)


class SnapshotSaveAs(JsonRequestHandler):

# TODO: Replace GET /snapshot/saveas
# to POST /pedalboards/current/snapshots/saveas
@web.asynchronous
@gen.engine
def get(self):
"""
Create a new snapshot with the suggested ``title`` based on the current pedalboard status;
.. code-block:: json

{
"ok": true,
"id": 1,
"title": "Snapshot name"
}

:return: `true` if it was successfully deleted
"""
title = self.get_argument('title') # TODO rename to name (standardization)
idx = SESSION.host.snapshot_saveas(title)
title = SESSION.host.snapshot_name(idx)

yield gen.Task(SESSION.host.hmi_report_ss_name_if_current, idx)

self.write({
'ok': idx is not None, # FIXME: Always true
'id': idx,
'title': title, # TODO rename to name (standardization)
})


class SnapshotRemove(JsonRequestHandler):

# TODO: Replace GET /snapshot/remove?id=<snapshot id>
# to DELETE /pedalboards/<pedalboard id>/snapshots/<snapshot id>
def get(self):
"""
Remove the snapshot of identifier ``id`` of the loaded pedalboard

:return: `true` if it was successfully deleted
"""
idx = int(self.get_argument('id'))
ok = SESSION.host.snapshot_remove(idx)

self.write(ok)


class SnapshotRename(JsonRequestHandler):

# TODO: Replace GET /snapshot/rename?id=<snapshot id>&?title=<snapshot name>
# to PATCH /pedalboards/<pedalboard id>/snapshots/<snapshot id>
@web.asynchronous
@gen.engine
def get(self):
"""
Rename the snapshot of ``ìd`` identifier with the suggested ``title``.

.. code-block:: json

{
"ok": true,
"title": "Snapshot name"
}

:return: new snapshot name
"""
idx = int(self.get_argument('id'))
title = self.get_argument('title') # TODO rename to name (standardization)
ok = SESSION.host.snapshot_rename(idx, title)

if ok:
title = SESSION.host.snapshot_name(idx)

yield gen.Task(SESSION.host.hmi_report_ss_name_if_current, idx)

self.write({
'ok': ok,
'title': title, # TODO rename to name (standardization)
})


class SnapshotLoad(JsonRequestHandler):

# TODO: Replace GET /snapshot/load?id=<snapshot id>
# to POST /pedalboards/current/snapshots/<snapshot id>/load
@web.asynchronous
@gen.engine
def get(self):
"""
Loads the snapshot of ``ìd`` identifier.

:return: Was snapshot loaded?
"""
idx = int(self.get_argument('id'))
abort_catcher = SESSION.host.abort_previous_loading_progress("web SnapshotLoad")
ok = yield gen.Task(SESSION.host.snapshot_load_gen_helper, idx, False, abort_catcher)
self.write(ok)
8 changes: 8 additions & 0 deletions mod/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -3003,8 +3003,15 @@ def _snapshot_unique_name(self, name):
return get_unique_name(name, names) or name

def snapshot_make(self, name):
"""
Create a snapshot based on current pedalboard

:param name: snapshot name
:return: snapshot created
"""
self.pedalboard_modified = True

# TODO Create Snapshot model class
snapshot = {
"name": name,
"data": {},
Expand Down Expand Up @@ -3081,6 +3088,7 @@ def snapshot_remove(self, idx):
def snapshot_load_gen_helper(self, idx, from_hmi, abort_catcher, callback):
self.snapshot_load(idx, from_hmi, abort_catcher, callback)

# FIXME: Rename callback argument (https://stackoverflow.com/a/57121126)
@gen.coroutine
def snapshot_load(self, idx, from_hmi, abort_catcher, callback):
if idx in (self.HMI_SNAPSHOTS_1, self.HMI_SNAPSHOTS_2, self.HMI_SNAPSHOTS_3):
Expand Down
Loading