diff --git a/apps/app.py b/apps/app.py index 8e8fdca..48ae460 100644 --- a/apps/app.py +++ b/apps/app.py @@ -1,42 +1,31 @@ ''' -Simple RestAPI server with APIFlask, postgresql (psycopg2) +Simple RestAPI server with FastAPI, SQLAlchemy run module, open http://localhost:5000 for Swagger UI or use Postman -for saving swagger openAPI schema to file use command in terminal. pwd must be the same as where app.py -flask spec --output openapi.json +uvicorn app:app --reload --host 127.0.0.1 --port 5000 ''' from datetime import timedelta -from apiflask import APIFlask +from fastapi import FastAPI import logging import logging.config from log4mongo.handlers import MongoHandler -import schemas -from routes import configure_routes + +from routes import router_goods, router_orders, router_auth import log_config from mongo_functions import is_mongo_run -app = APIFlask(__name__) -configure_routes(app) -app.url_map.strict_slashes = False # open /goods/ as /goods -app.config['DESCRIPTION'] = 'RestAPI server with Apiflask and postgresql' -app.config['BASE_RESPONSE_SCHEMA'] = schemas.BaseResponse -# the data key should match the data field name in the base response schema -# defaults to "data" -app.config['BASE_RESPONSE_DATA_KEY '] = 'data' -app.config["JWT_SECRET_KEY"] = "mysecretkey" -app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(minutes=5) -app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=30) -app.secret_key = 'BAD_SECRET_KEY' - - +if not is_mongo_run(): + logging.config.dictConfig(log_config.config_mongo) +else: + logging.config.dictConfig(log_config.config_console) +logger = logging.getLogger('my_log') +logger.info("app started!!!") +app = FastAPI() +app.include_router(router_goods) +app.include_router(router_orders) +app.include_router(router_auth) if __name__ == "__main__": - if not is_mongo_run(): - logging.config.dictConfig(log_config.config_mongo) - else: - logging.config.dictConfig(log_config.config_console) - logger = logging.getLogger('my_log') - logger.info("app started!!!") - app.run(debug=0, host='0.0.0.0') - pass + import uvicorn + uvicorn.run("app:app", host="127.0.0.1", port=5000, reload=True) diff --git a/apps/config.py b/apps/config.py index af31bc1..a162efc 100644 --- a/apps/config.py +++ b/apps/config.py @@ -1,8 +1,8 @@ # from config import host,user,password,db_name # host = "192.168.0.103" for docker -host = "localhost" -user = "flask_user" -password = "flask_user" -db_name = "flask_db" -port = 5433 +DATABASE_USER = 'zvic' +DATABASE_PASSWORD = 'zvic' +DATABASE_HOST = 'localhost' +DATABASE_PORT = '5433' +DATABASE_NAME = 'postgres' diff --git a/apps/db.py b/apps/db.py index c9ff600..9d3b91c 100644 --- a/apps/db.py +++ b/apps/db.py @@ -1,136 +1,109 @@ -import psycopg2 -from config import host, user, password, db_name, port - - -def connect_db(host=host, user=user, password=password, db_name=db_name): - try: - connection = psycopg2.connect( - host=host, - user=user, - password=password, - database=db_name, - port=port - ) - except psycopg2.OperationalError: - print('Error: No BD server ready, try one more') - return 1 - connection.autocommit = True - return connection - - -# select * records from db and return list -def select_all_goods_db() -> list: - connection = connect_db() - if connection == 1: return ["ERROR_serverDB_not_ready"] - with connection.cursor() as g: - g.execute("SELECT id, name FROM goods ORDER BY id;") - cnt = g.fetchall() - goods = [] - for row in cnt: - goods.append({'id': row[0], 'name': row[1], }) - close_db(connection) - return goods - - -# select all orders from db, return list of dictionaries -def select_all_orders_db(user_email: str) -> list: - connection = connect_db() - if connection == 1: return ["ERROR_serverDB_not_ready"] - with connection.cursor() as g: - g.execute( - "SELECT id, order_date, customer_name, customer_email, delivery_address, status, notes FROM orders WHERE customer_email=%s ORDER BY id;", (user_email,)) - cnt = g.fetchall() - orders = [] - for row in cnt: - orders.append({ - 'id': row[0], - 'order_date': row[1], - 'customer_name': row[2], - 'customer_email': row[3], - 'delivery_address': row[4], - 'status': row[5], - 'notes': row[6], - }) - close_db(connection) - return orders - - -# select id record from db and return dict -def select_id_good_db(id: int) -> dict: - connection = connect_db() - if connection == 1: return ["ERROR_serverDB_not_ready"] - with connection.cursor() as g: - g.execute( - "SELECT id,name,price,manufacture_date,picture_url FROM goods WHERE id=%s;", (id,)) - cnt = g.fetchone() - goods = {} - goods['id'] = cnt[0] - goods['name'] = cnt[1] - goods['price'] = cnt[2] - goods['manufacture_date'] = cnt[3] - goods['picture_url'] = cnt[4] - close_db(connection) - return goods - - -# insert new good into db -def insert_good_db(good_list: dict) -> list: - connection = connect_db() - if connection == 1: return ["ERROR_serverDB_not_ready"] - with connection.cursor() as g: - g.execute(""" - INSERT INTO goods (name, price, manufacture_date, picture_url) - VALUES (%s,%s,%s,%s);""", (good_list.get('name'), good_list.get('price'), good_list.get('manufacture_date'), good_list.get('picture_url'))) - g.execute("SELECT id FROM goods ORDER BY id DESC LIMIT 1;") - cnt = g.fetchone() - close_db(connection) - return cnt[0] - - -def insert_order_db(order_list: dict) -> int: - connection = connect_db() - if connection == 1: return ["ERROR_serverDB_not_ready"] - with connection.cursor() as g: - g.execute(""" - INSERT INTO orders (order_date, customer_name, customer_email, delivery_address, status, notes) - VALUES (%s,%s,%s,%s,%s,%s);""", (order_list.get('order_date'), order_list.get('customer_name'), order_list.get('customer_email'), - order_list.get('delivery_address'), 'new_ord', order_list.get('notes'))) - g.execute("SELECT id FROM orders ORDER BY id DESC LIMIT 1;") - cnt = g.fetchone() - goods = order_list.get('good_item') - for item in goods: - g.execute(""" - INSERT INTO order_item (ammount, notes, order_id, good_id) - VALUES (%s,%s,%s,%s);""", (item.get('ammount'), 'notes null', cnt[0], item.get('good_id'))) - close_db(connection) - return cnt[0] # return id new order - - -def update_id_good_db(id: int, good_list: dict) -> int: - connection = connect_db() - if connection == 1: return ["ERROR_serverDB_not_ready"] - with connection.cursor() as g: - g.execute("SELECT COUNT(id) FROM goods WHERE id=%s", (id,)) - cnt = g.fetchone() - if not cnt[0]: - close_db(connection) - return 0 - g.execute(""" - UPDATE goods SET name=%s,price=%s,manufacture_date=%s, - picture_url=%s WHERE id=%s;""", (good_list.get('name'), good_list.get('price'), good_list.get('manufacture_date'), good_list.get('picture_url'), id)) - close_db(connection) - return 1 +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +import logging +from models import Base, Goods, Orders, Order_item +import config -def delete_id_good_db(id: int) -> str: - connection = connect_db() - if connection == 1: return ["ERROR_serverDB_not_ready"] - with connection.cursor() as g: - g.execute("""DELETE FROM goods WHERE id=%s;""", (id,)) - close_db(connection) - return g.statusmessage +engine = create_engine( + f'postgresql+psycopg2://{config.DATABASE_USER}:{config.DATABASE_PASSWORD}@{config.DATABASE_HOST}:{config.DATABASE_PORT}/{config.DATABASE_NAME}', + pool_pre_ping=True, echo=False) + +Base.metadata.create_all(engine) +session_factory = sessionmaker(bind=engine) +db_session = session_factory() +logger = logging.getLogger('db') + + +def select_all_goods_db(): + goods = db_session.query(Goods.id, Goods.name).all() + return goods + + +def select_all_orders_db(user_email: str): + orders = db_session.query(Orders).filter( + Orders.customer_email == user_email).all() + return orders + + +def select_id_good_db(id: int): + goods = db_session.query(Goods).filter(Goods.id == id).one() + return goods -def close_db(connection: any) -> None: - if connection: - connection.close() +def insert_good_db(good_list: dict): + good = Goods(**good_list) + db_session.add(good) + db_session.commit() + return good.id + + +def insert_order_db(order_list: dict): + good_items = order_list.pop('good_item') + order = Orders(**order_list) + db_session.add(order) + db_session.flush() + for g in good_items: + good = Order_item(**g, order_id=order.id, notes='temp') + db_session.add(good) + db_session.commit() + return order.id + + +def update_id_good_db(id: int, good_list: dict): + good = db_session.query(Goods).get(id) + for key, value in good_list.items(): + setattr(good, key, value) + db_session.add(good) + db_session.commit() + return good.id + + +def delete_id_good_db(id: int) -> str: + c = db_session.query(Goods).filter(Goods.id == id) + if not db_session.query(c.exists()).scalar(): + return -1 + res = db_session.query(Goods).filter(Goods.id == id).delete() + db_session.commit() + return res + + +def init_db(): + cnt = db_session.query(Goods).count() + if not cnt: + db_session.add_all([ + Goods(name='Beer', price=112, + manufacture_date='05/07/22', picture_url='pic112'), + Goods(name='Mushrooms', price=12, + manufacture_date='05/07/22', picture_url='pic1/mush'), + Goods(name='keyboard', price=3, + manufacture_date='05/07/20', picture_url='keyb1/c'), + Goods(name='iphone', price=345, + manufacture_date='05/07/15', picture_url='apple.com'), + Goods(name='mouse', price=5, manufacture_date='05/01/12', + picture_url='mouse.com'), + ]) + db_session.add_all([ + Orders(order_date='02/05/2014', customer_name='Sekretov', + customer_email='secret@mail.ru', delivery_address='Apatity', status='new', notes='ww'), + Orders(order_date='02/05/2015', customer_name='Mihrin', customer_email='mihrin@mail.ru', + delivery_address='kirovsk', status='temp', notes='aas'), + Orders(order_date='02/05/2031', customer_name='Zelenskyi', + customer_email='zelo@mail.ru', delivery_address='Kyiev', status='ready', notes=' '), + Orders(order_date='02/05/2022', customer_name='Karlson', customer_email='karlson@mail.ru', + delivery_address='Stohgolm', status='reset', notes='ee'), + Orders(order_date='02/05/2021', customer_name='Ivan', customer_email='ivan@n.r', + delivery_address='prostokvash', status='deliv', notes='sas'), + ]) + db_session.commit() + good1 = db_session.query(Goods).filter(Goods.name == 'keyboard').one() + orders1 = db_session.query(Orders).filter( + Orders.customer_name == 'Sekretov').one() + oi1 = Order_item(ammount=3, notes='notesss') + oi1.goods = good1 + oi1.orders = orders1 + db_session.add(oi1) + db_session.commit() + + +init_db() diff --git a/apps/models.py b/apps/models.py new file mode 100644 index 0000000..e14d85f --- /dev/null +++ b/apps/models.py @@ -0,0 +1,45 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import Column, Integer, TIMESTAMP, VARCHAR, REAL, Date, ForeignKey +from sqlalchemy.orm import relationship +from datetime import datetime + + +Base = declarative_base() + + +class Goods(Base): + __tablename__ = 'goods' + id = Column(Integer, nullable=False, unique=True, + primary_key=True, autoincrement=True) + name = Column(VARCHAR(40), nullable=False) + price = Column(REAL, nullable=False) + manufacture_date = Column(Date, default=datetime.now, nullable=True) + picture_url = Column(VARCHAR(100), nullable=True) + order_item = relationship("Order_item") + + +class Orders(Base): + __tablename__ = 'orders' + id = Column(Integer, nullable=False, unique=True, + primary_key=True, autoincrement=True) + order_date = Column(Date, default=datetime.now, nullable=False) + customer_name = Column(VARCHAR(40), nullable=False) + customer_email = Column(VARCHAR(40), nullable=True) + delivery_address = Column(VARCHAR(50), nullable=True) + status = Column(VARCHAR(50), nullable=True) + notes = Column(VARCHAR(50), nullable=True) + order_item = relationship("Order_item") + + +class Order_item(Base): + __tablename__ = 'order_item' + id = Column(Integer, nullable=False, unique=True, + primary_key=True, autoincrement=True) + ammount = Column(Integer, nullable=False) + notes = Column(VARCHAR(10), nullable=True) + order_id = Column(ForeignKey( + 'orders.id', ondelete='CASCADE'), nullable=False, index=True) + good_id = Column(ForeignKey('goods.id', ondelete='CASCADE'), + nullable=False, index=True) + orders = relationship("Orders") + goods = relationship("Goods") diff --git a/apps/mongo_functions.py b/apps/mongo_functions.py index 5bff204..890bcc1 100644 --- a/apps/mongo_functions.py +++ b/apps/mongo_functions.py @@ -4,7 +4,8 @@ def is_mongo_run() -> int: - client = pymongo.MongoClient(serverSelectionTimeoutMS=3) + client = pymongo.MongoClient( + 'localhost', 27017, serverSelectionTimeoutMS=3) try: client.admin.command('ismaster') except ConnectionFailure: @@ -12,23 +13,26 @@ def is_mongo_run() -> int: return 1 return 0 -def select_logs_from_mongo(timestart: datetime, timeend: datetime, module:str = None) -> dict: + +def select_logs_from_mongo(timestart: datetime, timeend: datetime, module: str = None) -> dict: if is_mongo_run(): return 1 client = pymongo.MongoClient('localhost', 27017) db = client['mongo_logs'] collection = db['logs'] - query = {"$and" : [ - {'timestamp':{"$gte": timestart}}, - {'timestamp':{"$lte": timeend}} - ] + query = {"$and": [ + {'timestamp': {"$gte": timestart}}, + {'timestamp': {"$lte": timeend}} + ] } if module and module != 'all': query['$and'].append({'module': module}) - res = collection.find(query, {'message':1, '_id':0, 'timestamp':1, 'level':1 , 'module':1}) + res = collection.find( + query, {'message': 1, '_id': 0, 'timestamp': 1, 'level': 1, 'module': 1}) return list(res) if __name__ == "__main__": - r = select_logs_from_mongo(datetime(2023,4,30, 18,00,00), datetime(2023,5,2, 00,00,00), 'app') + r = select_logs_from_mongo( + datetime(2023, 4, 30, 18, 00, 00), datetime(2023, 5, 2, 00, 00, 00), 'app') print(r) diff --git a/apps/oauth_functions.py b/apps/oauth_functions.py index 7853bc5..a7dc376 100644 --- a/apps/oauth_functions.py +++ b/apps/oauth_functions.py @@ -1,15 +1,71 @@ # from google.oauth2 import id_token from google_auth_oauthlib.flow import Flow -# import google.auth.transport.requests +from datetime import datetime, timedelta import os +from fastapi import HTTPException, status +from fastapi.security import HTTPBearer +from jose import jwt, JWTError, ExpiredSignatureError + +from schema import TokenData GOOGLE_CLIENT_ID = "175712151730-dblfkundctnmakngt8iok09dmf0p0nmp.apps.googleusercontent.com" client_secrets_file = "client_secret_web.json" -os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" # to allow Http traffic for local dev +# to allow Http traffic for local dev +os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" +SECRET_KEY = "secretkey" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRED = 1 +oauth2_scheme = HTTPBearer() flow = Flow.from_client_secrets_file( client_secrets_file=client_secrets_file, - scopes=["https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email", "openid"], + scopes=["https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/userinfo.email", "openid"], redirect_uri="http://127.0.0.1:5000/callback" ) + +CREDENTIALS_EXCEPTIONS = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, +) +CREDENTIALS_EXCEPTIONS_EXPIRED = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="token expired", + headers={"WWW-Authenticate": "Bearer"}, +) + + +def create_access_token(data: dict): + to_encode = data.copy() + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRED) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +def verify_token(token: str): + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + email: str = payload.get("email") + if email is None: + raise CREDENTIALS_EXCEPTIONS + return email + except ExpiredSignatureError: + raise CREDENTIALS_EXCEPTIONS_EXPIRED + except JWTError: + raise CREDENTIALS_EXCEPTIONS + + +def refresh_token(token: str): + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + except ExpiredSignatureError: + payload = jwt.get_unverified_claims(token) + except JWTError: + raise CREDENTIALS_EXCEPTIONS + email = payload.get("email") + if email is None: + raise CREDENTIALS_EXCEPTIONS + return create_access_token({"email": email}) diff --git a/apps/requirements.txt b/apps/requirements.txt index f038196..abadd7d 100644 --- a/apps/requirements.txt +++ b/apps/requirements.txt @@ -1,7 +1,9 @@ psycopg2-binary==2.9.3 -Flask==2.2.2 -apiflask -flask-jwt-extended +sqlalchemy==1.3 +fastapi +uvicorn[standard] +pydantic[email] +python-jose[cryptography] google-auth-oauthlib pyopenssl redis diff --git a/apps/routes.py b/apps/routes.py index b4c2312..2aeaf0b 100644 --- a/apps/routes.py +++ b/apps/routes.py @@ -1,213 +1,189 @@ -from flask import redirect, jsonify, request -from apiflask import abort +from fastapi import APIRouter, Depends, HTTPException, Request import requests +from fastapi.responses import RedirectResponse from pip._vendor import cachecontrol import google.auth.transport.requests from google.oauth2 import id_token -from flask_jwt_extended import JWTManager,create_access_token, create_refresh_token,jwt_required,get_jwt_identity import redis import json import time +from datetime import datetime import logging import db -import schemas +from schema import GoodIn, OrderIn import oauth_functions import mongo_functions - -jwt = JWTManager() logger = logging.getLogger('my_log') - -def configure_routes(app): - jwt.init_app(app) - @app.get('/') - @app.doc(hide=True) - def index(): - data = {'message': 'Hello!'} - logger.info("return index page") - return redirect("/docs", code=302) - - @app.get('/goods') - @app.output(schemas.GoodsOut(many=True), status_code=200) - def get_goods(): - goods = db.select_all_goods_db() - if goods == ["ERROR_serverDB_not_ready"]: abort(500, message='ERROR_serverDB_not_ready') - logger.info("return get('/goods')") - return { - 'data': goods, - 'code': 200, - } - - @app.get('/orders') - @jwt_required() - @app.output(schemas.OrdersOut(many=True), status_code=200) - def get_orders(): - current_user = get_jwt_identity() - print(current_user) - orders = db.select_all_orders_db(current_user) - if orders == ["ERROR_serverDB_not_ready"]: abort(500, message='ERROR_serverDB_not_ready') - logger.info(f"return get('/orders') for user {current_user}") - return { - 'data': orders, - 'code': 200, - } - - @app.get('/goods/') - @app.output(schemas.GoodOut, status_code=200) - def get_good_id(good_id): - good = db.select_id_good_db(good_id) - if good == ["ERROR_serverDB_not_ready"]: abort(500, message='ERROR_serverDB_not_ready') - if len(good) == 0: - logger.info(f"Error get goods for id={good_id}") - abort(404, message='Error_no_id') - logger.info(f"return get goods for id={good_id}") - return { - 'data': good, - 'code': 200, - } - - @app.post('/goods') - @app.input(schemas.GoodIn) - @app.output(schemas.MessageOk, status_code=201) - def create_good(data): - if not data: - return abort(400, message = 'Error_no_json') - res = {'id': db.insert_good_db(data)} - if res['id'] == ["ERROR_serverDB_not_ready"]: abort(500, message='ERROR_serverDB_not_ready') +router_goods = APIRouter(tags=["goods"],) +router_orders = APIRouter(tags=["orders"],) +router_auth = APIRouter(tags=["auth"],) + + +@router_goods.get("/") +def index(): + return RedirectResponse("/docs") + + +@router_goods.get("/goods") +def get_goods(): + goods = db.select_all_goods_db() + logger.info("return get('/goods')") + return { + 'data': goods, + 'code': 200, + } + + +@router_orders.get("/orders") +def get_orders(credentials: str = Depends(oauth_functions.oauth2_scheme)): + token = credentials.credentials + email = oauth_functions.verify_token(token) + orders = db.select_all_orders_db(email) + logger.info(f"return get('/orders') for user {email}") + return { + 'data': orders, + 'code': 200, + } + + +@router_goods.get("/goods/{good_id}") +def get_good_id(good_id: int): + good = db.select_id_good_db(good_id) + logger.info(f"return get goods for id={good_id}") + return { + 'data': good, + 'code': 200, + } + + +@router_goods.post("/goods") +def create_good(good: GoodIn): + res = {'id': db.insert_good_db(good.dict())} + try: + with redis.Redis() as r: + r.flushdb() + except (redis.exceptions.ConnectionError, redis.exceptions.BusyLoadingError): + logger.info("ERROR get_goods_cached(): redis not ready!") + logger.info(f"post goods for {res}") + return { + 'data': res, + 'code': 201, + } + + +@router_orders.post("/orders") +def create_order(order: OrderIn): + res = {'id': db.insert_order_db(order.dict())} + logger.info(f"post orders for {res}") + return { + 'data': res, + 'code': 201, + } + + +@router_goods.put("/goods/{good_id}") +def put_good_id(good_id: int, good: GoodIn): + res = {'id': db.update_id_good_db(good_id, good.dict())} + try: + with redis.Redis() as r: + r.flushdb() + except (redis.exceptions.ConnectionError, redis.exceptions.BusyLoadingError): + print("ERROR get_goods_cached(): redis not ready!") + logger.info(f"put goods for {res}") + return { + 'data': res, + 'code': 200, + } + + +@router_goods.delete("/goods/{good_id}") +def delete_good_id(good_id): + res = db.delete_id_good_db(good_id) + if res == -1: + raise HTTPException(status_code=404, detail="Error_no_id") + logger.info(f"delete goods for id={good_id}") + return { + 'data': res, + 'code': 204, + } + + +@router_auth.get("/login") +def login(): + authorization_url, state = oauth_functions.flow.authorization_url() + return RedirectResponse(url=authorization_url) + + +@router_auth.get("/callback") +def callback(request: Request): + oauth_functions.flow.fetch_token(authorization_response=str(request.url)) + credentials = oauth_functions.flow.credentials + request_session = requests.session() + cached_session = cachecontrol.CacheControl(request_session) + token_request = google.auth.transport.requests.Request( + session=cached_session) + + id_info = id_token.verify_oauth2_token( + id_token=credentials._id_token, + request=token_request, + audience=oauth_functions.GOOGLE_CLIENT_ID + ) + email = id_info.get("email") + print(f"Success auth for {email=}") + access_token = oauth_functions.create_access_token({"email": email}) + return {"access_token": access_token, "token_type": "bearer"} + + +@router_auth.get("/refresh_token") +def refresh_token(credentials: str = Depends(oauth_functions.oauth2_scheme)): + token = credentials.credentials + access_token = oauth_functions.refresh_token(token) + logger.info(f"refresh token for {token}") + return {"access_token": access_token, "token_type": "bearer"} + + +@router_goods.get("/goods_cached") +def get_goods_cached(): + output_redis = None + goods_cached = [] + try: + with redis.Redis() as r: + output_redis = r.get('goods') + except (redis.exceptions.ConnectionError, redis.exceptions.BusyLoadingError): + logger.info("ERROR get_goods_cached(): redis not ready!") + + if output_redis: + goods_cached = json.loads(output_redis) + else: + tmp = db.select_all_goods_db() + for i in tmp: + goods_cached.append( + {'id': i[0], 'name': i[1], 'price': len(i[1]) + 0.99}) + time.sleep(0.9) try: with redis.Redis() as r: - r.flushdb() + r.set('goods', json.dumps(goods_cached)) except (redis.exceptions.ConnectionError, redis.exceptions.BusyLoadingError): logger.info("ERROR get_goods_cached(): redis not ready!") - logger.info(f"post goods for {res}") - return { - 'data': res, - 'code': 201, - } - - @app.post('/orders') - @app.input(schemas.OrderIn) - @app.output(schemas.MessageOk, status_code=201) - def create_order(data): - if not data: - return abort(400, message = 'Error_no_json') - res = {'id': db.insert_order_db(data)} - if res['id'] == ["ERROR_serverDB_not_ready"]: abort(500, message='ERROR_serverDB_not_ready') - logger.info(f"post orders for {res}") - return { - 'data': res, - 'code': 201, - } - - @app.put('/goods/') - @app.input(schemas.GoodIn) - @app.output(schemas.MessageOk, status_code=201) - def put_good_id(good_id, data): - if not data: - return abort(400, message = 'Error_no_json') - res = {'id': db.update_id_good_db(good_id, data)} - if res['id'] == ["ERROR_serverDB_not_ready"]: abort(500, message='ERROR_serverDB_not_ready') - if not res: - return abort(404, 'Error:no id') - try: - with redis.Redis() as r: - r.flushdb() - except (redis.exceptions.ConnectionError, redis.exceptions.BusyLoadingError): - print("ERROR get_goods_cached(): redis not ready!") - logger.info(f"put goods for {res}") - return { - 'data': res, - 'code': 201, - } - - @app.delete('/goods/') - @app.output(schemas.MessageOk, status_code=204) # if status 204 - no json - def delete_good_id(good_id): - res = db.delete_id_good_db(good_id) - if res == ["ERROR_serverDB_not_ready"]: abort(500, message='ERROR_serverDB_not_ready') - if res[-1] == '0': - return abort(404, message='Error_no_id') - logger.info(f"delete goods for id={good_id}") - return { - 'data': 1, - 'code': 204, - } - - @app.get('/login') - def login(): - authorization_url, state = oauth_functions.flow.authorization_url() - return redirect(authorization_url,code=302) - - @app.get("/callback") - def callback(): - oauth_functions.flow.fetch_token(authorization_response=request.url) - credentials = oauth_functions.flow.credentials - request_session = requests.session() - cached_session = cachecontrol.CacheControl(request_session) - token_request = google.auth.transport.requests.Request(session=cached_session) - - id_info = id_token.verify_oauth2_token( - id_token=credentials._id_token, - request=token_request, - audience=oauth_functions.GOOGLE_CLIENT_ID - ) - email = id_info.get("email") - access_token = create_access_token(identity=email) - refresh_token = create_refresh_token(identity=email) - logger.info(f"jwt token return succesfully for {email}") - return jsonify(access_token=access_token, refresh_token=refresh_token) - - - @app.get("/refresh_token") - @jwt_required(refresh=True) - def refresh(): - identity = get_jwt_identity() - access_token = create_access_token(identity=identity) - logger.info(f'return new token in refresh for {identity}') - return jsonify(access_token=access_token) - - - @app.get("/goods_cached") - @app.output(schemas.GoodsOutCached(many=True), status_code=200) - def get_goods_cached(): - output_redis = None - try: - with redis.Redis() as r: - output_redis = r.get('goods') - except (redis.exceptions.ConnectionError, redis.exceptions.BusyLoadingError): - logger.info("ERROR get_goods_cached(): redis not ready!") - - - if output_redis: - goods_cached = json.loads(output_redis) - else: - goods_cached = db.select_all_goods_db() - for item in goods_cached: - item['price'] = len(item['name']) + 0.99 - time.sleep(0.9) - try: - with redis.Redis() as r: - r.set('goods', json.dumps(goods_cached)) - except (redis.exceptions.ConnectionError, redis.exceptions.BusyLoadingError): - logger.info("ERROR get_goods_cached(): redis not ready!") - logger.info("return goods_cached ") - return { - 'data': goods_cached, - 'code': 200, - } - - @app.get("/logs") - @app.input(schemas.LogsFilterIn, location = 'query') - @app.output(schemas.LogsOut(many=True), status_code=200) - def get_logs(query): - result = (mongo_functions.select_logs_from_mongo(query['timestart'], - query['timeend'], query.get('module'))) - if result == 1: - logger.info(f"Error get logs, server Mongo not ready") - abort(404, 'Error_Mongo_not_ready') - logger.info(f"return logs from Mongo") - return { - 'data': result, - 'code': 200, - } + logger.info("return goods_cached ") + return { + 'data': goods_cached, + 'code': 200, + } + + +@router_goods.get("/logs") +def get_logs(timestart: datetime = "2023-06-25T00:53:00", timeend: datetime = "2023-06-28T00:53:00", module: str = "all"): + print(timestart, timeend) + result = (mongo_functions.select_logs_from_mongo( + timestart, timeend)) # , query.get('module'))) + if result == 1: + logger.info(f"Error get logs, server Mongo not ready") + raise HTTPException(status_code=404, detail="Mongo not ready") + logger.info(f"return logs from Mongo") + return { + 'data': result, + 'code': 200, + } diff --git a/apps/schema.py b/apps/schema.py new file mode 100644 index 0000000..a4ae7d0 --- /dev/null +++ b/apps/schema.py @@ -0,0 +1,34 @@ +from datetime import date +from pydantic import BaseModel, EmailStr, validator +from typing import Optional + + +class GoodIn(BaseModel): + # id = int + name: str = "Kakao" + price: float = 2.5 + manufacture_date: date | None + picture_url: str | None = r"pics.com/kakao.jpg" + + +class OrderGood(BaseModel): + good_id: int + ammount: int + + @validator('ammount') + def ammount_positive(cls, v): + assert v > 0, 'must be positive' + return v + + +class OrderIn(BaseModel): + order_date: date + customer_name: str + customer_email: EmailStr | None + delivery_address: str | None + notes: str | None + status: str | None + good_item: list[OrderGood] + +class TokenData(BaseModel): + email: Optional[str] = None diff --git a/apps/schemas.py b/apps/schemas.py deleted file mode 100644 index 5129602..0000000 --- a/apps/schemas.py +++ /dev/null @@ -1,76 +0,0 @@ -from apiflask import Schema, fields -from apiflask.validators import OneOf -from apiflask.validators import Range - - -class BaseResponse(Schema): - data = fields.Field() # the data key - code = fields.Integer() - - -class GoodsOut(Schema): # list of short goods for response - id = fields.Integer(metadata={'example': 2}) - name = fields.String(metadata={'example': 'Coffee'}) - -class GoodsOutCached(GoodsOut): - price = fields.Float(metadata={'example': 55.99}) - - -class GoodIn(Schema): # one good with full fields - name = fields.String(required=True, metadata={'example': 'Coffee'}) - price = fields.Integer(metadata={'example': 21}) - manufacture_date = fields.Date(metadata={'example': '2019-02-05'}) - picture_url = fields.String(metadata={'example': 'pic.com/mypic.jpg'}) - - -class GoodOut(GoodIn): # one good with full fields - id = fields.Integer() - - -class OrdersOut(Schema): # list of orders for responce - id = fields.Integer(required=True) - order_date = fields.Date() - customer_name = fields.String() - customer_email = fields.String() - delivery_address = fields.String() - status = fields.String() - notes = fields.String() - - class Meta: - ordered = True - - -class OrderGood(Schema): - good_id = fields.Integer(required=True, validate=Range( - min=1), metadata={'example': 1}) - ammount = fields.Integer(required=True, validate=Range( - 1, 1000), metadata={'example': 2}) - - -class OrderIn(Schema): - order_date = fields.Date(required=True, metadata={'example': '2022-11-05'}) - customer_name = fields.String( - required=True, metadata={'example': 'Carlson'}) - customer_email = fields.String(metadata={'example': 'carlson@gmail.com'}) - delivery_address = fields.String(metadata={'example': 'Apatity'}) - notes = fields.String(metadata={'example': 'poor customer'}) - good_item = fields.List(fields.Nested(OrderGood()), required=True) - - -class MessageOk(Schema): - id = fields.Integer() - -class LoginIn(Schema): - username = fields.String(required=True, metadata={'example': 'vic'}) - password = fields.String(metadata={'example': "mypass"}) - -class LogsFilterIn(Schema): - timestart = fields.DateTime(required=True, metadata={'example': '2023-04-30 18:00:00'}) - timeend = fields.DateTime(required=True, metadata={'example': '2023-05-01 18:00:00'}) - module = fields.String(validate=OneOf(['all', 'app', 'routes']), metadata={'example': "routes"}) - -class LogsOut(Schema): - level = fields.String() - message = fields.String() - module = fields.String() - timestamp = fields.DateTime() diff --git a/db/flask_db.sql b/db/flask_db.sql index c147f68..1c9f66f 100644 --- a/db/flask_db.sql +++ b/db/flask_db.sql @@ -4,75 +4,75 @@ CREATE ROLE flask_user PASSWORD 'flask_user' CREATEDB CREATEROLE INHERIT LOGIN; CREATE DATABASE flask_db WITH OWNER = flask_user ENCODING = 'UTF-8'; \connect flask_db -CREATE TABLE public.goods ( - id serial PRIMARY KEY,--public.goods_id_seq - name character varying(40) NOT NULL, - price real, - manufacture_date date, - picture_url character varying(100) -); +-- CREATE TABLE public.goods ( +-- id serial PRIMARY KEY,--public.goods_id_seq +-- name character varying(40) NOT NULL, +-- price real, +-- manufacture_date date, +-- picture_url character varying(100) +-- ); -CREATE TABLE public.orders ( - id serial PRIMARY KEY, --public.orders_id_seq - order_date date, - customer_name character varying(40) NOT NULL, - customer_email character varying(40), - delivery_address character varying(50), - status varchar(10), - notes character varying(40) -); +-- CREATE TABLE public.orders ( +-- id serial PRIMARY KEY, --public.orders_id_seq +-- order_date date, +-- customer_name character varying(40) NOT NULL, +-- customer_email character varying(40), +-- delivery_address character varying(50), +-- status varchar(10), +-- notes character varying(40) +-- ); -CREATE TABLE public.order_item( - id serial PRIMARY KEY, --public.order_item_id_seq - ammount integer NOT NULL, - notes character varying(40) -); +-- CREATE TABLE public.order_item( +-- id serial PRIMARY KEY, --public.order_item_id_seq +-- ammount integer NOT NULL, +-- notes character varying(40) +-- ); --- ALTER TABLE ONLY public.goods ADD CONSTRAINT goods_pkey PRIMARY KEY (id); -ALTER TABLE public.order_item ADD COLUMN order_id INTEGER; -ALTER TABLE public.order_item - ADD CONSTRAINT fk_orderid - FOREIGN KEY (order_id) - REFERENCES public.orders(id); -ALTER TABLE public.order_item ADD COLUMN good_id INTEGER; -ALTER TABLE public.order_item - ADD CONSTRAINT fk_goodid - FOREIGN KEY (good_id) - REFERENCES public.goods(id); -ALTER SEQUENCE public.orders_id_seq RESTART WITH 10; -ALTER SEQUENCE public.goods_id_seq RESTART WITH 10; -ALTER SEQUENCE public.order_item_id_seq RESTART WITH 10; +-- -- ALTER TABLE ONLY public.goods ADD CONSTRAINT goods_pkey PRIMARY KEY (id); +-- ALTER TABLE public.order_item ADD COLUMN order_id INTEGER; +-- ALTER TABLE public.order_item +-- ADD CONSTRAINT fk_orderid +-- FOREIGN KEY (order_id) +-- REFERENCES public.orders(id); +-- ALTER TABLE public.order_item ADD COLUMN good_id INTEGER; +-- ALTER TABLE public.order_item +-- ADD CONSTRAINT fk_goodid +-- FOREIGN KEY (good_id) +-- REFERENCES public.goods(id); +-- ALTER SEQUENCE public.orders_id_seq RESTART WITH 10; +-- ALTER SEQUENCE public.goods_id_seq RESTART WITH 10; +-- ALTER SEQUENCE public.order_item_id_seq RESTART WITH 10; -ALTER TABLE public.goods OWNER TO flask_user; -ALTER TABLE public.orders OWNER TO flask_user; -ALTER TABLE public.order_item OWNER TO flask_user; -GRANT ALL ON TABLE public.goods TO flask_user; -GRANT ALL ON TABLE public.orders TO flask_user; -GRANT ALL ON TABLE public.order_item TO flask_user; +-- ALTER TABLE public.goods OWNER TO flask_user; +-- ALTER TABLE public.orders OWNER TO flask_user; +-- ALTER TABLE public.order_item OWNER TO flask_user; +-- GRANT ALL ON TABLE public.goods TO flask_user; +-- GRANT ALL ON TABLE public.orders TO flask_user; +-- GRANT ALL ON TABLE public.order_item TO flask_user; GRANT ALL ON SCHEMA public TO flask_user; -INSERT INTO public.goods VALUES (1,'Beer', 112, '05/07/22', 'pic112'); -INSERT INTO public.goods VALUES (2,'Mushrooms', 12, '05/07/22', 'pic1'); -INSERT INTO public.goods VALUES (3,'keyboard', 2.5, '05/07/22', 'pickkeybord'); -INSERT INTO public.goods VALUES (4,'iphone', 199, '2/2/11', 'ya.com'); -INSERT INTO public.goods VALUES (5,'power supply', 1, '2/2/11', 'fig.com'); -INSERT INTO public.goods VALUES (6,'mouse', 1000.8, '2/2/11', 'mses.com'); +-- INSERT INTO public.goods VALUES (1,'Beer', 112, '05/07/22', 'pic112'); +-- INSERT INTO public.goods VALUES (2,'Mushrooms', 12, '05/07/22', 'pic1'); +-- INSERT INTO public.goods VALUES (3,'keyboard', 2.5, '05/07/22', 'pickkeybord'); +-- INSERT INTO public.goods VALUES (4,'iphone', 199, '2/2/11', 'ya.com'); +-- INSERT INTO public.goods VALUES (5,'power supply', 1, '2/2/11', 'fig.com'); +-- INSERT INTO public.goods VALUES (6,'mouse', 1000.8, '2/2/11', 'mses.com'); -INSERT INTO public.orders (id, order_date, customer_name, customer_email, delivery_address, status, notes) - VALUES (1,'02/05/2019', 'Sekretov', 'zvic1981@gmail.com', 'Apatity', 'new', 'paid'), - (2,'02/05/2019', 'Mihrin', 'zvic1981@gmail.com', 'Kirovsk', 'transact', 'not paid'), - (3,'02/05/2019', 'zakharov', 'zakharov@list.ru', 'Apatity', 'paid', 'paid'), - (4,'02/05/2019', 'Zelenskiy', 'zelensky@gmail.com', 'Kiyev', 'closed', 'paid'), - (5,'02/05/2019', 'Carlson', 'zvic1981@gmail.com', 'Stogholm', 'paid', 'paid'); +-- INSERT INTO public.orders (id, order_date, customer_name, customer_email, delivery_address, status, notes) +-- VALUES (1,'02/05/2019', 'Sekretov', 'zvic1981@gmail.com', 'Apatity', 'new', 'paid'), +-- (2,'02/05/2019', 'Mihrin', 'zvic1981@gmail.com', 'Kirovsk', 'transact', 'not paid'), +-- (3,'02/05/2019', 'zakharov', 'zakharov@list.ru', 'Apatity', 'paid', 'paid'), +-- (4,'02/05/2019', 'Zelenskiy', 'zelensky@gmail.com', 'Kiyev', 'closed', 'paid'), +-- (5,'02/05/2019', 'Carlson', 'zvic1981@gmail.com', 'Stogholm', 'paid', 'paid'); -INSERT INTO public.order_item (id, ammount, notes, order_id, good_id) - VALUES (1, 4, 'keyboard', 1, 3), - (2, 1, 'wifi', 1, 2), - (3, 29, 'power', 3, 5), - (4, 12, 'iphone', 3, 4); +-- INSERT INTO public.order_item (id, ammount, notes, order_id, good_id) +-- VALUES (1, 4, 'keyboard', 1, 3), +-- (2, 1, 'wifi', 1, 2), +-- (3, 29, 'power', 3, 5), +-- (4, 12, 'iphone', 3, 4); -SELECT pg_catalog.setval('public.goods_id_seq', 11, true); +-- SELECT pg_catalog.setval('public.goods_id_seq', 11, true); -- diff --git a/flask_api.postman_collection.json b/flask_api.postman_collection.json index ca0abab..0f7d8ec 100644 --- a/flask_api.postman_collection.json +++ b/flask_api.postman_collection.json @@ -55,7 +55,7 @@ "method": "GET", "header": [], "url": { - "raw": "http://127.0.0.1:5000/logs?timestart=2023-04-30 18:00:00&timeend=2023-05-03 18:00:00&module=routes", + "raw": "http://127.0.0.1:5000/logs?timestart=2008-09-15T15:53:00&timeend=2008-09-15T15:53:00", "protocol": "http", "host": [ "127", @@ -70,15 +70,16 @@ "query": [ { "key": "timestart", - "value": "2023-04-30 18:00:00" + "value": "2008-09-15T15:53:00" }, { "key": "timeend", - "value": "2023-05-03 18:00:00" + "value": "2008-09-15T15:53:00" }, { "key": "module", - "value": "routes" + "value": "routes", + "disabled": true } ] } diff --git a/readme.md b/readme.md index 83752ca..dfc3cb9 100644 --- a/readme.md +++ b/readme.md @@ -3,9 +3,9 @@ Flask_api is a Python application for test API REST. It's the model of simple st Technologies used: - APIFlask -- psycopg2 +- SQLAlchemy - pytest -- FlaskJWT +- Jose jwt - GoogleAuth - Docker compose - Github Action