feat: Make it a web application #10

Open
typosaurus wants to merge 6 commits from typosaurus/9-make-it-a-web-application into main
6 changed files with 237 additions and 2 deletions

View File

@ -1,2 +1,5 @@
verify:
@python3 -m compileall -q src tests && python3 -m unittest discover -s tests -q && echo "verification passed"
@python3.13 -m compileall -q src tests app && python3.13 -m unittest discover -s tests -q && echo "verification passed"
run:
FLASK_APP=app flask run

View File

@ -1,3 +1,11 @@
# typosaurus-sandbox
Sandbox for Typosaurus end-to-end verification
Sandbox for Typosaurus end-to-end verification
## Running the web app
```bash
make run
```
The development server starts at http://127.0.0.1:5000.

67
app/__init__.py Normal file
View File

@ -0,0 +1,67 @@
# retoor <retoor@molodetz.nl>
import os
from flask import Flask
from flask import jsonify
from flask import make_response
from flask import request
from flask.wrappers import Response
from src.calculator import add
from src.calculator import clamp
from src.calculator import clamp_to_byte
from src.calculator import subtract
app = Flask(__name__)
@app.route('/')
def index() -> Response:
index_path = os.path.join(os.path.dirname(__file__), 'index.html')
with open(index_path) as f:
return make_response(f.read(), 200, {'Content-Type': 'text/html'})
@app.route('/add', methods=['GET'])
def add_route() -> Response:
try:
left = int(request.args['left'])
right = int(request.args['right'])
except (KeyError, TypeError, ValueError):
return make_response(jsonify({'error': 'Invalid or missing parameters'}), 400)
return make_response(jsonify({'result': add(left, right)}), 200)
@app.route('/subtract', methods=['GET'])
def subtract_route() -> Response:
try:
left = int(request.args['left'])
right = int(request.args['right'])
except (KeyError, TypeError, ValueError):
return make_response(jsonify({'error': 'Invalid or missing parameters'}), 400)
return make_response(jsonify({'result': subtract(left, right)}), 200)
@app.route('/clamp', methods=['GET'])
def clamp_route() -> Response:
try:
value = int(request.args['value'])
low = int(request.args['low'])
high = int(request.args['high'])
except (KeyError, TypeError, ValueError):
return make_response(jsonify({'error': 'Invalid or missing parameters'}), 400)
try:
result = clamp(value, low, high)
except ValueError:
return make_response(jsonify({'error': 'Invalid or missing parameters'}), 400)
return make_response(jsonify({'result': result}), 200)
@app.route('/clamp_to_byte', methods=['GET'])
def clamp_to_byte_route() -> Response:
try:
value = int(request.args['value'])
except (KeyError, TypeError, ValueError):
return make_response(jsonify({'error': 'Invalid or missing parameters'}), 400)
return make_response(jsonify({'result': clamp_to_byte(value)}), 200)

69
app/index.html Normal file
View File

@ -0,0 +1,69 @@
<!-- retoor <retoor@molodetz.nl> -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calculator</title>
</head>
<body>
<h1>Calculator</h1>
<fieldset>
<legend>Add / Subtract</legend>
<input type="number" id="left" placeholder="Left operand">
<input type="number" id="right" placeholder="Right operand">
<button onclick="calculate('add')">Add</button>
<button onclick="calculate('subtract')">Subtract</button>
</fieldset>
<fieldset>
<legend>Clamp</legend>
<input type="number" id="value" placeholder="Value">
<input type="number" id="low" placeholder="Low">
<input type="number" id="high" placeholder="High">
<button onclick="calculate('clamp')">Clamp</button>
</fieldset>
<fieldset>
<legend>Clamp to Byte</legend>
<input type="number" id="byte_value" placeholder="Value">
<button onclick="calculate('clamp_to_byte')">Clamp to Byte</button>
</fieldset>
<p id="output"></p>
<script>
function calculate(operation) {
const resultEl = document.getElementById('output');
let url;
if (operation === 'add' || operation === 'subtract') {
const left = document.getElementById('left').value;
const right = document.getElementById('right').value;
url = '/' + operation + '?left=' + encodeURIComponent(left) + '&right=' + encodeURIComponent(right);
} else if (operation === 'clamp') {
const value = document.getElementById('value').value;
const low = document.getElementById('low').value;
const high = document.getElementById('high').value;
url = '/clamp?value=' + encodeURIComponent(value) + '&low=' + encodeURIComponent(low) + '&high=' + encodeURIComponent(high);
} else if (operation === 'clamp_to_byte') {
const value = document.getElementById('byte_value').value;
url = '/clamp_to_byte?value=' + encodeURIComponent(value);
}
fetch(url)
.then(function(response) {
return response.json().then(function(data) {
if (!response.ok) {
resultEl.textContent = 'Error: ' + (data.error || 'Unknown error');
} else {
resultEl.textContent = 'Result: ' + data.result;
}
});
})
.catch(function() {
resultEl.textContent = 'Error: Network error';
});
}
</script>
</body>
</html>

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
Flask>=3.0,<4.0

87
tests/test_api.py Normal file
View File

@ -0,0 +1,87 @@
# retoor <retoor@molodetz.nl>
import json
import unittest
from app import app
class TestCalculatorAPI(unittest.TestCase):
def setUp(self) -> None:
self.client = app.test_client()
def test_add_success(self) -> None:
response = self.client.get('/add?left=3&right=5')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, {'result': 8})
def test_add_missing_param_returns_400(self) -> None:
response = self.client.get('/add?left=3')
self.assertEqual(response.status_code, 400)
self.assertIn('error', response.json)
def test_add_invalid_type_returns_400(self) -> None:
response = self.client.get('/add?left=abc&right=5')
self.assertEqual(response.status_code, 400)
self.assertIn('error', response.json)
def test_subtract_success(self) -> None:
response = self.client.get('/subtract?left=10&right=3')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, {'result': 7})
def test_subtract_missing_param_returns_400(self) -> None:
response = self.client.get('/subtract?right=3')
self.assertEqual(response.status_code, 400)
self.assertIn('error', response.json)
def test_subtract_invalid_type_returns_400(self) -> None:
response = self.client.get('/subtract?left=10&right=xyz')
self.assertEqual(response.status_code, 400)
self.assertIn('error', response.json)
def test_clamp_success(self) -> None:
response = self.client.get('/clamp?value=5&low=0&high=10')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, {'result': 5})
def test_clamp_value_below_low_returns_low(self) -> None:
response = self.client.get('/clamp?value=-5&low=0&high=10')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, {'result': 0})
def test_clamp_missing_param_returns_400(self) -> None:
response = self.client.get('/clamp?value=5&low=0')
self.assertEqual(response.status_code, 400)
self.assertIn('error', response.json)
def test_clamp_invalid_type_returns_400(self) -> None:
response = self.client.get('/clamp?value=abc&low=0&high=10')
self.assertEqual(response.status_code, 400)
self.assertIn('error', response.json)
def test_clamp_low_greater_than_high_returns_400(self) -> None:
response = self.client.get('/clamp?value=5&low=10&high=0')
self.assertEqual(response.status_code, 400)
self.assertIn('error', response.json)
def test_clamp_to_byte_success(self) -> None:
response = self.client.get('/clamp_to_byte?value=100')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, {'result': 100})
def test_clamp_to_byte_above_255_returns_255(self) -> None:
response = self.client.get('/clamp_to_byte?value=300')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, {'result': 255})
def test_clamp_to_byte_missing_param_returns_400(self) -> None:
response = self.client.get('/clamp_to_byte')
self.assertEqual(response.status_code, 400)
self.assertIn('error', response.json)
def test_clamp_to_byte_invalid_type_returns_400(self) -> None:
response = self.client.get('/clamp_to_byte?value=abc')
self.assertEqual(response.status_code, 400)
self.assertIn('error', response.json)