├── docs ├── .nojekyll ├── docs │ ├── usage.md │ ├── docker.md │ ├── introduction.md │ ├── usage_example.md │ ├── installation.md │ ├── redis_activation.md │ └── security.md ├── _media │ ├── icon.png │ ├── faviconV2.png │ └── logo.svg ├── _sidebar.md ├── _coverpage.md ├── README.md └── index.html ├── models ├── __init__.py └── models.py ├── tests ├── __init__.py └── test_main.py ├── controllers ├── __init__.py └── mawaqitController.py ├── scraping ├── __init__.py └── script.py ├── .gitignore ├── Dockerfile ├── requirements.txt ├── config ├── redisClient.py ├── settings.py └── auth.py ├── docker-compose.yaml ├── .env.example ├── main.py ├── .github └── workflows │ ├── python-app.yml │ └── docker-image.yml ├── LICENSE.md └── README.md /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/docs/usage.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /controllers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/docs/docker.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scraping/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .pytest_cache 3 | __pycache__ 4 | poetry.lock 5 | pyproject.toml -------------------------------------------------------------------------------- /docs/_media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrsofiane/mawaqit-api/HEAD/docs/_media/icon.png -------------------------------------------------------------------------------- /docs/_media/faviconV2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrsofiane/mawaqit-api/HEAD/docs/_media/faviconV2.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.6-alpine3.14 2 | 3 | WORKDIR /app 4 | 5 | COPY . /app/ 6 | 7 | RUN pip install -r requirements.txt 8 | 9 | CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | uvicorn==0.37.0 2 | requests==2.32.5 3 | fastapi==0.118.0 4 | httpx==0.28.1 5 | pytest==8.4.2 6 | beautifulsoup4==4.14.2 7 | redis==5.0.1 8 | python-dotenv==1.1.1 9 | slowapi==0.1.9 10 | pydantic-settings==2.11.0 -------------------------------------------------------------------------------- /config/redisClient.py: -------------------------------------------------------------------------------- 1 | import os 2 | from redis import Redis 3 | from config.settings import settings 4 | 5 | # Conditionally initialize Redis client 6 | if settings.ENABLE_REDIS: 7 | redisClient = Redis.from_url(settings.REDIS_URI) 8 | else: 9 | redisClient = None -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - Getting Started 2 | 3 | - [Introduction](/docs/introduction.md) 4 | - [Installation](/docs/installation.md) 5 | - [Security & Authentication](/docs/security.md) 6 | - [Activating Redis](/docs/redis_activation.md) 7 | 8 | - Usage Examples 9 | - [API Usage Example](/docs/usage_example.md) 10 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | mawaqit-api: 5 | image: mrsofiane/mawaqit-api:latest 6 | ports: 7 | - "80:80" 8 | depends_on: 9 | - mawaqit-api-redis 10 | env_file: 11 | - .env 12 | 13 | mawaqit-api-redis: 14 | image: redis:alpine3.18 15 | ports: 16 | - "6379:6379" 17 | -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ![logo](_media/icon.png) 4 | 5 | # Mawaqit Api v1.2.0 6 | 7 | > Rest API for fetching prayer times from Mawaqit.net. 8 | 9 | - Fast and reliable 10 | - Easy integration 11 | - Returns prayer times in JSON format 12 | 13 | [GitHub](https://github.com/mrsofiane/mawaqit-api/) 14 | [Get Started](#Mawaqit-api) 15 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Authentication (optional) 2 | # Set to true to enable bearer token authentication 3 | # If enabled, BEARER_TOKEN must be set 4 | ENABLE_AUTH=False 5 | BEARER_TOKEN=your_secure_bearer_token_here # Generate a secure token. Example: openssl rand -hex 32 6 | 7 | # Rate Limiting Configuration 8 | # Format: "number/time_unit" (e.g., "100/minute", "1000/hour") 9 | # Default: 60 requests per minute per IP address 10 | RATE_LIMIT=60/minute 11 | 12 | # Redis Configuration (optional) 13 | # Set to True if you want to enable Redis caching 14 | ENABLE_REDIS=False 15 | REDIS_URI=redis://localhost:6379 16 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | from fastapi import FastAPI 3 | from slowapi import Limiter, _rate_limit_exceeded_handler 4 | from slowapi.util import get_remote_address 5 | from slowapi.middleware import SlowAPIMiddleware 6 | from slowapi.errors import RateLimitExceeded 7 | from controllers.mawaqitController import router as mawaqitRouter 8 | from config.settings import settings 9 | 10 | def create_app() -> FastAPI: 11 | app = FastAPI(title='Mawaqit Api', debug=False, read_root="/") 12 | 13 | if settings.ENABLE_REDIS: 14 | storage_uri = settings.REDIS_URI 15 | limiter = Limiter(key_func=get_remote_address, default_limits=[settings.RATE_LIMIT], storage_uri=storage_uri) 16 | else: 17 | limiter = Limiter(key_func=get_remote_address, default_limits=[settings.RATE_LIMIT]) 18 | 19 | app.state.limiter = limiter 20 | app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) 21 | app.add_middleware(SlowAPIMiddleware) 22 | 23 | return app 24 | 25 | app = create_app() 26 | app.include_router(router=mawaqitRouter) -------------------------------------------------------------------------------- /docs/docs/introduction.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | Welcome to the Mawaqit API documentation! 4 | 5 | Mawaqi Api is a Rest API for mawaqit.net using the FastAPI framework. The mawaqit.net website provides prayer times for more than 8000 mosques around the world. The idea behind this API is to create a web API that utilizes the mawaqit website as a data source to fetch prayer times and return them in JSON format with the minimum necessary information. 6 | 7 | By using this API, developers can easily integrate prayer time functionality into their frontend web applications, mobile apps, or other software projects. Instead of manually scraping HTML from the mawaqit.net website, developers can simply make requests to the API and receive well-structured JSON responses, streamlining the development process. 8 | 9 | Furthermore, by using this API, developers have the flexibility to self-host the solution, providing them with full control over the data and ensuring reliability and scalability for their applications. 10 | 11 | [Next Page: Installation](/docs/installation.md) 12 | -------------------------------------------------------------------------------- /config/settings.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings, SettingsConfigDict 2 | from pydantic import model_validator, ValidationError 3 | from typing import Optional 4 | import sys 5 | 6 | class Settings(BaseSettings): 7 | model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") 8 | 9 | RATE_LIMIT : str = "60/minute" 10 | 11 | # REDIS 12 | ENABLE_REDIS: bool = False 13 | REDIS_URI: str = "redis://localhost:6379" 14 | 15 | # Authentication 16 | ENABLE_AUTH: bool = False 17 | BEARER_TOKEN: Optional[str] = None 18 | 19 | @model_validator(mode="after") 20 | def _check_auth(self): 21 | if self.ENABLE_AUTH and not self.BEARER_TOKEN: 22 | raise ValueError( 23 | "ENABLE_AUTH is set to true but BEARER_TOKEN environment variable is not set. " 24 | "Please configure BEARER_TOKEN in your .env file." 25 | ) 26 | return self 27 | 28 | try: 29 | settings = Settings() 30 | except ValidationError as e: 31 | print(e, file=sys.stderr) 32 | sys.exit(1) 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | name: Mawaqit API 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Python 3.9 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: "3.9" 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install flake8 pytest 27 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 28 | - name: Lint with flake8 29 | run: | 30 | # stop the build if there are Python syntax errors or undefined names 31 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 32 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 33 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 34 | - name: Test with pytest 35 | run: | 36 | pytest -------------------------------------------------------------------------------- /models/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, HttpUrl 2 | from typing import Optional 3 | from datetime import datetime 4 | 5 | class PrayerTimes(BaseModel): 6 | fajr: str 7 | sunrise: str 8 | dohr: str 9 | asr: str 10 | maghreb: str 11 | icha: str 12 | 13 | class IqamaPrayerTimes(BaseModel): 14 | fajr: str 15 | dohr: str 16 | asr: str 17 | maghreb: str 18 | icha: str 19 | 20 | 21 | class Announcement(BaseModel): 22 | id: int 23 | uuid: str 24 | title: str 25 | content: Optional[str] = None 26 | image: Optional[HttpUrl] = None 27 | video: Optional[HttpUrl] = None 28 | startDate: Optional[datetime] = None 29 | endDate: Optional[datetime] = None 30 | updated: datetime 31 | duration: Optional[int] = None 32 | isMobile: bool 33 | isDesktop: bool 34 | 35 | class MosqueServices(BaseModel): 36 | womenSpace: bool 37 | janazaPrayer: bool 38 | aidPrayer: bool 39 | childrenCourses: bool 40 | adultCourses: bool 41 | ramadanMeal: bool 42 | handicapAccessibility: bool 43 | ablutions: bool 44 | parking: bool 45 | 46 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sofiane Louchene 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docker Image to Docker Hub 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # only run when you push a version tag, e.g. v1.0.0 7 | 8 | jobs: 9 | build-and-push: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Log in to Docker Hub 17 | uses: docker/login-action@v3 18 | with: 19 | username: ${{ secrets.DOCKERHUB_USERNAME }} 20 | password: ${{ secrets.DOCKERHUB_TOKEN }} 21 | 22 | - name: Extract version from tag 23 | id: vars 24 | run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 25 | 26 | - name: Build Docker image 27 | run: | 28 | docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/mawaqit-api:${{ env.VERSION }} . 29 | docker tag ${{ secrets.DOCKERHUB_USERNAME }}/mawaqit-api:${{ env.VERSION }} ${{ secrets.DOCKERHUB_USERNAME }}/mawaqit-api:latest 30 | 31 | - name: Push Docker image 32 | run: | 33 | docker push ${{ secrets.DOCKERHUB_USERNAME }}/mawaqit-api:${{ env.VERSION }} 34 | docker push ${{ secrets.DOCKERHUB_USERNAME }}/mawaqit-api:latest 35 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Mawaqit Api 2 | 3 | [![GitHub Repo stars](https://img.shields.io/github/stars/mrsofiane/mawaqit-api?style=flat)](https://github.com/mrsofiane/mawaqit-api/stargazers) 4 | ![GitHub Tag](https://img.shields.io/github/v/tag/mrsofiane/mawaqit-api) 5 | ![GitHub License](https://img.shields.io/github/license/mrsofiane/mawaqit-api) 6 | ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/mrsofiane/mawaqit-api/python-app.yml) 7 | [![GitHub release](https://img.shields.io/github/release/mrsofiane/mawaqit-api.svg)](https://github.com/mrsofiane/mawaqit-api/releases) 8 | 9 | ## Introduction 10 | 11 | > Welcome to the Mawaqit API documentation! 12 | 13 | Mawaqi Api is a Rest Api for [mawaqit.net](https://mawaqit.net) using FastApi framework, the mawaqit.net website gives you the prayer times for more than 8000 mosques around the world, the idea behind this api is to create an api web app that uses the mawaqit website as data source to fetch prayer times and return them in json with the minimum information needed, the current website is using php so it's returning the whole html every get request. 14 | 15 | If you're new to the Mawaqit API, this documentation will guide you through getting started, installation, usage, and more. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mawaqit Api 2 | 3 | [![GitHub Repo stars](https://img.shields.io/github/stars/mrsofiane/mawaqit-api?style=flat)](https://github.com/mrsofiane/mawaqit-api/stargazers) 4 | ![GitHub Tag](https://img.shields.io/github/v/tag/mrsofiane/mawaqit-api) 5 | ![GitHub License](https://img.shields.io/github/license/mrsofiane/mawaqit-api) 6 | ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/mrsofiane/mawaqit-api/python-app.yml) 7 | [![GitHub release](https://img.shields.io/github/release/mrsofiane/mawaqit-api.svg)](https://github.com/mrsofiane/mawaqit-api/releases) 8 | 9 | Mawaqi Api is a Rest Api for [mawaqit.net](https://mawaqit.net) using FastApi framework, 10 | the mawaqit.net website gives you the prayer times for more than 8000 mosques around the world, 11 | the idea behind this api is to create an api web app that uses the mawaqit website as data source 12 | to fetch prayer times and return them in json with the minimum information needed, 13 | the current website is using php so it's returning the whole html every get request. 14 | 15 | ## Documentation 16 | 17 | Find the installation and usage documentation [here](https://mrsofiane.me/mawaqit-api). 18 | 19 | ## License 20 | 21 | [MIT](https://github.com/mrsofiane/mawaqit-api/blob/main/LICENSE.md) 22 | -------------------------------------------------------------------------------- /config/auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | Bearer token authentication using FastAPI security dependencies 3 | """ 4 | from fastapi import Depends, HTTPException, status 5 | from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials 6 | 7 | from config.settings import settings 8 | security = HTTPBearer(auto_error=False) 9 | 10 | def get_bearer_token() -> str: 11 | """Get the expected bearer token from environment variables""" 12 | auth_enabled = settings.ENABLE_AUTH 13 | if not auth_enabled: 14 | return None 15 | 16 | bearer_token = settings.BEARER_TOKEN 17 | return bearer_token 18 | 19 | def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> str: 20 | """Verify bearer token from Authorization header.""" 21 | expected_token = get_bearer_token() 22 | 23 | if expected_token is None: 24 | return None 25 | 26 | if credentials is None: 27 | raise HTTPException( 28 | status_code=status.HTTP_401_UNAUTHORIZED, 29 | detail="Missing Authorization header", 30 | headers={"WWW-Authenticate": "Bearer"}, 31 | ) 32 | 33 | if credentials.credentials != expected_token: 34 | raise HTTPException( 35 | status_code=status.HTTP_401_UNAUTHORIZED, 36 | detail="Invalid bearer token", 37 | headers={"WWW-Authenticate": "Bearer"}, 38 | ) 39 | 40 | return credentials.credentials 41 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 12 | 13 | 17 | 34 | 35 | 36 |
37 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /docs/docs/usage_example.md: -------------------------------------------------------------------------------- 1 | ## Usage Examples 2 | 3 | In the following examples, the mosque ID used is `assalam-argenteuil`. 4 | 5 | To demonstrate how to use the Mawaqit API, here are some usage examples: 6 | 7 | Replace `{masjid_id}` with the ID of the mosque you're interested in. You can find the mosque ID on the [Mawaqit website](https://mawaqit.net). 8 | 9 | The full API documentation is available at `localhost:8000/docs`. 10 | 11 | 1. **Retrieve Prayer Times for a Specific Mosque:** 12 | 13 | - **Request:** 14 | 15 | ```http 16 | GET /api/v1/{masjid_id}/prayer-times 17 | ``` 18 | 19 | - **Example:** 20 | 21 | ```http 22 | GET /api/v1/assalam-argenteuil/prayer-times 23 | ``` 24 | 25 | - **Result:** 26 | ```json 27 | { 28 | "fajr": "05:31", 29 | "sunrise": "06:45", 30 | "dohr": "13:02", 31 | "asr": "16:22", 32 | "maghreb": "19:13", 33 | "icha": "20:24" 34 | } 35 | ``` 36 | 37 | 2. **Retrieve Month Calendar of Prayer Times:** 38 | 39 | - **Request:** 40 | 41 | ```http 42 | GET /api/v1/{masjid_id}/calendar/{month_number} 43 | ``` 44 | 45 | - **Example:** 46 | 47 | ```http 48 | GET /api/v1/assalam-argenteuil/calendar/5 49 | ``` 50 | 51 | - **Result:** 52 | 53 | ```json 54 | [ 55 | { 56 | "fajr": "05:04", 57 | "sunrise": "06:30", 58 | "dohr": "13:53", 59 | "asr": "17:48", 60 | "maghreb": "21:10", 61 | "icha": "22:33" 62 | }, 63 | { 64 | ... 65 | }, 66 | { 67 | "fajr": "04:10", 68 | "sunrise": "05:52", 69 | "dohr": "13:53", 70 | "asr": "18:03", 71 | "maghreb": "21:48", 72 | "icha": "23:28" 73 | } 74 | ] 75 | ``` 76 | 77 | These examples demonstrate how to use the Mawaqit API endpoints to retrieve prayer times data for specific mosques, calendars for the year and month, etc. Adjust the `{masjid_id}` and `{month_number}` placeholders with the actual IDs and numbers as needed. 78 | -------------------------------------------------------------------------------- /docs/docs/installation.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | There are multiple ways to install and deploy the Mawaqi API depending on your preferences and requirements. 4 | 5 | 1. **Using Source Code and Running Python:** 6 | 7 | - Clone the repository from [GitHub](https://github.com/mrsofiane/mawaqit-api). 8 | - Ensure you have Python installed on your system (version 3.8 or higher). 9 | - Navigate to the project directory. 10 | - Create virtual environment `python -m venv env` or `python3 -m venv env`. 11 | - Activate the virtual environment `source env/bin/activate`. 12 | - Install dependencies using pip: `pip install -r requirements.txt` or `pip3 install -r requirements.txt`. 13 | - Run the API using the following command: `uvicorn main:app --host 0.0.0.0 --port 8000`. 14 | - The API will be accessible at `http://localhost:8000`. 15 | 16 | 2. **Using prebuilt Docker Image:** 17 | 18 | - Ensure you have Docker installed on your system. 19 | - Pull the prebuilt Docker image from Docker Hub: `docker pull mrsofiane/mawaqit-api:latest`. 20 | - Run the Docker container: `docker run -d --name mawaqit-api -p 8000:80 mrsofiane/mawaqit-api:latest`. 21 | - The API will be accessible at `http://localhost:8000`. 22 | 23 | 3. **Using Docker to Build Image from Source:** 24 | 25 | - Ensure you have Docker installed on your system. 26 | - Clone the repository from [GitHub](https://github.com/mrsofiane/mawaqit-api). 27 | - Navigate to the project directory. 28 | - Build the Docker image using the provided Dockerfile: `docker build -t mawaqi-api .`. 29 | - Run the Docker container: `docker run -d --name mawaqit-api -p 8000:80 mawaqi-api`. 30 | - The API will be accessible at `http://localhost:8000`. 31 | 32 | Choose the installation method that best suits your environment and preferences. 33 | 34 | > **⚠️ Important:** If you are **self-hosting** this API, it is **highly recommended to enable bearer token authentication** to protect your instance. See the [Security & Authentication](/docs/security.md) guide for details. 35 | 36 | **API Documentation:** 37 | 38 | You can find the API documentation at the path `/docs` relative to your API's base URL. It's an OpenAPI documentation generated automatically from FastAPI. 39 | 40 | ## Next Steps 41 | 42 | - [Security & Authentication Setup](/docs/security.md) 43 | - [Activating Redis](/docs/redis_activation.md) 44 | -------------------------------------------------------------------------------- /docs/docs/redis_activation.md: -------------------------------------------------------------------------------- 1 | ## Installing and Activating Redis 2 | 3 | If you wish to use Redis for caching and improving response times, you can activate it by following these steps: 4 | 5 | 1. **Ensure Redis is Installed:** 6 | 7 | - Before proceeding, ensure that Redis is installed on your system or a remote server. You can download and install Redis from the [official website](https://redis.io/download). 8 | - If you prefer to use Docker, you can run Redis in a Docker container: 9 | ```bash 10 | docker run -d --name mawaqit-redis -p 6379:6379 redis:alpine3.18 11 | ``` 12 | 13 | 2. **With Python:** 14 | 15 | - Before running the API, ensure that Redis is installed and running on your system or a remote server. 16 | - Set the environment variables for Redis host and port: 17 | ```bash 18 | export REDIS_URI=your_redis_uri 19 | export ENABLE_REDIS=true 20 | ``` 21 | Replace `your_redis_uri` with the appropriate values for your Redis uri. 22 | - Start the API using the following command: 23 | ```bash 24 | uvicorn main:app --host 0.0.0.0 --port 8000 25 | ``` 26 | - The API will be accessible at `http://localhost:8000`. 27 | 28 | 3. **With Docker:** 29 | 30 | - If you're building the Docker image locally, ensure that Redis is installed and running on your system or a remote server. 31 | - Set the environment variables when running the Docker container: 32 | ```bash 33 | docker run -d --name mawaqi-api-with-redis \ 34 | -e REDIS_URI=your_redis_uri \ 35 | -e ENABLE_REDIS=true \ 36 | -p 8000:80 mawaqi-api 37 | ``` 38 | - This command runs the Docker container named `mawaqi-api-with-redis` with the specified environment variables. Ensure to replace `your_redis_uri` with your Redis uri. 39 | - The API will be accessible at `http://localhost:8000`. 40 | 41 | ## Docker Compose Configuration 42 | 43 | If you prefer to manage your Mawaqi API and Redis containers together using Docker Compose, you can use the existing `docker-compose.yml` file in your project directory. 44 | 45 | 1. **Navigate to Your Project Directory**: 46 | 47 | Open a terminal or command prompt and navigate to the directory where your `docker-compose.yml` file is located. 48 | 49 | 2. **Run Docker Compose**: 50 | 51 | Execute the following command to start both the Mawaqi API and Redis containers using Docker Compose: 52 | 53 | ```bash 54 | docker-compose up -d 55 | ``` 56 | 57 | By following these steps, you can activate Redis for caching in your Mawaqi API deployment, enhancing its performance. 58 | -------------------------------------------------------------------------------- /controllers/mawaqitController.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends 2 | from fastapi.encoders import jsonable_encoder 3 | 4 | from typing import List 5 | 6 | import scraping.script as script 7 | import models.models as models 8 | from config.auth import verify_token 9 | 10 | router = APIRouter(prefix="/api/v1") 11 | 12 | @router.get("/", summary="Greetings") 13 | def read_root(): 14 | return {"Greetings": "Hello and Welcome to this Api, this api use the mawaqit.net as data source of prayers time in more than 8000 masjid, this api can be used to fetch data in json, you can find our docs on /docs. "} 15 | 16 | @router.get("/{masjid_id}/", status_code=200, summary="get the raw data from mawaqit website") 17 | def get_raw_data(masjid_id: str, _: str = Depends(verify_token)): 18 | r = script.fetch_mawaqit(masjid_id) 19 | return {"rawdata": r} 20 | 21 | @router.get("/{masjid_id}/announcements", status_code=200, summary="get the announcements of a specific mosque", response_model=List[models.Announcement]) 22 | def get_announcements(masjid_id: str, _: str = Depends(verify_token)): 23 | r = script.get_announcements(masjid_id) 24 | return r 25 | 26 | @router.get("/{masjid_id}/services", status_code=200, summary="Get the services of a specific mosque", response_model=models.MosqueServices) 27 | def get_services(masjid_id: str, _: str = Depends(verify_token)): 28 | services = script.get_services(masjid_id) 29 | return services 30 | 31 | @router.get("/{masjid_id}/prayer-times", status_code=200, summary="get the prayer times of the current day", response_model=models.PrayerTimes) 32 | def get_prayer_times(masjid_id: str, _: str = Depends(verify_token)): 33 | prayer_times = script.get_prayer_times_of_the_day(masjid_id) 34 | return prayer_times 35 | 36 | 37 | @router.get("/{masjid_id}/calendar", status_code=200, summary="get the year calendar of the prayer times") 38 | def get_year_calendar(masjid_id: str, _: str = Depends(verify_token)): 39 | r = script.get_calendar(masjid_id) 40 | return {"calendar": r} 41 | 42 | 43 | @router.get("/{masjid_id}/calendar/{month_number}", status_code=200, summary="get the month calendar of the prayer times", response_model=List[models.PrayerTimes]) 44 | def get_month_calendar(masjid_id: str, month_number: int, _: str = Depends(verify_token)): 45 | month_dict = script.get_month(masjid_id, month_number) 46 | return jsonable_encoder(month_dict) 47 | 48 | @router.get("/{masjid_id}/calendar-iqama/{month_number}", status_code=200, summary="get the month calendar iqama of the prayer times", response_model=List[models.IqamaPrayerTimes]) 49 | def get_month_calendar_iqama(masjid_id: str, month_number: int, _: str = Depends(verify_token)): 50 | month_dict = script.get_month_iqama(masjid_id, month_number) 51 | return jsonable_encoder(month_dict) 52 | -------------------------------------------------------------------------------- /docs/_media/logo.svg: -------------------------------------------------------------------------------- 1 | Created by Vectors Marketfrom the Noun Project -------------------------------------------------------------------------------- /docs/docs/security.md: -------------------------------------------------------------------------------- 1 | # Security Features: Authentication & Rate Limiting 2 | 3 | ## Overview 4 | 5 | Mawaqit API includes two main security features: 6 | 7 | 1. **Bearer Token Authentication** (opt-in) - Protect your API with a secret token 8 | 2. **Rate Limiting** (always active) - Prevent abuse by limiting requests per IP address 9 | 10 | ## Bearer Token Authentication 11 | 12 | ### What It Does 13 | 14 | Bearer token authentication ensures that only authorized clients can access your API endpoints (except public endpoints like root info and documentation). 15 | 16 | ### Configuration 17 | 18 | #### Enable Authentication 19 | 20 | 1. **Generate a secure bearer token:** 21 | 22 | ```bash 23 | # Linux/Mac 24 | openssl rand -hex 32 25 | 26 | # Windows PowerShell 27 | [System.Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32)) 28 | ``` 29 | 30 | 2. **Add to your `.env` file:** 31 | 32 | ```env 33 | ENABLE_AUTH=true 34 | BEARER_TOKEN=your_generated_secure_token_here 35 | ``` 36 | 37 | 3. **Restart the API** - Authentication is now required 38 | 39 | #### Disable Authentication (Default) 40 | 41 | ```env 42 | ENABLE_AUTH=false 43 | # or simply omit this variable 44 | ``` 45 | 46 | ### Making Authenticated Requests 47 | 48 | #### Using Swagger UI 49 | 50 | When authentication is enabled: 51 | 52 | 1. Open `http://your-api.com/docs` 53 | 2. Click the "Authorize" button (top-right) 54 | 3. Enter your bearer token 55 | 4. Click "Authorize" 56 | 5. Now you can test protected endpoints directly in the browser 57 | 58 | ### Public Endpoints 59 | 60 | These endpoints are **always accessible** without authentication: 61 | 62 | - `GET /api/v1/` - Root/info endpoint 63 | - `/docs` - Swagger UI documentation 64 | - `/openapi.json` - OpenAPI schema 65 | - `/redoc` - ReDoc documentation 66 | 67 | ### Error Responses 68 | 69 | **Missing Authorization Header:** 70 | ```json 71 | { 72 | "detail": "Missing Authorization header" 73 | } 74 | ``` 75 | HTTP Status: `401 Unauthorized` 76 | 77 | **Invalid Bearer Token:** 78 | ```json 79 | { 80 | "detail": "Invalid bearer token" 81 | } 82 | ``` 83 | HTTP Status: `401 Unauthorized` 84 | 85 | ## Rate Limiting 86 | 87 | ### What It Does 88 | 89 | Rate limiting prevents abuse by restricting the number of requests each IP address can make within a time window. 90 | 91 | - **Default Limit:** 60 requests per minute per unique IP address 92 | - **Always Active:** Cannot be disabled, only configured 93 | 94 | ### Configuration 95 | 96 | Edit your `.env` file: 97 | 98 | ```env 99 | # Format: "number/time_unit" 100 | RATE_LIMIT=60/minute # 60 requests per minute (default) 101 | RATE_LIMIT=1000/hour # 1000 requests per hour 102 | RATE_LIMIT=10000/day # 10000 requests per day 103 | RATE_LIMIT=5/second # 5 requests per second 104 | ``` 105 | 106 | ### Rate Limit Exceeded Response 107 | 108 | ```json 109 | { 110 | "detail": "Rate limit exceeded: 60 per 1 minute" 111 | } 112 | ``` 113 | HTTP Status: `429 Too Many Requests` 114 | 115 | ## Environment Variables 116 | 117 | | Variable | Required | Default | Description | 118 | |----------|----------|---------|-------------| 119 | | `ENABLE_AUTH` | No | `false` | Enable/disable bearer token authentication | 120 | | `BEARER_TOKEN` | Yes (if `ENABLE_AUTH=true`) | - | Your secret bearer token | 121 | | `RATE_LIMIT` | No | `60/minute` | Rate limit per IP address | 122 | 123 | ## Next Steps 124 | 125 | - [View API Usage Examples](/docs/usage_example.md) 126 | - [Enable Redis Caching](/docs/redis_activation.md) 127 | - [Docker Deployment](/docs/docker.md) 128 | -------------------------------------------------------------------------------- /scraping/script.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | from fastapi import HTTPException 4 | from config.redisClient import redisClient 5 | from redis.exceptions import RedisError 6 | from typing import List 7 | 8 | import json 9 | import re 10 | import models.models as models 11 | 12 | 13 | def fetch_mawaqit(masjid_id:str): 14 | WEEK_IN_SECONDS = 604800 15 | retrieved_data = None 16 | 17 | # Check if Redis client is initialized 18 | if redisClient is not None: 19 | try: 20 | retrieved_data = redisClient.get(masjid_id) 21 | except RedisError: 22 | print("Error when reading from cache") 23 | 24 | if retrieved_data: 25 | return json.loads(retrieved_data) 26 | 27 | url = f"https://mawaqit.net/fr/m/{masjid_id}" 28 | r = requests.get(url) 29 | if r.status_code == 200: 30 | soup = BeautifulSoup(r.text, 'html.parser') 31 | searchString = r'(?:var|let)\s+confData\s*=\s*(.*?);' 32 | script = soup.find('script', string=re.compile(searchString, re.DOTALL)) 33 | if script: 34 | mawaqit = re.search(searchString, script.string, re.DOTALL) 35 | if mawaqit: 36 | conf_data_json = mawaqit.group(1) 37 | conf_data = json.loads(conf_data_json) 38 | # Store data in Redis if client is initialized 39 | if redisClient is not None: 40 | redisClient.set(masjid_id, json.dumps(conf_data), ex=WEEK_IN_SECONDS) 41 | return conf_data 42 | else: 43 | raise HTTPException(status_code=500, detail=f"Failed to extract confData JSON for {masjid_id}") 44 | else: 45 | print("Script containing confData not found.") 46 | raise HTTPException(status_code=500, detail=f"Script containing confData not found for {masjid_id}") 47 | if r.status_code == 404: 48 | raise HTTPException(status_code=404, detail=f"{masjid_id} not found") 49 | if r.status_code == 500: 50 | raise HTTPException(status_code=502, detail=f"something went wrong fetching {url}") 51 | 52 | def get_prayer_times_of_the_day(masjid_id): 53 | confData = fetch_mawaqit(masjid_id) 54 | times = confData["times"] 55 | sunrise = confData["shuruq"] 56 | prayer_time = models.PrayerTimes(fajr=times[0], sunrise=sunrise, dohr=times[1], asr=times[2], maghreb=times[3], icha=times[4]) 57 | prayer_dict = prayer_time.model_dump() 58 | return prayer_dict 59 | 60 | def get_calendar(masjid_id): 61 | confData = fetch_mawaqit(masjid_id) 62 | return confData["calendar"] 63 | 64 | def get_month(masjid_id, month_number): 65 | if month_number < 1 or month_number > 12: 66 | raise HTTPException(status_code=400, detail=f"Month number should be between 1 and 12") 67 | confData = fetch_mawaqit(masjid_id) 68 | month = confData["calendar"][month_number - 1] 69 | prayer_times_list = [ 70 | models.PrayerTimes( 71 | fajr=prayer[0], 72 | sunrise=prayer[1], 73 | dohr=prayer[2], 74 | asr=prayer[3], 75 | maghreb=prayer[4], 76 | icha=prayer[5] 77 | ) 78 | for prayer in month.values() 79 | ] 80 | return prayer_times_list 81 | 82 | def get_month_iqama(masjid_id, month_number): 83 | if month_number < 1 or month_number > 12: 84 | raise HTTPException(status_code=400, detail=f"Month number should be between 1 and 12") 85 | confData = fetch_mawaqit(masjid_id) 86 | month = confData["iqamaCalendar"][month_number - 1] 87 | iqama_times_list = [ 88 | models.IqamaPrayerTimes( 89 | fajr=iqama[0], 90 | dohr=iqama[1], 91 | asr=iqama[2], 92 | maghreb=iqama[3], 93 | icha=iqama[4] 94 | ) 95 | for iqama in month.values() 96 | ] 97 | 98 | return iqama_times_list 99 | 100 | def get_announcements(masjid_id: int) -> List[models.Announcement]: 101 | confData = fetch_mawaqit(masjid_id) 102 | announcements = confData.get("announcements", []) 103 | return [models.Announcement(**a) for a in announcements] 104 | 105 | def get_services(masjid_id: int) -> models.MosqueServices: 106 | confData = fetch_mawaqit(masjid_id) 107 | mosque_services = models.MosqueServices(**confData) 108 | return mosque_services -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | 3 | from main import app 4 | from config.settings import settings 5 | 6 | client = TestClient(app) 7 | API_ROOT = "/api/v1" 8 | 9 | 10 | def _apply_settings(monkeypatch, enable_auth=False, bearer_token=None): 11 | """ 12 | Hilfsfunktion: patched settings für Tests. 13 | """ 14 | monkeypatch.setattr(settings, "ENABLE_AUTH", enable_auth, raising=False) 15 | monkeypatch.setattr(settings, "BEARER_TOKEN", bearer_token, raising=False) 16 | 17 | 18 | def _auth_header(): 19 | return ( 20 | {"Authorization": f"Bearer {settings.BEARER_TOKEN}"} 21 | if getattr(settings, "ENABLE_AUTH", False) and getattr(settings, "BEARER_TOKEN", None) 22 | else {} 23 | ) 24 | 25 | 26 | def test_read_root(): 27 | """Root endpoint should be accessible without authentication""" 28 | response = client.get(API_ROOT) 29 | assert response.status_code == 200 30 | assert response.json() == { 31 | "Greetings": "Hello and Welcome to this Api, this api use the mawaqit.net as data source of prayers time in more than 8000 masjid, this api can be used to fetch data in json, you can find our docs on /docs. " 32 | } 33 | 34 | 35 | def test_missing_bearer_token_when_auth_enabled(monkeypatch): 36 | """Wenn Auth aktiviert ist, sollten Anfragen ohne Token 401 zurückgeben""" 37 | _apply_settings(monkeypatch, enable_auth=True, bearer_token="test-secret") 38 | response = client.get(f"{API_ROOT}/assalam-argenteuil/prayer-times") 39 | assert response.status_code == 401 40 | detail = response.json().get("detail", "") 41 | assert "Authorization" in detail or "bearer" in str(detail).lower() 42 | 43 | 44 | def test_missing_bearer_token_when_auth_disabled(monkeypatch): 45 | """Wenn Auth deaktiviert ist, sollten Anfragen ohne Token 200 zurückgeben""" 46 | _apply_settings(monkeypatch, enable_auth=False, bearer_token=None) 47 | response = client.get(f"{API_ROOT}/assalam-argenteuil/prayer-times") 48 | assert response.status_code == 200 49 | 50 | 51 | def test_invalid_bearer_token(monkeypatch): 52 | """Ungültiges Token sollte 401 zurückgeben, wenn Auth aktiviert ist""" 53 | _apply_settings(monkeypatch, enable_auth=True, bearer_token="valid-token") 54 | response = client.get( 55 | f"{API_ROOT}/assalam-argenteuil/prayer-times", 56 | headers={"Authorization": "Bearer invalid-token"}, 57 | ) 58 | assert response.status_code == 401 59 | 60 | 61 | def test_get_prayer_times(monkeypatch): 62 | """Prayer times endpoint""" 63 | _apply_settings(monkeypatch, enable_auth=True, bearer_token="valid-token") 64 | response = client.get(f"{API_ROOT}/assalam-argenteuil/prayer-times", headers=_auth_header()) 65 | r_json = response.json() 66 | assert response.status_code == 200 67 | assert len(r_json) == 6 68 | 69 | 70 | def test_get_year_calendar(monkeypatch): 71 | """Year calendar endpoint""" 72 | _apply_settings(monkeypatch, enable_auth=True, bearer_token="valid-token") 73 | response = client.get(f"{API_ROOT}/assalam-argenteuil/calendar", headers=_auth_header()) 74 | r_json = response.json() 75 | assert response.status_code == 200 76 | assert len(r_json["calendar"]) == 12 77 | 78 | 79 | def test_get_month_calendar(monkeypatch): 80 | """Month calendar endpoint""" 81 | _apply_settings(monkeypatch, enable_auth=True, bearer_token="valid-token") 82 | response = client.get(f"{API_ROOT}/assalam-argenteuil/calendar/1", headers=_auth_header()) 83 | r_json = response.json() 84 | assert response.status_code == 200 85 | assert len(r_json) == 31 86 | 87 | 88 | def test_get_announcements(monkeypatch): 89 | """Announcements endpoint""" 90 | _apply_settings(monkeypatch, enable_auth=True, bearer_token="valid-token") 91 | response = client.get(f"{API_ROOT}/assalam-argenteuil/announcements", headers=_auth_header()) 92 | assert response.status_code == 200 93 | 94 | 95 | def test_get_services(monkeypatch): 96 | """Services endpoint""" 97 | _apply_settings(monkeypatch, enable_auth=True, bearer_token="valid-token") 98 | response = client.get(f"{API_ROOT}/assalam-argenteuil/services", headers=_auth_header()) 99 | assert response.status_code == 200 100 | 101 | 102 | def test_get_month_calendar_iqama(monkeypatch): 103 | """Month calendar iqama endpoint""" 104 | _apply_settings(monkeypatch, enable_auth=True, bearer_token="valid-token") 105 | response = client.get(f"{API_ROOT}/assalam-argenteuil/calendar-iqama/1", headers=_auth_header()) 106 | r_json = response.json() 107 | assert response.status_code == 200 108 | assert len(r_json) == 31 109 | --------------------------------------------------------------------------------