From 18643f320790fd2d4dcc7c60641c229d81369d89 Mon Sep 17 00:00:00 2001 From: will Date: Thu, 1 Feb 2024 19:51:26 +0800 Subject: [PATCH] feat: add holiday check --- .env.example | 34 +++++++++++++++++++++++++++++++++- .github/workflows/ci-cd.yml | 3 +++ config.py | 3 +++ database.py | 2 +- dependencies.py | 4 ++-- main.py | 2 +- service.py | 30 ++++++++++++++++++++++++++++-- utils.py => utils/auth.py | 1 + utils/decorators.py | 16 ++++++++++++++++ jwt.py => utils/jwt.py | 0 10 files changed, 88 insertions(+), 7 deletions(-) rename utils.py => utils/auth.py (99%) create mode 100644 utils/decorators.py rename jwt.py => utils/jwt.py (100%) diff --git a/.env.example b/.env.example index 2ef6488..2f58445 100644 --- a/.env.example +++ b/.env.example @@ -8,4 +8,36 @@ DB_USER= DB_PASS= DB_NAME= WORKDAY_CUT_OFF_TIME= -MINIMUM_WORKING_HOURS= \ No newline at end of file +MINIMUM_WORKING_HOURS= +HOLIDAYS_API_URL= +HOLIDAYS_API_KEY= +COUNTRY= + + + +3. Create a Workload Identity on Google Cloud with gcloud +export GITHUB_REPO=Will413028/attendance-system-fastapi + +export PROJECT_ID=evident-airline-412703 + +export SERVICE_ACCOUNT=github-actions-service-account + +export WORKLOAD_IDENTITY_POOL=gh-pool + +export WORKLOAD_IDENTITY_PROVIDER=gh-provider + + +echo $WORKLOAD_IDENTITY_PROVIDER_LOCATION + +projects/227244282831/locations/global/workloadIdentityPools/gh-pool/providers/gh-provider + + +echo $SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com + +github-actions-service-account@evident-airline-412703.iam.gserviceaccount.com + +test +DATABASE_URL=mysql+pymysql://myuser:12345678@localhost:3306/attendance-system + +google sql +DATABASE_URL=mysql+pymysql://root:9KTS43ChtahzE@104.199.155.119:3306/attendance-system diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 0ae5264..d9f27c1 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -24,6 +24,9 @@ jobs: DB_NAME: ${{ secrets.DB_NAME }} WORKDAY_CUT_OFF_TIME: ${{ secrets.WORKDAY_CUT_OFF_TIME }} MINIMUM_WORKING_HOURS: ${{ secrets.MINIMUM_WORKING_HOURS }} + HOLIDAYS_API_URL: ${{ secrets.HOLIDAYS_API_URL }} + HOLIDAYS_API_KEY: ${{ secrets.HOLIDAYS_API_KEY }} + COUNTRY: ${{ secrets.COUNTRY }} steps: - name: Checkout repository uses: actions/checkout@v4 diff --git a/config.py b/config.py index ec20782..0508e35 100644 --- a/config.py +++ b/config.py @@ -13,6 +13,9 @@ class Settings(BaseSettings): db_name: str workday_cut_off_time: str minimum_working_hours: int + holidays_api_url: str + holidays_api_key: str + country: str class Config: env_file = ".env" diff --git a/database.py b/database.py index 7df39f0..dd82186 100644 --- a/database.py +++ b/database.py @@ -1,7 +1,7 @@ from sqlalchemy import create_engine, DateTime, Integer from sqlalchemy.orm import sessionmaker, DeclarativeBase, mapped_column from sqlalchemy.sql import func -from google.cloud.sql.connector import Connector +from google.cloud.sql.connector import Connector, create_async_connector from config import Settings diff --git a/dependencies.py b/dependencies.py index e34b311..81785b3 100644 --- a/dependencies.py +++ b/dependencies.py @@ -1,7 +1,7 @@ from fastapi import Depends, HTTPException, status from sqlalchemy.orm import Session -import models, schemas, utils, service +import models, schemas, utils.auth as auth, service from database import get_db @@ -18,6 +18,6 @@ def authenticate_user(data: schemas.LoginInput, db: Session = Depends(get_db)) - if not db_user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='User not found') - if not utils.verify_password(data.password, db_user.password): + if not auth.verify_password(data.password, db_user.password): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid password or email') return db_user diff --git a/main.py b/main.py index b03f8d7..8f09f8c 100644 --- a/main.py +++ b/main.py @@ -10,7 +10,7 @@ import models import schemas import service -import jwt +from utils import jwt from config import Settings from database import get_db diff --git a/service.py b/service.py index 599b36b..a8197bb 100644 --- a/service.py +++ b/service.py @@ -1,5 +1,6 @@ from typing import Optional from datetime import datetime, date, timedelta +import requests from fastapi import HTTPException, status from sqlalchemy.orm import Session @@ -7,7 +8,8 @@ import models import schemas -import utils +from utils import auth +from utils.decorators import cache_holiday_check from config import Settings @@ -48,7 +50,7 @@ def get_all_attendance_records(db: Session, attendance_type: Optional[str] = Non def create_user(db: Session, user: schemas.UserCreateInput): - user.password = utils.get_password_hash(user.password) + user.password = auth.get_password_hash(user.password) db_user = models.User(**user.model_dump()) db.add(db_user) @@ -100,6 +102,9 @@ def create_attendance(db: Session, user_id: schemas.AttendanceRecordUpdateInput, workday = get_workday(current_time, config.workday_cut_off_time) + if is_holiday(workday, config.country, config.holidays_api_key, config.holidays_api_url): + raise HTTPException(status_code=400, detail="Cannot record attendance on holidays") + today_attendance_record = get_user_attendance_by_workday(db, user_id, workday) if today_attendance_record: @@ -176,3 +181,24 @@ def is_leave_early(time_in: datetime, minimum_working_hours: int, current_time: working_hours = (current_time - time_in).total_seconds() / 3600 return working_hours < minimum_working_hours + + +@cache_holiday_check +def is_holiday(date: date, country: str, holidays_api_key: str, holidays_api_url: str) -> bool: + params = { + "api_key": holidays_api_key, + "country": country, + "year": date.year, + "month": date.month, + "day": date.day, + } + try: + response = requests.get(holidays_api_url, params=params) + response.raise_for_status() + data = response.json() + holidays = data.get('response', {}).get('holidays', []) + + return len(holidays) > 0 + except requests.RequestException as e: + print(e) + return False diff --git a/utils.py b/utils/auth.py similarity index 99% rename from utils.py rename to utils/auth.py index c0ce18b..26d0106 100644 --- a/utils.py +++ b/utils/auth.py @@ -1,5 +1,6 @@ from passlib.context import CryptContext + pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") diff --git a/utils/decorators.py b/utils/decorators.py new file mode 100644 index 0000000..4eaa600 --- /dev/null +++ b/utils/decorators.py @@ -0,0 +1,16 @@ +from functools import wraps + + +def cache_holiday_check(func): + holiday_cache = {} + + @wraps(func) + def wrapper_check_holiday(date, *args, **kwargs): + if date in holiday_cache: + print('has ') + + return holiday_cache[date] + is_holiday = func(date, *args, **kwargs) + holiday_cache[date] = is_holiday + return is_holiday + return wrapper_check_holiday \ No newline at end of file diff --git a/jwt.py b/utils/jwt.py similarity index 100% rename from jwt.py rename to utils/jwt.py