diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 8fd2cea..ce7ba38 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -6,23 +6,21 @@ on: - fast-api jobs: - docker: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ vars.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push - uses: docker/build-push-action@v4 - with: - context: . - push: true - tags: ${{ vars.DOCKERHUB_USERNAME }}/muslimpro-api:${{ vars.VERSION }} \ No newline at end of file + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: ${{ vars.DOCKERHUB_USERNAME }}/muslimpro-api:${{ vars.VERSION }} diff --git a/Dockerfile b/Dockerfile index 2f5419f..9ea0aa1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,4 +4,4 @@ COPY . /app WORKDIR /app RUN pip3 install -Ur requirements.txt -CMD ["gunicorn" , "router:app", "-w 4", "-k uvicorn.workers.UvicornWorker"] \ No newline at end of file +CMD python3 router.py \ No newline at end of file diff --git a/MuslimProScrapper/__init__.py b/MuslimProScrapper/__init__.py index 3107e10..d0e8f5d 100644 --- a/MuslimProScrapper/__init__.py +++ b/MuslimProScrapper/__init__.py @@ -1,3 +1,3 @@ from .main import Search from .model import Item, Response -from .const import CalculationMethod, AsrjuristicMethod \ No newline at end of file +from .const import CalculationMethod, AsrjuristicMethod diff --git a/MuslimProScrapper/const.py b/MuslimProScrapper/const.py index 700904f..bc2dee1 100644 --- a/MuslimProScrapper/const.py +++ b/MuslimProScrapper/const.py @@ -6,6 +6,7 @@ # Indonesian Based RAMADHAN_API_INDONESIA = "https://calendarific.com/holiday/indonesia/idul-fitri" + class CalculationMethod(Enum): DEFAULT = "Precalc" ALGERIAN_MINISTER_OF_RELIGIOUS_AND_WAKFS = "Algeria" @@ -25,11 +26,12 @@ class CalculationMethod(Enum): SHIA_ITHNA_ASHARI_JAFARI = "Jafari" SIHAT_KEMENAG_KEMENTERIAN_AGAMA_RI = "KEMENAG" TUNISIAN_MINISTRY_OF_RELIGIOUS_AFFAIRS = "Tunisia" - UAE_GENERAL_AUTHORITY_OF_ISLAMIC_AFFAIRS_AND_ENDOWMENTS = "AwqafUAE" + UAE_GENERAL_AUTHORITY_OF_ISLAMIC_AFFAIRS_AND_ENDOWMENTS = "AwqafUAE" UMM_AL_QURA_MAKKAH = "Makkah" UNIVERSITY_OF_TEHRAN = "Tehran" FEDERATION_OF_ISLAMIC_ASSOCIATIONS_IN_BASQUE_COUNTRY = "BASQUE" + class AsrjuristicMethod(Enum): STANDARD_SHAFI_MALIKI_HANBALI = "Standard" HANAFI = "Hanafi" diff --git a/requirements.txt b/requirements.txt index 25005bc..f4c8301 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,20 +9,15 @@ cssselect==1.2.0 fastapi==0.110.0 fastapi-cache2==0.2.1 frozenlist==1.4.1 -gunicorn==21.2.0 h11==0.14.0 -httptools==0.6.1 idna==3.6 lxml==5.1.0 multidict==6.0.5 -packaging==24.0 pendulum==3.0.0 pydantic==2.6.4 pydantic_core==2.16.3 pyquery==2.0.0 python-dateutil==2.9.0.post0 -python-dotenv==1.0.1 -PyYAML==6.0.1 redis==4.6.0 setuptools==68.2.2 six==1.16.0 @@ -32,7 +27,5 @@ time-machine==2.14.0 typing_extensions==4.10.0 tzdata==2024.1 uvicorn==0.28.0 -watchfiles==0.21.0 -websockets==12.0 wheel==0.41.2 yarl==1.9.4 diff --git a/router.py b/router.py index b64c58b..f1a8934 100644 --- a/router.py +++ b/router.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, Field -from fastapi import FastAPI, Request +from fastapi import FastAPI, Request, HTTPException from fastapi.responses import FileResponse, JSONResponse, RedirectResponse from fastapi.encoders import jsonable_encoder from fastapi.staticfiles import StaticFiles @@ -21,13 +21,14 @@ from MuslimProScrapper import Search, Response as MuslimProResp from MuslimProScrapper.const import AsrjuristicMethod, CalculationMethod + @asynccontextmanager async def lifespan(_): - redis = aioredis.from_url(f"redis://:{environ.get('REDIS_PASS')}@redis") + redis = aioredis.from_url(environ.get('REDIS_HOST')) FastAPICache.init(RedisBackend(redis), prefix="muslimproapi-cache") yield -app = FastAPI(title='MuslimPro_API', lifespan=lifespan) +app = FastAPI(title='MuslimPro API', lifespan=lifespan, version="2.0") app.mount("/static", StaticFiles(directory="static"), name="static") @@ -49,7 +50,7 @@ async def where_ip(session: ClientSession, ip_address: str) -> str: class ModelResponse(BaseModel): - location: str = Field(default="Location not found!!") + location: str = Field(default="") calculationMethod: str = Field(default="") asrjuristicMethod: str = Field(default="") praytimes: dict = Field(default=dict()) @@ -62,14 +63,29 @@ class BaseResponse(BaseModel): data: ModelResponse = Field(default=ModelResponse()) -@app.get('/favicon.ico') +class CustomException(Exception): + + def __init__(self, status_code: int, message: str) -> None: + self.body: BaseResponse = BaseResponse() + self.body.status_code = status_code + self.body.message = message + self.body.data.location = "" + + +@app.get('/favicon.ico', response_class=FileResponse, include_in_schema=False) def favicon() -> FileResponse: return FileResponse("static/favicon.ico") -@app.get('/', response_model=None) -async def get_location_based(request: Request) -> RedirectResponse | JSONResponse: - resp: BaseResponse = BaseResponse() +@app.get('/', response_class=RedirectResponse, responses={ + 307: {'description': "Will redirect into your specific location path"} +}) +async def get_location_based(request: Request) -> RedirectResponse: + """ + Root redirection based ip + + By Looking up your ip adress location, this will redirect you into the specific path based your location + """ try: async with ClientSession() as session: client_ip: str = request.client.host @@ -79,28 +95,34 @@ async def get_location_based(request: Request) -> RedirectResponse | JSONRespons return RedirectResponse(redirect_url) except (IndexError, KeyError): - resp.status_code = 404 - return JSONResponse(content=jsonable_encoder(resp), status_code=404) + raise CustomException( + status_code=404, message="Location was not found!!") except: - resp.message = "Something went wrong" - resp.status_code = 500 - resp.data.location = "" - return JSONResponse(content=jsonable_encoder(resp), status_code=500) + raise CustomException( + status_code=500, message="Something went wrong!?") -@app.get('/{query}', response_class=JSONResponse) +@app.get('/{query}', response_model=BaseResponse, responses={ + 200: {'description': "Will return the data, as the model", 'model': BaseResponse} +}) @cache(expire=86400) -async def main_app(query: str, calcMethod: str = "", asjurMethod: str = "") -> JSONResponse: +async def main_app(query: str, calcMethod: str = CalculationMethod.DEFAULT.name, asjurMethod: str = AsrjuristicMethod.STANDARD_SHAFI_MALIKI_HANBALI.name) -> JSONResponse: + """ + Main Appplication + + - Query\n + :query -> Pass this your location, to lookup the data + - Parameters\n + :calcMethod[Optional] -> Put calculation method(read source code)\n + :asjurMethod[Optional] -> Put asrjuristic method(read source code) + """ resp: BaseResponse = BaseResponse() - status: int = 200 - par1, par2 = calcMethod.replace( - ' ', '_').upper(), asjurMethod.replace(' ', '_').upper() + par1 = calcMethod.replace(' ', '_').upper() + par2 = asjurMethod.replace(' ', '_').upper() + calc = CalculationMethod[par1] + asjur = AsrjuristicMethod[par2] - calc = CalculationMethod[par1] if hasattr( - CalculationMethod, par1) else CalculationMethod.DEFAULT - asjur = AsrjuristicMethod[par2] if hasattr( - AsrjuristicMethod, par2) else AsrjuristicMethod.STANDARD_SHAFI_MALIKI_HANBALI async with ClientSession() as session: api = Search(session=session, calculation=calc, asrjuristic=asjur) @@ -114,7 +136,8 @@ async def main_app(query: str, calcMethod: str = "", asjurMethod: str = "") -> J city_name: str = location.get('city_name', '') country_name: str = location.get('country_name', '') - resp.data.location = f"{city_name.title()}, {country_name.title()}" + resp.data.location = f"{city_name.title()}, { + country_name.title()}" resp.data.calculationMethod = data.calculationMethod resp.data.asrjuristicMethod = data.asrjuristicMethod for i in iter(data.raw): @@ -128,20 +151,25 @@ async def main_app(query: str, calcMethod: str = "", asjurMethod: str = "") -> J resp.data.ramadhan = "Currently only supported in Indonesia" except (IndexError, KeyError): - status = 404 - resp.status_code = status + raise CustomException( + status_code=404, message="Location was not found!!") except: - status = 500 - resp.message = "Something went wrong" - resp.status_code = status - resp.data.location = "" + raise CustomException( + status_code=500, message="Something went wrong!?") - return JSONResponse(content=jsonable_encoder(resp), status_code=status) + return JSONResponse(content=jsonable_encoder(resp)) -@app.exception_handler(400) @app.exception_handler(404) -async def docs(e): - return RedirectResponse('https://github.com/lleans/Muslim-Pro-Scrapper') +@app.exception_handler(500) +@app.exception_handler(422) +@app.exception_handler(400) +async def error_handling(_, exec: Exception) -> JSONResponse: + if isinstance(exec, CustomException): + return JSONResponse(content=jsonable_encoder(exec.body), status_code=exec.body.status_code) + + return RedirectResponse(url='/docs') -uvicorn.run("router:app", port=int(environ.get('PORT')) or 8000, workers=4) \ No newline at end of file +if __name__ == "__main__": + uvicorn.run("router:app", host="0.0.0.0", + port=int(environ.get('PORT')) or 8000, log_level="info", workers=3) diff --git a/setup.py b/setup.py index 1711ab5..ef5099a 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ import setuptools -requirements = [requirement.strip() for requirement in open('requirements.txt', 'r', encoding='utf-8').readlines()] +requirements = [requirement.strip() for requirement in open( + 'requirements.txt', 'r', encoding='utf-8').readlines()] with open("README.md", "r", encoding='utf-8') as fh: long_description = fh.read() @@ -21,4 +22,4 @@ "Operating System :: OS Independent", ], install_requires=requirements, python_requires='>=3.10', -) \ No newline at end of file +)