diff --git a/.fission/local-deployment.json b/.fission/local-deployment.json index 9785052..593720d 100644 --- a/.fission/local-deployment.json +++ b/.fission/local-deployment.json @@ -3,11 +3,11 @@ "secrets": { "fission-ailbl-user-address-env": { "literals": [ - "S3_BUCKET=ailbl", - "S3_ENDPOINT_URL=http://160.30.113.113:9000", - "S3_ACCESS_KEY_ID=quyen", - "S3_SECRET_ACCESS_KEY=12345678", - "S3_PREFIX=user/avatar" + "PG_HOST=160.30.113.113", + "PG_PORT=45432", + "PG_DB=postgres", + "PG_USER=postgres", + "PG_PASS=q2q32RQx9R9qVAp3vkVrrASnSUUhzKvC" ] } } diff --git a/apps/ailbl-admin_address-delete-update.py b/apps/ailbl-admin_address-delete-update.py index 795aab8..9bf759d 100644 --- a/apps/ailbl-admin_address-delete-update.py +++ b/apps/ailbl-admin_address-delete-update.py @@ -8,11 +8,11 @@ def main(): """ ```fission { - "name": "address-admin-delete-update", + "name": "address-admin-delete", "http_triggers": { - "address-admin-delete-update-http": { + "address-admin-delete-http": { "url": "/ailbl/admin/users/{UserId}/addresses/{UserAddrId}", - "methods": ["DELETE", "PUT"] + "methods": ["DELETE"] } } } diff --git a/apps/ailbl-user_address-delete-update.py b/apps/ailbl-user_address-delete-update.py deleted file mode 100644 index 4cce2ac..0000000 --- a/apps/ailbl-user_address-delete-update.py +++ /dev/null @@ -1,59 +0,0 @@ -import crud -from flask import jsonify, request - -# from storage.minio_client import get_minio_client, check_existing_avatar_on_minio, upload_to_minio -ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"} - - -def main(): - """ - ```fission - { - "name": "address-users-delete-update", - "http_triggers": { - "address-users-delete-update-http": { - "url": "/ailbl/users/addresses", - "methods": ["DELETE", "PUT"] - } - } - } - ``` - """ - try: - if request.method == "DELETE": - return make_delete_request() - elif request.method == "PUT": - return make_update_request() - else: - return {"error": "Method not allow"}, 405 - except Exception as ex: - return jsonify({"error": str(ex)}), 500 - - -def make_delete_request(): - try: - user_id = request.headers.get("X-User") # Lay user_id tu header X-User - # Lay file tu form-data voi key la 'avatar' - file = request.files.get("avatar") - if not user_id or not file: - return jsonify({"error": "user_id or file is required"}), 400 - # Check mimetype(kieu du lieu cua file anh) - if file.mimetype not in ALLOWED_IMAGE_TYPES: - return jsonify( - {"error": "Invalid file type. Only JPG, PNG, GIF, WEBP are allowed."} - ), 400 - response, status = crud.update_or_create_avatar(user_id, file) - return jsonify(response), status - except Exception as e: - return jsonify({"error": str(e)}), 500 - - -def make_update_request(): - try: - user_id = request.headers.get("X-User") - if not user_id: - return jsonify({"error": "user_id is required"}), 400 - return crud.get_avatar_url(user_id) - # return jsonify(response), status - except Exception as e: - return jsonify({"error": str(e)}), 500 diff --git a/apps/ailbl-user_address-delete.py b/apps/ailbl-user_address-delete.py new file mode 100644 index 0000000..4a48720 --- /dev/null +++ b/apps/ailbl-user_address-delete.py @@ -0,0 +1,97 @@ +import crud +from flask import jsonify, request + +# from storage.minio_client import get_minio_client, check_existing_avatar_on_minio, upload_to_minio +ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"} + + +def main(): + """ + ```fission + { + "name": "address-users-delete", + "http_triggers": { + "address-users-delete-http": { + "url": "/ailbl/users/addresses/{UserAddrId}", + "methods": ["DELETE"] + } + } + } + ``` + """ + try: + if request.method == "DELETE": + return make_delete_request() + # elif request.method == "PUT": + # return make_update_request() + else: + return {"error": "Method not allow"}, 405 + except Exception as ex: + return jsonify({"error": str(ex)}), 500 + + +def make_delete_request(): + try: + # Lấy user_id từ header X-User + user_id = request.headers.get("X-User") + if not user_id: + return jsonify({"error": "user_id is required"}), 400 + + address_id = request.headers.get("X-Fission-Params-UserAddrId") + if not address_id: + return jsonify({"error": "address_id is required"}), 400 + + # Kiểm tra xem địa chỉ có tồn tại trong cơ sở dữ liệu không + if not crud.exists_address_for_delete(address_id, user_id): + return jsonify({"error": "Address not found"}), 404 + + # Xóa địa chỉ từ cơ sở dữ liệu + response, status = crud.delete_address(address_id, user_id) + return jsonify(response), status + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +# def make_update_request(): +# try: +# # Bước 1: Nhận thông tin từ header và body +# user_id = request.headers.get("X-User") +# if not user_id: +# return jsonify({"errorCode": "user_id is required"}), 400 + +# address_id = request.headers.get("X-Fission-Params-UserAddrId") +# if not address_id: +# return jsonify({"errorCode": "address_id is required"}), 400 + +# request_data = request.get_json() # Dữ liệu cập nhật từ body +# # Bước 3: Kiểm tra nếu không có dữ liệu cần cập nhật +# if not request_data: # Nếu body rỗng hoặc None +# return jsonify({"errorCode": "No update provided"}), 400 + +# # Bước 4: Kiểm tra sự tồn tại của địa chỉ +# if not crud.exists_address_for_delete_or_update(address_id, user_id): +# return jsonify({"errorCode": "Address not found"}), 404 + +# # Bước 5: Lấy dữ liệu cũ của địa chỉ +# current_address = crud.get_address_by_id(address_id) + +# # Bước 6: So Sánh dữ liệu cũ và mới +# if current_address == request_data: +# return jsonify({"errorCode": "No changes detected"}), 400 + +# # Bước 7: Cập nhật địa chỉ +# response, status = crud.update_address( +# address_id, user_id, request_data) + +# if status != 200: +# return jsonify({"errorCode": "Update Failed"}), status + +# # Bước 6: Trả về kết quả cập nhật +# return jsonify({ +# "id": address_id, +# "user_id": user_id, +# "address": request_data.get("address", ""), +# "status": "updated" +# }), 200, +# except Exception as e: +# return jsonify({"errorCode": "INTERNAL_SERVER_ERROR", "detail": str(e)}), 500 diff --git a/apps/ailbl-user_address-insert-get.py b/apps/ailbl-user_address-insert-get.py index 6415cb5..917c711 100644 --- a/apps/ailbl-user_address-insert-get.py +++ b/apps/ailbl-user_address-insert-get.py @@ -1,8 +1,7 @@ import crud +import logging from flask import jsonify, request - -# from storage.minio_client import get_minio_client, check_existing_avatar_on_minio, upload_to_minio -ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"} +from filters import AddressPage def main(): @@ -32,18 +31,27 @@ def main(): def make_insert_request(): try: - user_id = request.headers.get("X-User") # Lay user_id tu header X-User - # Lay file tu form-data voi key la 'avatar' - file = request.files.get("avatar") - if not user_id or not file: - return jsonify({"error": "user_id or file is required"}), 400 - # Check mimetype(kieu du lieu cua file anh) - if file.mimetype not in ALLOWED_IMAGE_TYPES: - return jsonify( - {"error": "Invalid file type. Only JPG, PNG, GIF, WEBP are allowed."} - ), 400 - response, status = crud.update_or_create_avatar(user_id, file) + user_id = request.headers.get("X-User") # Lấy user_id từ header X-User + if not user_id: + return jsonify({"error": "user_id is required"}), 400 + + data = request.get_json() # Lấy dữ liệu từ body request + if not data: + return jsonify({"error": "Address data is required"}), 400 + + address = data.get("address") + if not address: + return jsonify({"error": "Address is required"}), 400 + + # Kiểm tra nếu địa chỉ đã tồn tại cho người dùng này + if crud.exists_address_for_post(user_id, address): + return jsonify({"error": "Address already exists for this user"}), 409 + + # Tạo mới địa chỉ người dùng + response, status = crud.create_address(user_id, data) + return jsonify(response), status + except Exception as e: return jsonify({"error": str(e)}), 500 @@ -53,7 +61,28 @@ def make_get_request(): user_id = request.headers.get("X-User") if not user_id: return jsonify({"error": "user_id is required"}), 400 - return crud.get_avatar_url(user_id) - # return jsonify(response), status + + # Lấy tham số filter và phân trang từ request + # Sử dụng AddressPage để lấy filter và phân trang + paging = AddressPage.from_request_queries() + print(paging) + + # Gọi hàm CRUD để lấy danh sách địa chỉ của user_id với phân trang và lọc + response = crud.filter_address(user_id, paging) + + return jsonify( + response, + ), 200 + + # Kiểm tra xem response có dữ liệu không + # if not response[0]: # Nếu không có dữ liệu + # return jsonify({"message": "No addresses found"}), 404 + + # return jsonify({ + # "data": response[0], # Dữ liệu (danh sách các địa chỉ) + # "status": response[1], # Mã trạng thái HTTP (200) + # "headers": response[2] # Headers CORS + # }), response[1], response[2] + except Exception as e: return jsonify({"error": str(e)}), 500 diff --git a/apps/crud.py b/apps/crud.py index 225daae..e425d77 100644 --- a/apps/crud.py +++ b/apps/crud.py @@ -1,65 +1,301 @@ import io from flask import Response -from helpers import S3_BUCKET, get_secret, s3_client +from helpers import init_db_connection from PIL import Image +import logging + +# Giả sử bạn đã cấu hình logger +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) -# Create&Update function to upload or update user avatar S3/Minio -def update_or_create_avatar(user_id: str, file): +def exists_address_for_post(user_id: str, address: str): + cursor = None # Khởi tạo cursor để đảm bảo không gặp lỗi khi vào finally + conn = None # Khởi tạo conn để đảm bảo không gặp lỗi khi vào finally try: - file_data = file.read() - # Bản chất là đường dẫn trong bucket + tên file = user_id - object_name = f"{get_secret('S3_PREFIX')}/{user_id}" - result = s3_client.put_object( - Bucket=S3_BUCKET, - Key=object_name, - Body=io.BytesIO(file_data), - ContentLength=len(file_data), - ContentType=file.content_type, - ) + conn = init_db_connection() + cursor = conn.cursor() - return result, 200 + cursor.execute(""" + SELECT 1 + FROM ailbl_user_address + WHERE user_id = %s AND address = %s + """, (user_id, address)) + + row = cursor.fetchone() # Nếu có kết quả, trả về dữ liệu 1 dòng + return row is not None # Nếu tồn tại, trả về True + except Exception as e: + logger.error(f"Database error checking address existence: {e}") + raise + finally: + if cursor: + cursor.close() + if conn: + conn.close() # Đảm bảo đóng kết nối sau khi xong + + +def create_address(user_id: str, data): + try: + conn = init_db_connection() + cursor = conn.cursor() + + query = """ + INSERT INTO ailbl_user_address (user_id, address, area_code, city_code, created, modified) + VALUES (%s, %s, %s, %s, NOW(), NOW()) + """ + # Lay cac truong tu data + address = data.get("address") + area_code = data.get("area_code") + city_code = data.get("city_code") + + # Thực thi câu truy vấn SQL để thêm địa chỉ + cursor.execute(query, (user_id, address, area_code, city_code)) + conn.commit() # Lưu thay đổi vào cơ sở dữ liệu + + return { + "user_id": user_id, + "address": address, + "area_code": area_code, + "city_code": city_code, + "status": "added" + }, 200 + except Exception as e: + logging.error(f"Error creating address: {e}") + return {"error": str(e)}, 500 + finally: + if cursor: + cursor.close() + if conn: + conn.close() + + +def filter_address(user_id: str, paging): + conn = None # Khởi tạo conn với giá trị None + cursor = None + try: + conn = init_db_connection() # Kết nối cơ sở dữ liệu + cursor = conn.cursor() + + # Xây dựng điều kiện lọc + conditions = ["user_id = %(user_id)s"] + values = {"user_id": user_id} # Điều kiện cơ bản cho user_id + + # Lọc theo address + if paging.filter.address: + conditions.append("LOWER(Address) LIKE %(address)s") + values["address"] = f"%{paging.filter.address.lower()}%" + + # Lọc theo area_code + if paging.filter.area_code: + conditions.append("AreaCode = %(area_code)s") + values["area_code"] = paging.filter.area_code + + # Lọc theo city_code + if paging.filter.city_code: + conditions.append("CityCode = %(city_code)s") + values["city_code"] = paging.filter.city_code + + # Lọc theo ngày tạo + if paging.filter.created_from: + conditions.append("Created >= %(created_from)s") + values["created_from"] = paging.filter.created_from + + if paging.filter.created_to: + conditions.append("Created <= %(created_to)s") + values["created_to"] = paging.filter.created_to + + # Lọc theo ngày sửa đổi + if paging.filter.modified_from: + conditions.append("Modified >= %(modified_from)s") + values["modified_from"] = paging.filter.modified_from + + if paging.filter.modified_to: + conditions.append("Modified <= %(modified_to)s") + values["modified_to"] = paging.filter.modified_to + + # Kết hợp điều kiện lọc + where_clause = " AND ".join(conditions) + if where_clause: + where_clause = "WHERE " + where_clause + + # Sắp xếp kết quả nếu có: + order_clause = "" + if paging.sortby: + direction = "ASC" if paging.asc else "DESC" + order_clause = f"ORDER BY {paging.sortby} {direction}" + + # Kết hợp truy vấn + sql = f""" + SELECT + id, + user_id, + address, + area_code, + city_code, + created, + modified, + COUNT(*) OVER() AS total + FROM ailbl_user_address + {where_clause} + {order_clause} + LIMIT %(limit)s OFFSET %(offset)s + """ + values["limit"] = paging.size + values["offset"] = paging.page * paging.size + + print("SQL Query: ", sql) # In ra câu truy vấn SQL + print("Values: ", values) # In ra các tham số + + cursor.execute(sql, values) # Thực thi câu truy vấn với các giá trị + + # Lấy tất cả các kết quả + addresses = cursor.fetchall() + print("Fetched addresses: ", addresses) # In ra kết quả + + # Nếu không có kết quả + if not addresses: + return {"message": "No addresses found"}, 404 + + address_list = [] + for address in addresses: + address_dict = { + "id": address[0], + "user_id": address[1], + "address": address[2], + "area_code": address[3], + "city_code": address[4], + "created": address[5], + "modified": address[6] + } + + # Kiểm tra xem có cột 'total' hay không + if len(address) > 7: + address_dict["total"] = address[7] + + address_list.append(address_dict) + + return address_list, 200 except Exception as e: return {"error": str(e)}, 500 + finally: + if conn: + conn.close() -def get_avatar_url(user_id: str): # Read function to get user avatar from S3/Minio +def exists_address_for_delete(address_id: str, user_id: str): try: - response = s3_client.get_object( - Bucket=S3_BUCKET, - Key=f"{get_secret('S3_PREFIX')}/{user_id}" - ) - # image_data = response["body"].read(content_type) - image_data = response['Body'].read() + # Kiểm tra xem địa chỉ có tồn tại cho user_id này không + conn = init_db_connection() + with conn.cursor() as cursor: + cursor.execute(""" + SELECT 1 + FROM ailbl_user_address + WHERE id = %s AND user_id = %s + """, (address_id, user_id)) - with Image.open(io.BytesIO(image_data)) as img: - fmt = img.format.lower() # ví dụ: 'jpeg', 'png', 'webp' - content_type = f"image/{'jpeg' if fmt == 'jpg' else fmt}" - # return Response( - # io.BytesIO(image_data), - # content_type=content_type, - # direct_passthrough=True, - # ) - - return Response( - image_data, - content_type=content_type, - direct_passthrough=True - ), 200 + row = cursor.fetchone() + return row is not None # Nếu có kết quả, địa chỉ tồn tại except Exception as e: - return {"error": str(e)}, 500 + logger.error(f"Database error checking address existence: {e}") + raise + finally: + if conn: + conn.close() # Đảm bảo kết nối được đóng -# Delete Function to delete user avatar from S3/Minio -def delete_avatar(user_id: str) -> dict: +def delete_address(address_id: str, user_id: str): try: - result = s3_client.delete_object( - Bucket=S3_BUCKET, - Key=f"{get_secret('S3_PREFIX')}/{user_id}" - ) - return result, 200 + conn = init_db_connection() + cursor = conn.cursor() + + # Xóa địa chỉ từ bảng ailbl_user_address + cursor.execute(""" + DELETE FROM ailbl_user_address + WHERE id = %s AND user_id = %s + """, (address_id, user_id)) + + conn.commit() # Lưu thay đổi vào cơ sở dữ liệu + return {"message": "Address deleted successfully"}, 200 + except Exception as e: + logger.error(f"Error deleting address: {e}") return {"error": str(e)}, 500 + finally: + if cursor: + cursor.close() + if conn: + conn.close() + + +def update_address(address_id, user_id, request_data): + try: + conn = init_db_connection() + cursor = conn.cursor() + + # Xây dựng câu lệnh UPDATE + update_query = """ + UPDATE ailbl_user_address + SET address = %s, area_code = %s, city_code = %s, modified = NOW() + WHERE id = %s AND user_id = %s + RETURNING id; + """ + cursor.execute(update_query, ( + request_data.get("address"), + request_data.get("area_code"), + request_data.get("city_code"), + address_id, + user_id + )) + + # Lấy kết quả sau khi thực hiện UPDATE + result = cursor.fetchone() + if result: + conn.commit() + return {"id": result[0], "status": "updated"}, 200 + else: + return {"error": "Address not found or not updated"}, 404 + + except Exception as e: + # conn.rollback() + # current_app.logger.error(f"[update_address] DB Error: {e}") + return {"error": "DATABASE_ERROR"}, 500 + + finally: + cursor.close() + conn.close() + + +# def get_address_by_id(address_id: str): Update chưa xong +# conn = None +# cursor = None +# try: +# conn = init_db_connection() +# with conn.cursor() as cursor: +# cursor.execute(""" +# SELECT address, area_code, city_code, created, modified +# FROM ailbl_user_address +# WHERE id = %s; +# """, (address_id,)) + +# row = cursor.fetchone() +# if row: +# return { +# "address": row[0], +# "area_code": row[1], +# "city_code": row[2], +# "created": row[3], +# "modified": row[4] +# } +# else: +# return None # Nếu không tìm thấy địa chỉ, trả về None + +# except Exception as e: +# return {"error": "DATABASE_ERROR"}, 500 +# finally: +# if cursor: +# cursor.close() +# if conn: +# conn.close() diff --git a/apps/filters.py b/apps/filters.py new file mode 100644 index 0000000..273ad54 --- /dev/null +++ b/apps/filters.py @@ -0,0 +1,55 @@ +import dataclasses +from typing import Optional +from flask import request +from helpers import str_to_bool + + +@dataclasses.dataclass +class AddressFilter: + address: Optional[str] = None + area_code: Optional[str] = None + city_code: Optional[str] = None + created_from: Optional[str] = None + created_to: Optional[str] = None + modified_from: Optional[str] = None + modified_to: Optional[str] = None + + @classmethod + def from_request_queries(cls) -> "AddressFilter": + return cls( + address=request.args.get("filter[address]"), + area_code=request.args.get("filter[area_code]"), + city_code=request.args.get("filter[city_code]"), + created_from=request.args.get("filter[created_from]"), + created_to=request.args.get("filter[created_to]"), + modified_from=request.args.get("filter[modified_from]"), + modified_to=request.args.get("filter[modified_to]"), + ) + + +@dataclasses.dataclass +class Page: + page: int = 0 + size: int = 10 + asc: bool = False + + @classmethod + def from_request_queries(cls) -> "Page": + return Page( + page=int(request.args.get("page", 0)), + size=int(request.args.get("size", 10)), + asc=request.args.get("asc", type=str_to_bool) or False + ) + + +@dataclasses.dataclass +class AddressPage(Page): + sortby: Optional[str] = None + filter: AddressFilter = dataclasses.field( + default_factory=AddressFilter.from_request_queries + ) + + @classmethod + def from_request_queries(cls) -> "AddressPage": + base = Page.from_request_queries() + return cls(**dataclasses.asdict(base), sortby=request.args.get("sortby")) diff --git a/apps/helpers.py b/apps/helpers.py index 27594f9..2833d75 100644 --- a/apps/helpers.py +++ b/apps/helpers.py @@ -1,35 +1,110 @@ +import datetime import logging -import boto3 -SECRET_NAME = "fission-ailbl-user-avatar-env" +import socket +import typing +import psycopg2 +from flask import current_app +from psycopg2.extras import LoggingConnection + + +CORS_HEADERS = { + "Content-Type": "application/json", +} +SECRET_NAME = "fission-ailbl-user-address-env" +CONFIG_NAME = "fission-eom-notification-config" K8S_NAMESPACE = "default" +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + + +def init_db_connection(): + db_host = get_secret("PG_HOST", "locahost") + db_port = int(get_secret("PG_PORT", 55432)) + + if not check_port_open(ip=db_host, port=db_port): + raise Exception( + f"Establishing A Database Connection. {db_host}:{db_port}") + + # options = get_secret("PG_DBSCHEMA") + # if options: + # options = f"-c search_path={options}" # if specific db schema + conn = psycopg2.connect( + database=get_secret("PG_DB", "postgres"), + user=get_secret("PG_USER", "postgres"), + password=get_secret("PG_PASS", "secret"), + host=get_secret("PG_HOST", "127.0.0.1"), + port=int(get_secret("PG_PORT", 5432)), + # options=options, + # cursor_factory=NamedTupleCursor, + connection_factory=LoggingConnection, + ) + conn.initialize(logger) + return conn + + +# def db_row_to_dict(cursor, row): +# record = {} +# for i, column in enumerate(cursor.description): +# data = row[i] +# if isinstance(data, datetime.datetime): +# data = data.isoformat() +# record[column.name] = data +# return record + +def db_row_to_dict(cursor, row): + record = {} + for i, column in enumerate(cursor.description): + data = row[i] + if isinstance(data, (datetime.datetime, datetime.date)): + data = data.isoformat() + record[column.name] = data + return record + + +def db_rows_to_array(cursor, rows): + return [db_row_to_dict(cursor, row) for row in rows] + + def get_current_namespace() -> str: try: with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f: namespace = f.read() - except: + except Exception as err: + current_app.logger.error(err) namespace = K8S_NAMESPACE return str(namespace) -def get_secret(key: str, default=None) -> str: +def get_secret(key: str, default=None): namespace = get_current_namespace() path = f"/secrets/{namespace}/{SECRET_NAME}/{key}" try: with open(path, "r") as f: return f.read() - except: + except Exception as err: + current_app.logger.error(path, err) return default -S3_BUCKET = get_secret("S3_BUCKET") -S3_PREFIX = get_secret("S3_PREFIX") +def check_port_open(ip: str, port: int, timeout: int = 30): + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(timeout) + result = s.connect_ex((ip, port)) + return result == 0 + except Exception as err: + current_app.logger.err(f"Check port open error: {err}") + return False -s3_client = boto3.client( - "s3", - endpoint_url=get_secret("S3_ENDPOINT_URL"), - aws_access_key_id=get_secret("S3_ACCESS_KEY_ID"), - aws_secret_access_key=get_secret("S3_SECRET_ACCESS_KEY"), - config=boto3.session.Config(signature_version="s3v4"), -) + +def str_to_bool(value: str | None) -> typing.Optional[bool]: + if value is None: + return None + val = value.strip().lower() + if val in ("true", "1", "yes"): + return True + if val in ("false", "0", "no"): + return False + return None diff --git a/apps/requirements.txt b/apps/requirements.txt index f922a61..1402bf5 100644 --- a/apps/requirements.txt +++ b/apps/requirements.txt @@ -1,6 +1,6 @@ -# Flask==3.1.0 -# psycopg2-binary==2.9.10 -# pydantic==2.11.3 -# minio==7.2.5 -# Pillow==10.4.0 -# boto3==1.35.70 \ No newline at end of file +Flask==3.1.0 +psycopg2-binary==2.9.10 +pydantic==2.11.3 +minio==7.2.5 +Pillow==10.4.0 +boto3==1.35.70 \ No newline at end of file diff --git a/apps/schemas.py b/apps/schemas.py index f223a07..a451518 100644 --- a/apps/schemas.py +++ b/apps/schemas.py @@ -1,33 +1,18 @@ from pydantic import BaseModel, Field from typing import Optional -from enum import IntEnum -class TagKind(IntEnum): - ProjectGroup = 1 - ProjectData = 2 - ProjectMember = 3 - ProjectDiscussionTopic = 4 - Project = 5 - Ticket = 6 +class UserAddressRequest(BaseModel): + address: str = Field(..., max_length=255) + area_code: Optional[str] = Field(None, max_length=10) + city_code: Optional[str] = Field(None, max_length=30) + district_code: Optional[str] = Field(None, max_length=30) + is_primary: Optional[bool] = Field(None, description="is primary address ") -class TagRequest(BaseModel): - tag: str = Field(..., max_length=128) - kind: TagKind - ref: Optional[str] = Field(default=None, max_length=36) - primary_color: Optional[str] = Field(default=None, max_length=8) - secondary_color: Optional[str] = Field(default=None, max_length=8) - - -class TagRequestUpdate(BaseModel): - tag: str = Field(..., max_length=128) - kind: TagKind - ref: Optional[str] = Field(default=None, max_length=36) - primary_color: Optional[str] = Field(default=None, max_length=8) - secondary_color: Optional[str] = Field(default=None, max_length=8) - - -class TagRefRequest(BaseModel): - ref: str = Field(..., max_length=64) - sub_ref: Optional[str] = Field(default=None, max_length=1024) +class UserAddressUpdateRequest(BaseModel): + address: Optional[str] = Field(..., max_length=255) + area_code: Optional[str] = Field(None, max_length=10) + city_code: Optional[str] = Field(None, max_length=30) + district_code: Optional[str] = Field(None, max_length=30) + is_primary: Optional[bool] = Field(None, description="is primary address ")