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

Python translation #61

Merged
merged 27 commits into from
Mar 12, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
81c8954
Basic flask application runs and has a unit test
emilybache Mar 2, 2023
9f58166
add gitignore
emilybache Mar 2, 2023
295b080
additional tests
emilybache Mar 2, 2023
9345250
it connects to the db now too
emilybache Mar 2, 2023
f532dd2
tests in place, translation begun
emilybache Mar 3, 2023
df34fe1
tests passing
emilybache Mar 3, 2023
53a6e9c
put is also working
emilybache Mar 3, 2023
05c138e
use same password as everyone else
emilybache Mar 3, 2023
b51de77
first attempt at github workflow
emilybache Mar 3, 2023
50148b2
attempt 2
emilybache Mar 3, 2023
ce6cc8e
attempt 3
emilybache Mar 3, 2023
1760dc8
attempt 4
emilybache Mar 3, 2023
7a93a1c
attempt 5
emilybache Mar 3, 2023
b178bec
attempt 6
emilybache Mar 3, 2023
9754809
attempt 7
emilybache Mar 3, 2023
5203c63
attempt 8
emilybache Mar 3, 2023
31ec8a5
attempt 9
emilybache Mar 3, 2023
2bae589
attempt 10
emilybache Mar 3, 2023
48d46cd
attempt 11
emilybache Mar 3, 2023
bc74d2b
first draft of documentation
emilybache Mar 9, 2023
15ed66a
make db connection flexible enough to also use pymysql
emilybache Mar 9, 2023
a48cd5a
make db connection use sqlite3 as a backup option
emilybache Mar 9, 2023
fff09f9
Open and Close database connection properly on each request
emilybache Mar 9, 2023
2bc36a5
improve the docs
emilybache Mar 9, 2023
5e0f743
First attempt to get it to work on a system where mysql isn't installed
emilybache Mar 9, 2023
cfd9bbb
improve docs
emilybache Mar 9, 2023
0fbfe8c
wait for the server to start rather than sleeping a fixed time
emilybache Mar 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/python-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: python-build

env:
PROJECT_DIR: python

on:
push:
paths:
- 'python/**'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😎

- '.github/workflows/python-build.yml'
pull_request:
paths:
- 'python/**'
- '.github/workflows/python-build.yml'

jobs:
build:
defaults:
run:
working-directory: ./${{ env.PROJECT_DIR }}

runs-on: ubuntu-22.04

env:
DB_USER: root
DB_OLD_PASSWORD: root
DB_PASSWORD: mysql

steps:
- name: Checkout Repository
uses: actions/checkout@v2

- name: Start MYSQL and import DB

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is just copied from the typescript version. Should probably update all the versions together?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would leave that for a separate PR

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run: |
sudo systemctl start mysql
mysqladmin --user=${{ env.DB_USER }} --password=${{ env.DB_OLD_PASSWORD }} version
mysqladmin --user=${{ env.DB_USER }} --password=${{ env.DB_OLD_PASSWORD }} password ${{ env.DB_PASSWORD }}
mysql -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} < ${GITHUB_WORKSPACE}/database/initDatabase.sql

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like 👍🏼


- name: Install MySQL odbc driver

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 other translations don't rely on odbc, is it a must?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've experimented with this alternative:
emilybache#1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used odbc because it makes it easier to switch to a different database. It's probably not necessary here though as you point out

run: |
wget https://repo.mysql.com/apt/ubuntu/pool/mysql-8.0/m/mysql-community/mysql-community-client-plugins_8.0.32-1ubuntu22.04_amd64.deb
sudo dpkg -i mysql-community-client-plugins_8.0.32-1ubuntu22.04_amd64.deb
wget https://dev.mysql.com/get/Downloads/Connector-ODBC/8.0/mysql-connector-odbc_8.0.32-1ubuntu22.04_amd64.deb
sudo dpkg -i mysql-connector-odbc_8.0.32-1ubuntu22.04_amd64.deb

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'

- name: Set up dependencies
run: pip install -r requirements.txt

- name: Test
run: PYTHONPATH=src python -m pytest

3 changes: 3 additions & 0 deletions python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*/.pytest_cache
venv
**/__pycache__
4 changes: 4 additions & 0 deletions python/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Flask
pytest
requests
pyodbc
28 changes: 28 additions & 0 deletions python/src/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import pyodbc


def create_lift_pass_db_connection(connection_options):
driver = get_mariadb_driver()
connection_string = make_connection_string_template(driver) % (
connection_options["host"],
connection_options["user"],
connection_options["database"],
connection_options["password"],
)
return pyodbc.connect(connection_string)


def get_mariadb_driver():
drivers = []
for driver in pyodbc.drivers():
if driver.startswith("MySQL") or driver.startswith("MariaDB"):
drivers.append(driver)

if drivers:
return max(drivers)
else:
raise RuntimeError("No suitable drivers found for MySQL, is it installed?")


def make_connection_string_template(driver):
return 'DRIVER={' + driver + '};SERVER=%s;USER=%s;OPTION=3;DATABASE=%s;PASSWORD=%s'
74 changes: 74 additions & 0 deletions python/src/prices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import math

from flask import Flask
from flask import request
from datetime import datetime
from db import create_lift_pass_db_connection

app = Flask("lift-pass-pricing")

connection_options = {"host": 'localhost', "user": 'root', "database": 'lift_pass', "password": 'mysql'}
connection = create_lift_pass_db_connection(connection_options)


@app.route("/prices", methods=['GET', 'PUT'])
def prices():
res = {}
if request.method == 'PUT':
lift_pass_cost = request.args["cost"]
lift_pass_type = request.args["type"]
cursor = connection.cursor()
cursor.execute('INSERT INTO `base_price` (type, cost) VALUES (?, ?) ' +
'ON DUPLICATE KEY UPDATE cost = ?', (lift_pass_type, lift_pass_cost, lift_pass_cost))
return {}
elif request.method == 'GET':
cursor = connection.cursor()
cursor.execute(f'SELECT cost FROM base_price '
+ 'WHERE type = ? ', (request.args['type'],))
row = cursor.fetchone()
result = {"cost": row[0]}
if 'age' in request.args and request.args.get('age', type=int) < 6:
res["cost"] = 0
else:
if "type" in request.args and request.args["type"] != "night":
cursor = connection.cursor()
cursor.execute('SELECT * FROM holidays')
is_holiday = False
reduction = 0
for row in cursor.fetchall():
holiday = row[0]
if "date" in request.args:
d = datetime.fromisoformat(request.args["date"])
if d.year == holiday.year and d.month == holiday.month and holiday.day == d.day:
is_holiday = True
if not is_holiday and "date" in request.args and datetime.fromisoformat(request.args["date"]).weekday() == 0:
reduction = 35

# TODO: apply reduction for others
if 'age' in request.args and request.args.get('age', type=int) < 15:
res['cost'] = math.ceil(result["cost"]*.7)
else:
if 'age' not in request.args:
cost = result['cost'] * (1 - reduction/100)
res['cost'] = math.ceil(cost)
else:
if 'age' in request.args and request.args.get('age', type=int) > 64:
cost = result['cost'] * .75 * (1 - reduction / 100)
res['cost'] = math.ceil(cost)
elif 'age' in request.args:
cost = result['cost'] * (1 - reduction / 100)
res['cost'] = math.ceil(cost)
else:
if 'age' in request.args and request.args.get('age', type=int) >= 6:
if request.args.get('age', type=int) > 64:
res['cost'] = math.ceil(result['cost'] * .4)
else:
res.update(result)
else:
res['cost'] = 0

return res


if __name__ == "__main__":
app.run(port=3005)
79 changes: 79 additions & 0 deletions python/test/test_prices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import multiprocessing

import pytest
import requests
from datetime import datetime

from prices import app

TEST_PORT = 3006


def server(port):
app.run(port=port)


@pytest.fixture(autouse=True, scope="session")
def lift_pass_pricing_app():
""" starts the lift pass pricing flask app running on localhost """
p = multiprocessing.Process(target=server, args=(TEST_PORT,))
p.start()
yield f"http://127.0.0.1:{TEST_PORT}"
p.terminate()


def test_put_1jour_price(lift_pass_pricing_app):
response = requests.put(lift_pass_pricing_app + '/prices', params={'type': '1jour', 'cost': 35})
assert response.status_code == 200


def test_put_night_price(lift_pass_pricing_app):
response = requests.put(lift_pass_pricing_app + '/prices', params={'type': 'night', 'cost': 19})
assert response.status_code == 200


def test_default_cost(lift_pass_pricing_app):
response = requests.get(lift_pass_pricing_app + "/prices", params={'type': '1jour'})
assert response.json() == {'cost': 35}


@pytest.mark.parametrize(
"age,expectedCost", [
(5, 0),
(6, 25),
(14, 25),
(15, 35),
(25, 35),
(64, 35),
(65, 27),
])
def test_works_for_all_ages(lift_pass_pricing_app, age, expectedCost):
response = requests.get(lift_pass_pricing_app + "/prices", params={'type': '1jour', 'age': age})
assert response.json() == {'cost': expectedCost}


@pytest.mark.parametrize(
"age,expectedCost", [
(5, 0),
(6, 19),
(25, 19),
(64, 19),
(65, 8),
])
def test_works_for_night_passes(lift_pass_pricing_app, age, expectedCost):
response = requests.get(lift_pass_pricing_app + "/prices", params={'type': 'night', 'age': age})
assert response.json() == {'cost': expectedCost}


@pytest.mark.parametrize(
"age,expectedCost,ski_date", [
(15, 35, datetime.fromisoformat('2019-02-22')),
(15, 35, datetime.fromisoformat('2019-02-25')), # monday, holiday
(15, 23, datetime.fromisoformat('2019-03-11')), # monday
(65, 18, datetime.fromisoformat('2019-03-11')), # monday

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you use a coverage / mutation tool?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no I just translated the typescript tests.

])
def test_works_for_monday_deals(lift_pass_pricing_app, age, expectedCost, ski_date):
response = requests.get(lift_pass_pricing_app + "/prices", params={'type': '1jour', 'age': age, 'date': ski_date})
assert response.json() == {'cost': expectedCost}

# TODO 2-4, and 5, 6 day pass