Skip to content

Commit

Permalink
Merge pull request #894 from Shared-Reality-Lab/monarch-link-app
Browse files Browse the repository at this point in the history
Include the Monarch link app as a service on IMAGE-server
  • Loading branch information
JRegimbal authored Oct 23, 2024
2 parents f58f1d8 + b8a81c4 commit d1bfea8
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 1 deletion.
78 changes: 78 additions & 0 deletions .github/workflows/monarch-link-app-service.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Monarch Link App Service
on:
push:
branches: [ main ]
tags: [ "service-monarch-link-app-[0-9]+.[0-9]+.[0-9]+" ]
paths: [ "services/monarch-link-app/**" ]
pull_request:
branches: [ main ]
paths: [ "services/monarch-link-app/**" ]
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: shared-reality-lab/image-service-monarch-link-app
jobs:
lint:
name: PEP 8 style check.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install flake8
run: pip install flake8
- name: Check with flake8
run: python -m flake8 ./services/monarch-link-app --show-source
build-and-push-image:
name: Build and Push to Registry
needs: lint
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
submodules: true
- name: Log into GHCR
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Correct Tags
run: |
if [[ ${{ github.ref }} =~ ^refs/tags/service-monarch-link-app-[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "TAGGED=true" >> $GITHUB_ENV
else
echo "TAGGED=false" >> $GITHUB_ENV
fi
- name: Get timestamp
run: echo "timestamp=$(date -u +'%Y-%m-%dT%H.%M')" >> $GITHUB_ENV
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=${{ env.TAGGED }}
tags: |
type=match,enable=${{ env.TAGGED }},priority=300,pattern=service-monarch-link-app-(\d+.\d+.\d+),group=1
type=raw,priority=200,value=unstable
type=raw,priority=100,value=${{ env.timestamp }}
labels: |
org.opencontainers.image.title=IMAGE Service Monarch Link App
org.opencontainers.image.description=Service to link Monarch client with tactile authoring tool.
org.opencontainers.image.authors=IMAGE Project <[email protected]>
org.opencontainers.image.licenses=AGPL-3.0-or-later
maintainer=IMAGE Project <[email protected]>
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
file: ./services/monarch-link-app/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
5 changes: 5 additions & 0 deletions build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ services:
context: .
dockerfile: services/espnet-tts-fr/Dockerfile
image: "espnet-tts-fr:latest"
monarch-link-app-service:
build:
context: .
dockerfile: services/monarch-link-app/Dockerfile
image: "monarch-link-app:latest"
line-charts-preprocessor:
build:
context: .
Expand Down
8 changes: 7 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,13 @@ services:
image: ghcr.io/shared-reality-lab/image-handler-svg-action-recognition:${REGISTRY_TAG}
labels:
ca.mcgill.a11y.image.handler: enable


monarch-link-app:
profiles: [test, default]
image: "ghcr.io/shared-reality-lab/image-service-monarch-link-app:${REGISTRY_TAG}"
restart: unless-stopped
networks:
- traefik
# end - unicorn exclusive services
volumes:
sc-store:
Expand Down
19 changes: 19 additions & 0 deletions services/monarch-link-app/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM python:3.13

RUN apt-get install libcairo2

RUN adduser --disabled-password python
WORKDIR /usr/src/app
ENV PATH="/home/python/.local/bin:${PATH}"

RUN pip install --upgrade pip
COPY /services/monarch-link-app/requirements.txt /usr/src/app/requirements.txt
RUN pip install -r requirements.txt

COPY /services/monarch-link-app/ /usr/src/app

RUN chown -R python:python /usr/src/app

EXPOSE 80
USER python
CMD ["gunicorn", "app:app", "-b", "0.0.0.0:80", "--capture-output", "--log-level=debug" ]
16 changes: 16 additions & 0 deletions services/monarch-link-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
This is the app used to connect IMAGE-TactileAuthoring to IMAGE-Monarch.

# Monarch Link App

![license: AGPL](https://img.shields.io/badge/license-AGPL-success) [GitHub Container Registry Package](https://github.com/Shared-Reality-Lab/IMAGE-server/pkgs/container/image-service-monarch-link-app)

## Overview

This is the containerized version of a web app used to publish content from [IMAGE-TactileAuthoring](https://github.com/Shared-Reality-Lab/IMAGE-TactileAuthoring) which can then be fetched by [IMAGE-Monarch client](https://github.com/Shared-Reality-Lab/IMAGE-Monarch). It stores the data in a json file data.json which is recreated each time the container is restarted.

This container runs on port 80.

## Endpoints
- POST `https://monarch.unicorn.cim.mcgill.ca/create/<subscribed_code>` where the body is a JSON object with the key `data` set to the SVG in base64 format, `secret` set to the secret key, and `layer` set to the layer to be shown or None if no default layer is selected.
This can be used to create a new channel `<subscribed_code>` if one doesn't already exist. The data is stored in data.json. If `<subscribed_code>` already exists the `data` field corresponding it is changed only if the `secret` field matches the secret key when the channel was created.
- GET `https://monarch.unicorn.cim.mcgill.ca/display/<subscribed_code>` returns a JSON object with the [tactile svg schema](https://github.com/Shared-Reality-Lab/IMAGE-server/blob/24a41b4f36a8c89b1a94d7c31388703ece8c81c7/renderers/tactilesvg.schema.json).
187 changes: 187 additions & 0 deletions services/monarch-link-app/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# Copyright (c) 2024 IMAGE Project, Shared Reality Lab, McGill University
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# and our Additional Terms along with this program.
# If not, see
# <https://github.com/Shared-Reality-Lab/IMAGE-server/blob/main/LICENSE>.

from flask import Flask, request, abort, Response
from flask_bcrypt import Bcrypt
from flask_cors import CORS, cross_origin
import logging
import hashlib
import json
import random
import re
import uuid
from werkzeug.routing import BaseConverter, ValidationError

app = Flask(__name__)
bcrypt = Bcrypt(app)
logging.basicConfig(level=logging.DEBUG)


CORS(
app, resources={r"/*": {"origins": "*"}}
) # CORS allowed for all domains on all routes

try:
with open("data.json", 'x') as file:
json.dump(dict(), file)
except FileExistsError:
logging.debug("The file already exists")


def write_data(svgData):
with open("data.json", "w") as outfile:
json.dump(svgData, outfile)


def read_data():
with open('data.json', 'r') as openfile:
try:
return json.load(openfile)
except Exception:
# return a dict if the file is empty
return dict()


# generate an id that does not already exist
def generate_code(svgData):
code = ''.join([str(random.randint(1, 8)) for i in range(6)])
while code in svgData:
code = generate_code(svgData)
return code


# custom converter to validate that the id
# in the url is a valid id
class CodeConverter(BaseConverter):
def to_python(self, value):
# has six digits between 1 and 8
pattern = re.compile("^[1-8]{6}$")
# also has a length of six
if not pattern.match(value):
logging.debug('Received request with invalid ID value')
raise ValidationError('Invalid id value')
return value

def to_url(self, value):
return value


app.url_map.converters['code'] = CodeConverter


@app.route("/create", methods=["POST"])
@cross_origin()
def create():
if request.method == "POST":
logging.debug('Create request received')
try:
req_data = request.get_json()
svgData = read_data()
id = generate_code(svgData)
secret = uuid.uuid4().hex
svgData[id] = {"secret": bcrypt.generate_password_hash(secret)
.decode('utf-8'),
"data": req_data["data"],
"title": req_data["title"],
"layer": req_data["layer"]}
write_data(svgData)
logging.debug('Created new channel with code '+id)
return {"id": id, "secret": secret}
except KeyError:
logging.debug("Unexpected JSON format. Returning 400")
return "Unexpected JSON format", 400
except Exception as e:
logging.debug(e)
abort(Response(response=e))


@app.route("/update/<code:id>", methods=["POST"])
@cross_origin()
def update(id):
if request.method == "POST":
try:
logging.debug('Update request received')
req_data = request.get_json()
svgData = read_data()
if id in svgData:
if bcrypt.check_password_hash((svgData[id])["secret"],
req_data["secret"]):
svgData[id] = {"secret":
bcrypt.generate_password_hash(
req_data["secret"]).decode('utf-8'),
"data": req_data["data"],
"title": req_data["title"],
"layer": req_data["layer"]}
write_data(svgData)
logging.debug('Updated graphic')
return "Graphic in channel "+id+" has been updated!"
else:
logging.debug('Unauthorized access to existing channel!')
return "Unauthorized access to existing channel!", 401
else:
svgData[id] = {"secret":
bcrypt.generate_password_hash(
req_data["secret"]).decode('utf-8'),
"data": req_data["data"],
"title": req_data["title"],
"layer": req_data["layer"]}
write_data(svgData)
logging.debug('TEMP: Created new channel using update!')
return ("New channel created with code "+id +
". Creating new ids using update is" +
" only intended for testing!")
except KeyError:
logging.debug("Unexpected JSON format. Returning 400")
return "Unexpected JSON format", 400
except Exception as e:
logging.debug(e)
abort(Response(response=e))


@app.route("/display/<code:id>", methods=["GET"])
@cross_origin()
def display(id):
if request.method == "GET":
logging.debug('Display request received')
svgData = read_data()
if id in svgData:
try:
response = Response()
response.mimetype = "application/json"
response.set_data(json.dumps({"renderings": [
{"data": {"graphic": svgData[id]["data"],
"layer": svgData[id]["layer"]}}]}))
response.add_etag(hashlib.md5(
(svgData[id]["data"]+svgData[id]["layer"]).encode()))
response.make_conditional(request)
logging.debug('Sending tactile response')
return response
except Exception as e:
logging.debug(e)
abort(Response(response=e))
else:
logging.debug('ID does not exist')
return abort(404)


@app.route("/", methods=["POST", "GET"])
@cross_origin()
def home():
return "Hi"


if __name__ == "__main__":
app.run(host="0.0.0.0", port=80, debug=True)
12 changes: 12 additions & 0 deletions services/monarch-link-app/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
bcrypt>=4.2.0,<5.0.0
blinker>=1.8.2,<2.0.0
click>=8.1.7,<9.0.0
colorama>=0.4.6,<1.0.0
Flask==3.0.3
Flask-Bcrypt==1.0.1
Flask-Cors==5.0.0
itsdangerous>=2.2.0,<3.0.0
Jinja2>=3.1.4,<4.0.0
MarkupSafe>=3.0.1,<4.0.0
Werkzeug>=3.0.4,<4.0.0
gunicorn
9 changes: 9 additions & 0 deletions test-docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ services:
device_ids: ['0']
capabilities: ["gpu", "utility", "compute"]

monarch-link-app:
labels:
- "traefik.enable=true"
- "traefik.http.routers.monarch-link-app.rule=Host(`monarch.unicorn.cim.mcgill.ca`)"
- "traefik.http.routers.monarch-link-app.tls.certresolver=myresolver"
- traefik.docker.network=traefik
environment:
- SERVER_NAME=unicorn.cim.mcgill.ca

object-grouping:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
Expand Down

0 comments on commit d1bfea8

Please sign in to comment.