├── 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 | 
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 | [](https://github.com/mrsofiane/mawaqit-api/stargazers)
4 | 
5 | 
6 | 
7 | [](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 | [](https://github.com/mrsofiane/mawaqit-api/stargazers)
4 | 
5 | 
6 | 
7 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------