Compare commits

...

10 Commits

Author SHA1 Message Date
bba801f2d2
add balance entity
Add respecting schema, model and adapt all endpoints to utilize it.
Add two new endpoints for increasing and checking the balance.
2024-12-28 18:50:46 +02:00
32fd20b3ae migrate user, category and record API to db 2024-12-28 15:10:29 +02:00
b96ba3eb34 add automated testing script for user table 2024-12-28 12:50:32 +02:00
c2104fd9cc finish user API migration to DB 2024-12-28 12:49:54 +02:00
7ef08e64c2 migrate post user and get users api 2024-12-27 23:23:21 +02:00
9f5c2505de add environment files to .gitignore 2024-12-27 21:52:03 +02:00
9fe9a112e7 add database and marshmallow entities to app 2024-12-27 21:51:19 +02:00
a5c5c1850d add database container to docker-compose.yaml 2024-12-27 21:49:24 +02:00
2fcb8aa276 bump dependencies for application 2024-12-27 21:44:09 +02:00
cfe3fa5fc7
add variant for further development 2024-12-23 19:21:30 +02:00
7 changed files with 390 additions and 70 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
__pycache__/
*.env

View File

@ -13,3 +13,7 @@ docker-compose up
```
That should be enough to get the server up and running.
## Lab3 specifics
Variant for functionality extension: 27 % 3 = 0 => Income accountance

View File

@ -1,10 +1,75 @@
from flask import Flask, request
from flask import Flask, request, jsonify
import time
import json
from app.local_db import LocalDB
import uuid
from marshmallow import Schema, fields
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
ldb = LocalDB()
app.config.from_pyfile('config.py', silent=True)
db = SQLAlchemy(app)
class UserModel(db.Model):
__tablename__ = "user"
uuid = db.Column(db.String(32), unique=True, primary_key=True, nullable=False)
name = db.Column(db.String(64), nullable=False)
bal_uuid = db.Column(db.String(32), db.ForeignKey('balance.uuid'))
class CategoryModel(db.Model):
__tablename__ = "category"
uuid = db.Column(db.String(32), unique=True, primary_key=True, nullable=False)
name = db.Column(db.String(64), nullable=False)
class RecordModel(db.Model):
__tablename__ = "record"
uuid = db.Column(db.String(32), primary_key=True, nullable=False)
user_uuid = db.Column(db.String(32), db.ForeignKey('user.uuid'))
cat_uuid = db.Column(db.String(32), db.ForeignKey('category.uuid'))
date = db.Column(db.Date)
amount = db.Column(db.Integer)
class BalanceModel(db.Model):
__tablename__ = "balance"
uuid = db.Column(db.String(32), primary_key=True, nullable=False)
value = db.Column(db.Integer, nullable=False)
class UserSchema(Schema):
uuid = fields.Str()
name = fields.Str()
bal_uuid = fields.Str()
class CategorySchema(Schema):
uuid = fields.Str()
name = fields.Str()
class RecordSchema(Schema):
uuid = fields.Str()
user_uuid = fields.Str()
cat_uuid = fields.Str()
date = fields.Date()
amount = fields.Integer()
class BalanceSchema(Schema):
uuid = fields.Str()
value = fields.Integer()
user_schema = UserSchema()
users_schema = UserSchema(many = True)
category_schema = CategorySchema()
categories_schema = CategorySchema(many = True)
record_schema = RecordSchema()
records_schema = RecordSchema(many = True)
balance_schema = BalanceSchema()
# "migration"
with app.app_context():
db.create_all()
@app.route("/healthcheck")
def ep_healthcheck():
@ -15,56 +80,89 @@ def ep_healthcheck():
@app.route("/reset_users_because_postman_is_dumb_like_that")
def ep_reset():
ldb.reset()
return {}
db.session.query(RecordModel).delete()
db.session.query(CategoryModel).delete()
db.session.query(UserModel).delete()
db.session.query(BalanceModel).delete()
db.session.commit()
return {}, 200
@app.route("/users", methods = ["GET"])
def ep_users_get():
return ldb.get_users()
result = db.session.query(UserModel).all()
return users_schema.dumps(result)
@app.route("/user/<user_id>", methods = ["GET"])
def ep_user_get(user_id):
user = ldb.get_user(user_id)
result = db.session.query(UserModel).filter(UserModel.uuid == user_id).all()
if 'uuid' in user:
return user
if len(result) == 1:
return user_schema.dumps(result[0]), 200
else:
return user, 404
return {}, 404
@app.route("/user", methods = ["POST"])
def ep_user_post():
body = request.json
if 'name' in body:
r = ldb.add_user(body['name'])
if 'uuid' in r:
return r
else:
return r, 403
else:
if not body:
return {}, 403
if 'uuid' in body:
return {}, 403
b = BalanceModel(uuid=uuid.uuid4().hex, value=0)
body.update({'uuid': uuid.uuid4().hex})
body.update({'bal_uuid': b.uuid})
try:
_ = user_schema.load(body)
except ValidationError as e:
return {}, 403
u = UserModel(**body)
try:
db.session.add(b)
db.session.add(u)
db.session.commit()
except Exception as e:
db.session.rollback()
return {}, 403
return jsonify(user_schema.load(body)), 200
@app.route("/user/<user_id>", methods = ["DELETE"])
def ep_user_delete(user_id):
r = ldb.del_user(user_id)
try:
result = db.session.query(UserModel).filter(UserModel.uuid == user_id).all()
except Exception as e:
return {}, 403
if 'uuid' in r:
return r
else:
return r, 403
if len(result) == 0:
return {}, 404
try:
db.session.query(UserModel).filter(UserModel.uuid == user_id).delete()
db.session.query(BalanceModel).filter(BalanceModel.uuid == result[0].bal_uuid).delete()
db.session.commit()
except Exception as e:
db.session.rollback()
return {}, 403
return user_schema.dumps(result[0]), 200
@app.route("/category", methods = ["GET"])
def ep_category_get():
body = request.json
if 'uuid' in body:
category = ldb.get_category(body['uuid'])
result = db.session.query(CategoryModel).filter(CategoryModel.uuid == body['uuid']).all()
if 'uuid' in category:
return category
if len(result) == 1:
return user_schema.dumps(result[0]), 200
else:
return category, 404
return {}, 404
else:
return {}, 403
@ -72,81 +170,210 @@ def ep_category_get():
def ep_category_post():
body = request.json
if 'name' in body:
r = ldb.add_category(body['name'])
if 'uuid' in r:
return r
else:
return r, 403
else:
if not body:
return {}, 403
if 'uuid' in body:
return {}, 403
body.update({'uuid': uuid.uuid4().hex})
try:
_ = category_schema.load(body)
except ValidationError as e:
return {}, 403
c = CategoryModel(**body)
try:
db.session.add(c)
db.session.commit()
except Exception as e:
db.session.rollback()
return {}, 403
return jsonify(category_schema.load(body)), 200
@app.route("/category", methods = ["DELETE"])
def ep_category_delete():
body = request.json
if 'uuid' in body:
category = ldb.del_category(body['uuid'])
if 'uuid' in category:
return category
else:
return category, 404
else:
if 'uuid' not in body:
return {}, 403
cat_id = body['uuid']
try:
result = db.session.query(CategoryModel).filter(CategoryModel.uuid == cat_id).all()
except Exception as e:
return {}, 403
if len(result) == 0:
return {}, 404
try:
db.session.query(CategoryModel).filter(CategoryModel.uuid == cat_id).delete()
db.session.commit()
except Exception as e:
return {}, 403
return category_schema.dumps(result[0]), 200
@app.route("/record/<record_id>", methods = ["GET"])
def ep_record_get(record_id):
r = ldb.get_record(record_id)
result = db.session.query(RecordModel).filter(RecordModel.uuid == record_id).all()
if 'uuid' in r:
return r
if len(result) == 1:
return user_schema.dumps(result[0]), 200
else:
return r, 404
return {}, 404
@app.route("/record", methods = ["GET"])
def ep_record_get_filtered():
options = {}
r = db.session.query(RecordModel)
filtered = False
if 'user_id' in request.json:
options['user_id'] = request.json['user_id']
r = r.filter(RecordModel.user_uuid == request.json['user_id'])
filtered = True
elif 'user_uuid' in request.json:
r = r.filter(RecordModel.user_uuid == request.json['user_uuid'])
filtered = True
if 'cat_id' in request.json:
options['cat_id'] = request.json['cat_id']
r = r.filter(RecordModel.cat_uuid == request.json['cat_id'])
filtered = True
if 'cat_uuid' in request.json:
r = r.filter(RecordModel.cat_uuid == request.json['cat_uuid'])
filtered = True
if len(list(options.keys())) == 0:
return [], 400
r = ldb.filter_records(options)
return json.dumps(r)
if filtered:
return records_schema.dumps(r.all())
else:
return [], 403
@app.route("/record/<record_id>", methods = ["DELETE"])
def ep_record_del(record_id):
r = ldb.del_record(record_id)
try:
result = db.session.query(RecordModel).filter(RecordModel.uuid == record_id).all()
except Exception as e:
return {}, 403
if 'uuid' in r:
return r
else:
return r, 404
if len(result) == 0:
return {}, 404
db.session.query(RecordModel).filter(RecordModel.uuid == record_id).delete()
db.session.commit()
return record_schema.dumps(result[0]), 200
@app.route("/record", methods = ["POST"])
def ep_record_post():
body = request.json
if 'user_id' not in body:
return {}, 400
if not body:
return {}, 403
if 'cat_id' not in body:
return {}, 400
if 'uuid' in body:
return {}, 403
if 'amount' not in body:
return {}, 400
body.update({'uuid': uuid.uuid4().hex})
r = ldb.add_record(body['user_id'], body['cat_id'], body['amount'])
# backward compatibility with lab2 DB model
if 'cat_id' in body:
body.update({'cat_uuid': body['cat_id']})
del body['cat_id']
if 'uuid' in r:
return r
if 'user_id' in body:
body.update({'user_uuid': body['user_id']})
del body['user_id']
try:
_ = record_schema.load(body)
except Exception as e:
return {}, 403
r = RecordModel(**body)
b_id = db.session \
.query(UserModel) \
.filter(UserModel.uuid == body['user_uuid']) \
.all()[0] \
.bal_uuid
v = db.session \
.query(BalanceModel) \
.filter(BalanceModel.uuid == b_id) \
.all()[0] \
.value
BalanceModel.metadata.tables.get("balance").update().where(BalanceModel.metadata.tables.get("balance").c.uuid == b_id).values(value = v-body['amount'])
try:
db.session.add(r)
db.session.commit()
except Exception as e:
db.session.rollback()
return {}, 403
return jsonify(record_schema.load(body)), 200
@app.route("/balance_up", methods = ["POST"])
def ep_balance_up():
body = request.json
if 'user_id' in body:
body.update({'user_uuid': body['user_id']})
del body['user_id']
if 'user_uuid' not in body:
return {}, 403
try:
b_id = db.session \
.query(UserModel) \
.filter(UserModel.uuid == body['user_uuid']) \
.all()[0] \
.bal_uuid
v = db.session \
.query(BalanceModel) \
.filter(BalanceModel.uuid == b_id) \
.all()[0] \
.value
BalanceModel.metadata.tables.get("balance").update().where(BalanceModel.metadata.tables.get("balance").c.uuid == b_id).values(value = v + body['amount'])
except Exception as e:
return {}, 403
return {}, 200
@app.route("/balance", methods = ["GET"])
def ep_balance_get():
body = request.json
if 'user_id' in body:
body.update({'user_uuid': body['user_id']})
del body['user_id']
if 'user_uuid' not in body:
return {}, 403
try:
b_id = db.session \
.query(UserModel) \
.filter(UserModel.uuid == body['user_uuid']) \
.all()[0] \
.bal_uuid
result = db.session.query(BalanceModel).filter(BalanceModel.uuid == b_id).all()
except Exception as e:
return {}, 403
if len(result) == 1:
return user_schema.dumps(result[0]), 200
else:
return r, 403
return {}, 404

7
app/config.py Normal file
View File

@ -0,0 +1,7 @@
import os
PROPAGATE_EXCEPTIONS = True
SQLALCHEMY_DATABASE_URI = f'postgresql://{os.environ["POSTGRES_USER"]}:{os.environ["POSTGRES_PASSWORD"]}@db:5432/accountance'
SQLALCHEMY_TRACK_MODIFICATIONS = False
API_TITLE = "Finance REST API"
API_VERSION = 'v1'

View File

@ -12,3 +12,13 @@ services:
- "12402:12402"
volumes:
- ./app:/app/app
env_file:
- db.env
depends_on:
- db
db:
restart: unless-stopped
image: postgres:17.2-alpine3.21
env_file:
- db.env

View File

@ -1,7 +1,20 @@
alembic==1.14.0
apispec==6.8.0
blinker==1.8.2
click==8.1.7
Flask==3.0.3
Flask-Migrate==4.0.7
flask-smorest==0.45.0
Flask-SQLAlchemy==3.1.1
greenlet==3.1.1
itsdangerous==2.2.0
Jinja2==3.1.4
Mako==1.3.8
MarkupSafe==2.1.5
marshmallow==3.23.2
packaging==24.2
psycopg2-binary==2.9.10
SQLAlchemy==2.0.36
typing_extensions==4.12.2
webargs==8.6.0
Werkzeug==3.0.4

58
tests/lab3/user_test.sh Executable file
View File

@ -0,0 +1,58 @@
#!/bin/sh
HOST=$1
if [ -z HOST ]; then
echo "Host not specified, exiting"
exit 1;
fi;
echo -n 'Resetting DB...'
curl -X GET -f -s \
http://$HOST:12402/reset_users_because_postman_is_dumb_like_that > /dev/null
if [ $? -ne 0 ]; then
echo 'Exiting due to previous error'
exit 1
fi
echo ' Done.'
echo -n 'Creating users...'
for i in $(seq 9); do
curl -X POST -f -s \
--data "{\"name\": \"hi$i\"}" \
--header "Content-Type: application/json" \
http://$HOST:12402/user > /dev/null;
if [ $? -ne 0 ]; then
echo 'Exiting due to previous error'
exit 1
fi
done
echo ' Done.'
echo -n 'Getting user UUID to delete...'
DELETION_UUID=$(curl -X GET -f -s \
http://$HOST:12402/users \
| jq .[0].uuid);
if [ $? -ne 0 ]; then
echo 'Exiting due to previous error'
exit 1
fi
DELETION_UUID=${DELETION_UUID#\"}
DELETION_UUID=${DELETION_UUID%\"}
echo " $DELETION_UUID."
echo -n "Deleting user $DELETION_UUID..."
curl -X DELETE -f -s \
http://127.0.0.1:12402/user/$DELETION_UUID > /dev/null
if [ $? -ne 0 ]; then
echo 'Exiting due to previous error'
exit 1
fi
echo ' Done.'