-
Notifications
You must be signed in to change notification settings - Fork 82
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
Changes from 19 commits
81c8954
9f58166
295b080
9345250
f532dd2
df34fe1
53a6e9c
05c138e
b51de77
50148b2
ce6cc8e
1760dc8
7a93a1c
b178bec
9754809
5203c63
31ec8a5
2bae589
48d46cd
bc74d2b
15ed66a
a48cd5a
fff09f9
2bc36a5
5e0f743
cfd9bbb
0fbfe8c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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/**' | ||
- '.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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would leave that for a separate PR There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. like 👍🏼 |
||
|
||
- name: Install MySQL odbc driver | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 other translations don't rely on odbc, is it a must? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've experimented with this alternative: There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
*/.pytest_cache | ||
venv | ||
**/__pycache__ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
Flask | ||
pytest | ||
requests | ||
pyodbc |
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' |
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) |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. did you use a coverage / mutation tool? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😎