From 131567a096bff93c9c971efbad4db64aa78128e1 Mon Sep 17 00:00:00 2001 From: vic Date: Fri, 2 Jun 2023 16:16:35 +0300 Subject: [PATCH 1/4] rewrite DB functions with SQLAlchemy. Ready, no errors --- apps/app.py | 1 - apps/config.py | 10 +- apps/db.py | 238 +++++++++++++++++++----------------------- apps/models.py | 45 ++++++++ apps/requirements.txt | 1 + apps/routes.py | 49 +++------ db/flask_db.sql | 118 ++++++++++----------- 7 files changed, 233 insertions(+), 229 deletions(-) create mode 100644 apps/models.py diff --git a/apps/app.py b/apps/app.py index 8e8fdca..47e4f31 100644 --- a/apps/app.py +++ b/apps/app.py @@ -30,7 +30,6 @@ app.secret_key = 'BAD_SECRET_KEY' - if __name__ == "__main__": if not is_mongo_run(): logging.config.dictConfig(log_config.config_mongo) 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..68694c5 100644 --- a/apps/db.py +++ b/apps/db.py @@ -1,136 +1,110 @@ -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=True +) + +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/requirements.txt b/apps/requirements.txt index f038196..cd545ec 100644 --- a/apps/requirements.txt +++ b/apps/requirements.txt @@ -1,4 +1,5 @@ psycopg2-binary==2.9.3 +sqlalchemy==1.3 Flask==2.2.2 apiflask flask-jwt-extended diff --git a/apps/routes.py b/apps/routes.py index b4c2312..c972809 100644 --- a/apps/routes.py +++ b/apps/routes.py @@ -4,7 +4,7 @@ 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 +from flask_jwt_extended import JWTManager, create_access_token, create_refresh_token, jwt_required, get_jwt_identity import redis import json import time @@ -19,8 +19,10 @@ jwt = JWTManager() logger = logging.getLogger('my_log') + def configure_routes(app): jwt.init_app(app) + @app.get('/') @app.doc(hide=True) def index(): @@ -32,7 +34,6 @@ def index(): @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, @@ -44,9 +45,7 @@ def get_goods(): @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, @@ -57,10 +56,6 @@ def get_orders(): @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, @@ -71,10 +66,7 @@ def get_good_id(good_id): @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') try: with redis.Redis() as r: r.flushdb() @@ -90,10 +82,7 @@ def create_good(data): @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, @@ -102,12 +91,9 @@ def create_order(data): @app.put('/goods/') @app.input(schemas.GoodIn) - @app.output(schemas.MessageOk, status_code=201) + @app.output(schemas.MessageOk, status_code=200) 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: @@ -118,26 +104,25 @@ def put_good_id(good_id, data): logger.info(f"put goods for {res}") return { 'data': res, - 'code': 201, + 'code': 200, } @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': + if res == -1: return abort(404, message='Error_no_id') logger.info(f"delete goods for id={good_id}") return { - 'data': 1, + 'data': res, 'code': 204, } @app.get('/login') def login(): authorization_url, state = oauth_functions.flow.authorization_url() - return redirect(authorization_url,code=302) + return redirect(authorization_url, code=302) @app.get("/callback") def callback(): @@ -145,7 +130,8 @@ def callback(): 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) + token_request = google.auth.transport.requests.Request( + session=cached_session) id_info = id_token.verify_oauth2_token( id_token=credentials._id_token, @@ -158,7 +144,6 @@ def callback(): 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(): @@ -167,24 +152,24 @@ def refresh(): 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 + 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: - goods_cached = db.select_all_goods_db() - for item in goods_cached: - item['price'] = len(item['name']) + 0.99 + 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: @@ -198,11 +183,11 @@ def get_goods_cached(): } @app.get("/logs") - @app.input(schemas.LogsFilterIn, location = 'query') + @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'))) + query['timeend'], query.get('module'))) if result == 1: logger.info(f"Error get logs, server Mongo not ready") abort(404, 'Error_Mongo_not_ready') 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); -- From 7e9702ec8c2f7261077c384ff9b090e845b75ee1 Mon Sep 17 00:00:00 2001 From: vic Date: Fri, 9 Jun 2023 16:30:12 +0300 Subject: [PATCH 2/4] started rewrite on FastAPI --- apps/app.py | 37 ++++++---------- apps/db.py | 3 +- apps/requirements.txt | 5 +-- apps/routes.py | 99 ++++++++++++++++++++++++------------------- apps/schema.py | 10 +++++ apps/schemas.py | 76 --------------------------------- 6 files changed, 80 insertions(+), 150 deletions(-) create mode 100644 apps/schema.py delete mode 100644 apps/schemas.py diff --git a/apps/app.py b/apps/app.py index 47e4f31..8d34dee 100644 --- a/apps/app.py +++ b/apps/app.py @@ -5,37 +5,24 @@ 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 main:app --reload ''' 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 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!!!") - -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 +app = FastAPI() +app.include_router(router_goods) +app.include_router(router_orders) diff --git a/apps/db.py b/apps/db.py index 68694c5..9d3b91c 100644 --- a/apps/db.py +++ b/apps/db.py @@ -7,8 +7,7 @@ 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=True -) + pool_pre_ping=True, echo=False) Base.metadata.create_all(engine) session_factory = sessionmaker(bind=engine) diff --git a/apps/requirements.txt b/apps/requirements.txt index cd545ec..6b1424e 100644 --- a/apps/requirements.txt +++ b/apps/requirements.txt @@ -1,8 +1,7 @@ psycopg2-binary==2.9.3 sqlalchemy==1.3 -Flask==2.2.2 -apiflask -flask-jwt-extended +fastapi +uvicorn[standard] google-auth-oauthlib pyopenssl redis diff --git a/apps/routes.py b/apps/routes.py index c972809..79f8fde 100644 --- a/apps/routes.py +++ b/apps/routes.py @@ -1,66 +1,75 @@ -from flask import redirect, jsonify, request -from apiflask import abort -import requests +from fastapi import APIRouter, Depends +from fastapi.responses import RedirectResponse +from schema import GoodIn +# import requests 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 +# from flask_jwt_extended import JWTManager, create_access_token, create_refresh_token, jwt_required, get_jwt_identity import redis import json import time import logging import db -import schemas + import oauth_functions import mongo_functions -jwt = JWTManager() +# jwt = JWTManager() logger = logging.getLogger('my_log') +router_goods = APIRouter(prefix="/api/v1", tags=["goods"],) +router_orders = APIRouter(prefix="/api/v1", tags=["orders"],) -def configure_routes(app): - jwt.init_app(app) +@router_goods.get("/") +def index(): + return RedirectResponse("/docs") - @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() - 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() - orders = db.select_all_orders_db(current_user) - logger.info(f"return get('/orders') for user {current_user}") - return { - 'data': orders, - 'code': 200, - } +@router_goods.get("/goods") +def get_goods(): + goods = db.select_all_goods_db() + logger.info("return get('/goods')") + return { + 'data': goods, + '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) - logger.info(f"return get goods for id={good_id}") - return { - 'data': good, - 'code': 200, - } + +@router_orders.get("/orders") +def get_orders(): + orders = db.select_all_orders_db("mihrin@gmail.com") + logger.info(f"return get('/orders') for user ") + 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)} + return { + 'data': res, + 'code': 201, + } + + +''' +def configure_routes(app): + jwt.init_app(app) @app.post('/goods') @app.input(schemas.GoodIn) @@ -196,3 +205,5 @@ def get_logs(query): 'data': result, 'code': 200, } + +''' diff --git a/apps/schema.py b/apps/schema.py new file mode 100644 index 0000000..e5fecbc --- /dev/null +++ b/apps/schema.py @@ -0,0 +1,10 @@ +from datetime import datetime +from pydantic import BaseModel + + +class GoodIn(BaseModel): + id = int + name = str + price = float + manufacture_date = datetime + picture_url = str 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() From 5120925a1c9e92e37bc9ac1fd878149aafd91498 Mon Sep 17 00:00:00 2001 From: vic Date: Tue, 20 Jun 2023 13:17:29 +0300 Subject: [PATCH 3/4] continue rewriting routes --- apps/app.py | 5 ++ apps/oauth_functions.py | 1 + apps/requirements.txt | 1 + apps/routes.py | 105 +++++++++++++++---------------------- apps/schema.py | 113 +++++++++++++++++++++++++++++++++++++--- 5 files changed, 156 insertions(+), 69 deletions(-) diff --git a/apps/app.py b/apps/app.py index 8d34dee..ff29f20 100644 --- a/apps/app.py +++ b/apps/app.py @@ -12,6 +12,7 @@ import logging import logging.config from log4mongo.handlers import MongoHandler + from routes import router_goods, router_orders import log_config from mongo_functions import is_mongo_run @@ -26,3 +27,7 @@ app = FastAPI() app.include_router(router_goods) app.include_router(router_orders) +# if __name__ == "__main__": +# import uvicorn +# uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True) + \ No newline at end of file diff --git a/apps/oauth_functions.py b/apps/oauth_functions.py index 7853bc5..d543c3a 100644 --- a/apps/oauth_functions.py +++ b/apps/oauth_functions.py @@ -3,6 +3,7 @@ # import google.auth.transport.requests import os + 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 diff --git a/apps/requirements.txt b/apps/requirements.txt index 6b1424e..5b4fdf9 100644 --- a/apps/requirements.txt +++ b/apps/requirements.txt @@ -2,6 +2,7 @@ psycopg2-binary==2.9.3 sqlalchemy==1.3 fastapi uvicorn[standard] +pydantic[email] google-auth-oauthlib pyopenssl redis diff --git a/apps/routes.py b/apps/routes.py index 79f8fde..edca500 100644 --- a/apps/routes.py +++ b/apps/routes.py @@ -1,7 +1,5 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import RedirectResponse -from schema import GoodIn -# import requests from pip._vendor import cachecontrol import google.auth.transport.requests from google.oauth2 import id_token @@ -12,11 +10,10 @@ import logging import db - +from schema import GoodIn, OrderIn import oauth_functions import mongo_functions - # jwt = JWTManager() logger = logging.getLogger('my_log') router_goods = APIRouter(prefix="/api/v1", tags=["goods"],) @@ -60,73 +57,57 @@ def get_good_id(good_id: int): @router_goods.post("/goods") def create_good(good: GoodIn): - res = {'id': db.insert_good_db(good)} + 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, } -''' -def configure_routes(app): - jwt.init_app(app) +@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, + } - @app.post('/goods') - @app.input(schemas.GoodIn) - @app.output(schemas.MessageOk, status_code=201) - def create_good(data): - res = {'id': db.insert_good_db(data)} - 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_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, + } - @app.post('/orders') - @app.input(schemas.OrderIn) - @app.output(schemas.MessageOk, status_code=201) - def create_order(data): - res = {'id': db.insert_order_db(data)} - logger.info(f"post orders for {res}") - return { - 'data': res, - 'code': 201, - } +@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, + } - @app.put('/goods/') - @app.input(schemas.GoodIn) - @app.output(schemas.MessageOk, status_code=200) - def put_good_id(good_id, data): - res = {'id': db.update_id_good_db(good_id, data)} - 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': 200, - } +''' +def configure_routes(app): + jwt.init_app(app) - @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 == -1: - return abort(404, message='Error_no_id') - logger.info(f"delete goods for id={good_id}") - return { - 'data': res, - 'code': 204, - } @app.get('/login') def login(): diff --git a/apps/schema.py b/apps/schema.py index e5fecbc..de23b3f 100644 --- a/apps/schema.py +++ b/apps/schema.py @@ -1,10 +1,109 @@ -from datetime import datetime -from pydantic import BaseModel +from datetime import date +from pydantic import BaseModel, EmailStr, validator class GoodIn(BaseModel): - id = int - name = str - price = float - manufacture_date = datetime - picture_url = str + # 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] + + ''' + 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 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() + +''' From aa6265379ca3c833933707a01d776b2488b485dc Mon Sep 17 00:00:00 2001 From: vic Date: Wed, 28 Jun 2023 15:29:06 +0300 Subject: [PATCH 4/4] Made all routes with fastapi; Fixed errors in functions oauth, mongo, redis; Ready for review --- apps/app.py | 16 ++- apps/mongo_functions.py | 20 ++-- apps/oauth_functions.py | 61 ++++++++++- apps/requirements.txt | 1 + apps/routes.py | 173 +++++++++++++++--------------- apps/schema.py | 81 +------------- flask_api.postman_collection.json | 9 +- readme.md | 4 +- 8 files changed, 174 insertions(+), 191 deletions(-) diff --git a/apps/app.py b/apps/app.py index ff29f20..48ae460 100644 --- a/apps/app.py +++ b/apps/app.py @@ -1,11 +1,9 @@ ''' -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 main:app --reload +uvicorn app:app --reload --host 127.0.0.1 --port 5000 ''' from datetime import timedelta from fastapi import FastAPI @@ -13,7 +11,7 @@ import logging.config from log4mongo.handlers import MongoHandler -from routes import router_goods, router_orders +from routes import router_goods, router_orders, router_auth import log_config from mongo_functions import is_mongo_run @@ -27,7 +25,7 @@ app = FastAPI() app.include_router(router_goods) app.include_router(router_orders) -# if __name__ == "__main__": -# import uvicorn -# uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True) - \ No newline at end of file +app.include_router(router_auth) +if __name__ == "__main__": + import uvicorn + uvicorn.run("app:app", host="127.0.0.1", port=5000, reload=True) 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 d543c3a..a7dc376 100644 --- a/apps/oauth_functions.py +++ b/apps/oauth_functions.py @@ -1,16 +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 5b4fdf9..abadd7d 100644 --- a/apps/requirements.txt +++ b/apps/requirements.txt @@ -3,6 +3,7 @@ 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 edca500..2aeaf0b 100644 --- a/apps/routes.py +++ b/apps/routes.py @@ -1,12 +1,13 @@ -from fastapi import APIRouter, Depends, HTTPException +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 @@ -14,10 +15,10 @@ import oauth_functions import mongo_functions -# jwt = JWTManager() logger = logging.getLogger('my_log') -router_goods = APIRouter(prefix="/api/v1", tags=["goods"],) -router_orders = APIRouter(prefix="/api/v1", tags=["orders"],) +router_goods = APIRouter(tags=["goods"],) +router_orders = APIRouter(tags=["orders"],) +router_auth = APIRouter(tags=["auth"],) @router_goods.get("/") @@ -36,9 +37,11 @@ def get_goods(): @router_orders.get("/orders") -def get_orders(): - orders = db.select_all_orders_db("mihrin@gmail.com") - logger.info(f"return get('/orders') for user ") +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, @@ -79,6 +82,7 @@ def create_order(order: OrderIn): '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())} @@ -93,6 +97,7 @@ def put_good_id(good_id: int, good: GoodIn): 'code': 200, } + @router_goods.delete("/goods/{good_id}") def delete_good_id(good_id): res = db.delete_id_good_db(good_id) @@ -104,87 +109,81 @@ def delete_good_id(good_id): 'code': 204, } -''' -def configure_routes(app): - jwt.init_app(app) - - - @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 - goods_cached = [] + +@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: - output_redis = r.get('goods') + 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, + } + - 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.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, - } - -''' +@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 index de23b3f..a4ae7d0 100644 --- a/apps/schema.py +++ b/apps/schema.py @@ -1,5 +1,6 @@ from datetime import date from pydantic import BaseModel, EmailStr, validator +from typing import Optional class GoodIn(BaseModel): @@ -29,81 +30,5 @@ class OrderIn(BaseModel): status: str | None good_item: list[OrderGood] - ''' - 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 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() - -''' +class TokenData(BaseModel): + email: Optional[str] = None 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