├── .env.local.sample
├── .flake8
├── .github
└── ISSUE_TEMPLATE
│ └── request-for-api-access.md
├── .gitignore
├── Dockerfile
├── Dockerfile-prod
├── README.md
├── config.py
├── db
└── 00-samplegitdb.sql
├── docker-compose.prod.yml
├── docker-compose.yml
├── main.py
├── models.py
├── pyproject.toml
├── requirements.txt
├── scripts
└── key-gen.sh
├── spec.v1.yml
├── text_transform.py
└── uwsgi.ini
/.env.local.sample:
--------------------------------------------------------------------------------
1 | MYSQL_ROOT_PASSWORD=docker
2 | MYSQL_USER=docker
3 | MYSQL_PASSWORD=docker
4 | MYSQL_HOST=db
5 | MYSQL_DATABASE=hadithdb
6 |
7 | FLASK_ENV=development
8 | FLASK_APP=main.py
9 |
10 | AWS_SECRET=secret
11 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore = E402
3 | max-line-length = 160
4 | max-complexity = 10
5 | exclude =
6 | .git,
7 | venv,
8 | db
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/request-for-api-access.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Request for API access
3 | about: Request for access to api.sunnah.com
4 | title: 'Request for API access: [Your Name]'
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | * **Please tell us about yourself (include an email address):**
11 |
12 |
13 |
14 |
15 | * **Your purpose in using this API:**
16 |
17 |
18 |
19 |
20 | * **API rate limits:**
21 |
22 | * **Maximum requests per second:**
23 | * **Maximum requests per day:**
24 |
25 |
26 |
27 | * **Is your use case better served by having an offline dump of hadith data or programmatic API access?**
28 |
29 |
30 |
31 |
32 | * **What are the languages in which would you like hadith data?**
33 |
34 |
35 |
36 |
37 | * **What programming language will your API client be in?**
38 |
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 | __pycache__
3 | .DS_Store
4 |
5 | .vscode/
6 | .coverage
7 | venv
8 | .venv/
9 |
10 | .env.*
11 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.8-slim
2 |
3 | ENV PYTHONUNBUFFERED 1
4 |
5 | RUN mkdir /code
6 | WORKDIR /code
7 | COPY requirements.txt /code/
8 |
9 | RUN pip install -r requirements.txt
10 |
11 | RUN groupadd -g 777 appuser && \
12 | useradd -r -u 777 -g appuser appuser
13 | USER appuser
14 |
15 | COPY . /code/
16 |
--------------------------------------------------------------------------------
/Dockerfile-prod:
--------------------------------------------------------------------------------
1 | FROM python:3.8-slim
2 |
3 | ENV PYTHONUNBUFFERED 1
4 |
5 | RUN mkdir /code
6 | WORKDIR /code
7 | COPY requirements.txt /code/
8 |
9 | RUN apt-get update && \
10 | apt-get install gcc -y && \
11 | apt-get clean
12 | RUN pip install uwsgi && \
13 | pip install -r requirements.txt
14 |
15 | RUN groupadd -g 777 appuser && \
16 | useradd -r -u 777 -g appuser appuser
17 | USER appuser
18 |
19 | COPY . /code/
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Overview
2 |
3 | The official API of sunnah.com for retrieving information about hadith collections.
4 |
5 | # Getting started
6 |
7 | Please follow the instructions below.
8 |
9 | First create a local `.env.local` configuration file and update values as needed.
10 | A sample file is provided at `.env.local.sample`.
11 |
12 | Run manually:
13 | ```bash
14 | git clone REPO
15 | cd REPO
16 | python3 -m venv venv
17 | source venv/bin/activate
18 | pip install -r requirements.txt
19 | export FLASK_ENV=development FLASK_APP=main.py
20 | flask run --host=0.0.0.0
21 | ```
22 |
23 | Or alternatively use `docker-compose` which will give a full environment with a MySQL instance loaded with a sample dataset:
24 |
25 | ```bash
26 | docker-compose up
27 | ```
28 |
29 | * Use `--build` option to re-build.
30 | * Use the `-d` option to run in detached mode.
31 |
32 | You can then visit [localhost:5000](http://localhost:5000) to verify that it's running on your machine. Or, alternatively:
33 |
34 | ```bash
35 | $ curl http://localhost:5000
36 | ```
37 |
38 | ## Deployment
39 |
40 | Configuration files are located at `env.local` and `uwsgi.ini`.
41 |
42 | A production ready uWSGI daemon (uwsgi socket exposed on port 5001) can be started with:
43 |
44 | ```bash
45 | docker-compose -f docker-compose.prod.yml up -d --build
46 | ```
47 |
48 | ## Routes
49 |
50 | Visit https://sunnah.stoplight.io/docs/api/ for full API documentation.
51 |
52 | ## Linting and Formatting
53 |
54 | `flake8` and `black` are used for code linting and formatting respectively. Before submitting pull requests, make sure black and flake8 is run against the code. Follow the instructions below for using `black` and `flake8`:
55 |
56 | ```sh
57 | # goto repository root directory
58 | # make sure the virtual environment is activated
59 | black .
60 | flake8 .
61 | # fix any linting issues
62 | # Then you are ready to submit your PR
63 | ```
64 |
65 | To add more rules for linting and formatting, make changes to `.flake8` and `pyproject.toml` accordingly.
66 |
67 | # Guidelines for Sending a Pull Request
68 |
69 | 1. Only change one thing at a time.
70 | 2. Don't mix a lot of formatting changes with logic change in the same pull request.
71 | 3. Keep code refactor and logic change in separate pull requests.
72 | 4. Squash your commits. When you address feedback, squash it as well. No one benefits from "addressed feedback" commit in the history.
73 | 5. Break down bigger changes into smaller separate pull requests.
74 | 6. If changing UI, attach a screenshot of how the changes look.
75 | 7. Reference the issue being fixed by adding the issue tag in the commit message.
76 | 8. Do not send a big change before first proposing it and getting a buy-in from the maintainer.
77 |
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | import os
2 | from dotenv import load_dotenv
3 |
4 | load_dotenv(".env.local")
5 |
6 |
7 | class Config(object):
8 | JSON_SORT_KEYS = False
9 | AWS_SECRET = "{AWS_SECRET}".format(**os.environ)
10 | SQLALCHEMY_TRACK_MODIFICATIONS = False
11 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}/{MYSQL_DATABASE}".format(**os.environ)
12 |
--------------------------------------------------------------------------------
/docker-compose.prod.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | web:
5 | command: uwsgi --ini uwsgi.ini
6 | build:
7 | context: .
8 | dockerfile: Dockerfile-prod
9 | ports:
10 | - "5001:5001"
11 | restart: on-failure
12 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | db:
5 | image: mysql:8.0.35
6 | platform: linux/x86_64
7 | command: --default-authentication-plugin=mysql_native_password
8 | volumes:
9 | - ./db:/docker-entrypoint-initdb.d/:ro
10 | env_file:
11 | - .env.local
12 | web:
13 | depends_on:
14 | - "db"
15 | build: .
16 | platform: linux/x86_64
17 | command: flask run --host=0.0.0.0
18 | volumes:
19 | - .:/code
20 | ports:
21 | - "5000:5000"
22 | env_file:
23 | - .env.local
24 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import functools
2 | from flask import Flask, jsonify, request, abort
3 | from sqlalchemy import func, or_
4 | from werkzeug.exceptions import HTTPException
5 |
6 | app = Flask(__name__)
7 | app.config.from_object("config.Config")
8 |
9 | from models import HadithCollection, Book, Chapter, Hadith
10 |
11 |
12 | @app.before_request
13 | def verify_secret():
14 | if not app.debug and request.headers.get("x-aws-secret") != app.config["AWS_SECRET"]:
15 | abort(401)
16 |
17 |
18 | @app.errorhandler(HTTPException)
19 | def jsonify_http_error(error):
20 | response = {"error": {"details": error.description, "code": error.code}}
21 |
22 | return jsonify(response), error.code
23 |
24 |
25 | def paginate_results(f):
26 | @functools.wraps(f)
27 | def decorated_function(*args, **kwargs):
28 | limit = int(request.args.get("limit", 50))
29 | page = int(request.args.get("page", 1))
30 |
31 | queryset = f(*args, **kwargs).paginate(page=page, per_page=limit, max_per_page=100)
32 | result = {
33 | "data": [x.serialize() for x in queryset.items],
34 | "total": queryset.total,
35 | "limit": queryset.per_page,
36 | "previous": queryset.prev_num,
37 | "next": queryset.next_num,
38 | }
39 | return jsonify(result)
40 |
41 | return decorated_function
42 |
43 |
44 | def single_resource(f):
45 | @functools.wraps(f)
46 | def decorated_function(*args, **kwargs):
47 | result = f(*args, **kwargs).first_or_404()
48 | result = result.serialize()
49 | return jsonify(result)
50 |
51 | return decorated_function
52 |
53 |
54 | @app.route("/", methods=["GET"])
55 | def home():
56 | return "
Welcome to sunnah.com API.
"
57 |
58 |
59 | @app.route("/v1/collections", methods=["GET"])
60 | @paginate_results
61 | def api_collections():
62 | return HadithCollection.query.order_by(HadithCollection.collectionID)
63 |
64 |
65 | @app.route("/v1/collections/", methods=["GET"])
66 | @single_resource
67 | def api_collection(name):
68 | return HadithCollection.query.filter_by(name=name)
69 |
70 |
71 | @app.route("/v1/collections//books", methods=["GET"])
72 | @paginate_results
73 | def api_collection_books(name):
74 | return Book.query.filter_by(collection=name, status=4).order_by(func.abs(Book.ourBookID))
75 |
76 |
77 | @app.route("/v1/collections//books/", methods=["GET"])
78 | @single_resource
79 | def api_collection_book(name, bookNumber):
80 | book_id = Book.get_id_from_number(bookNumber)
81 | return Book.query.filter_by(collection=name, status=4, ourBookID=book_id)
82 |
83 |
84 | @app.route("/v1/collections//books//hadiths", methods=["GET"])
85 | @paginate_results
86 | def api_collection_book_hadiths(collection_name, bookNumber):
87 | return Hadith.query.filter_by(collection=collection_name, bookNumber=bookNumber).order_by(Hadith.englishURN)
88 |
89 |
90 | @app.route("/v1/collections//hadiths/", methods=["GET"])
91 | @single_resource
92 | def api_collection_hadith(collection_name, hadithNumber):
93 | return Hadith.query.filter_by(collection=collection_name, hadithNumber=hadithNumber)
94 |
95 |
96 | @app.route("/v1/collections//books//chapters", methods=["GET"])
97 | @paginate_results
98 | def api_collection_book_chapters(collection_name, bookNumber):
99 | book_id = Book.get_id_from_number(bookNumber)
100 | return Chapter.query.filter_by(collection=collection_name, arabicBookID=book_id).order_by(Chapter.babID)
101 |
102 |
103 | @app.route("/v1/collections//books//chapters/", methods=["GET"])
104 | @single_resource
105 | def api_collection_book_chapter(collection_name, bookNumber, chapterId):
106 | book_id = Book.get_id_from_number(bookNumber)
107 | return Chapter.query.filter_by(collection=collection_name, arabicBookID=book_id, babID=chapterId)
108 |
109 |
110 | @app.route("/v1/hadiths/", methods=["GET"])
111 | @single_resource
112 | def api_hadith(urn):
113 | return Hadith.query.filter(or_(Hadith.arabicURN == urn, Hadith.englishURN == urn))
114 |
115 |
116 | @app.route("/v1/hadiths/random", methods=["GET"])
117 | @single_resource
118 | def api_hadiths_random():
119 | # TODO Make this configurable instead of hardcoding
120 | return Hadith.query.filter_by(collection="riyadussalihin").order_by(func.rand())
121 |
122 |
123 | if __name__ == "__main__":
124 | app.run(host="0.0.0.0")
125 |
--------------------------------------------------------------------------------
/models.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 | from main import app
3 | from text_transform import cleanup_text, cleanup_en_text, cleanup_chapter_title, cleanup_en_chapter_title
4 | import json
5 |
6 |
7 | db = SQLAlchemy(app)
8 | db.reflect()
9 |
10 |
11 | def is_number(s):
12 | try:
13 | int(s)
14 | return True
15 | except ValueError:
16 | return False
17 |
18 |
19 | class HadithCollection(db.Model):
20 | __tablename__ = "Collections"
21 |
22 | def serialize(self):
23 | return {
24 | "name": self.name,
25 | "hasBooks": self.includesBooks,
26 | "hasChapters": self.includesChapters,
27 | "collection": [
28 | {"lang": "en", "title": self.englishTitle, "shortIntro": self.shortintro},
29 | {"lang": "ar", "title": self.arabicTitle, "shortIntro": self.shortIntroArabic if hasattr(self, "shortIntroArabic") else self.shortintro},
30 | ],
31 | "totalHadith": self.totalhadith,
32 | "totalAvailableHadith": self.numhadith,
33 | }
34 |
35 |
36 | class Book(db.Model):
37 | __tablename__ = "BookData"
38 |
39 | id_number_map = {-1: "introduction", -35: "35b"}
40 |
41 | @staticmethod
42 | def get_number_from_id(book_id):
43 | # Logic for dealing with non-straightforward ourBookIDs
44 | book_number = Book.id_number_map.get(book_id, int(book_id))
45 | return str(book_number)
46 |
47 | @staticmethod
48 | def get_id_from_number(book_number):
49 | number_id_map = {v: k for k, v in Book.id_number_map.items()}
50 |
51 | if is_number(book_number):
52 | book_id = int(book_number)
53 | else:
54 | book_id = number_id_map.get(book_number)
55 |
56 | return str(book_id)
57 |
58 | def serialize(self):
59 | bookNumber = Book.get_number_from_id(self.ourBookID)
60 | return {
61 | "bookNumber": bookNumber,
62 | "book": [{"lang": "en", "name": self.englishBookName}, {"lang": "ar", "name": self.arabicBookName}],
63 | "hadithStartNumber": self.firstNumber,
64 | "hadithEndNumber": self.lastNumber,
65 | "numberOfHadith": self.totalNumber,
66 | }
67 |
68 |
69 | class Chapter(db.Model):
70 | __tablename__ = "ChapterData"
71 |
72 | def serialize(self):
73 | bookNumber = Book.get_number_from_id(self.arabicBookID)
74 | return {
75 | "bookNumber": bookNumber,
76 | "chapterId": str(self.babID),
77 | "chapter": [
78 | {
79 | "lang": "en",
80 | "chapterNumber": str(self.englishBabNumber),
81 | "chapterTitle": cleanup_en_chapter_title(self.englishBabName),
82 | "intro": cleanup_en_text(self.englishIntro),
83 | "ending": cleanup_en_text(self.englishEnding),
84 | },
85 | {
86 | "lang": "ar",
87 | "chapterNumber": str(self.arabicBabNumber),
88 | "chapterTitle": cleanup_chapter_title(self.arabicBabName),
89 | "intro": cleanup_text(self.arabicIntro),
90 | "ending": cleanup_text(self.arabicEnding),
91 | },
92 | ],
93 | }
94 |
95 |
96 | class Hadith(db.Model):
97 | __tablename__ = "HadithTable"
98 |
99 | rel_collection = db.relationship(
100 | "HadithCollection", primaryjoin="Hadith.collection == HadithCollection.name", foreign_keys="Hadith.collection", lazy="joined"
101 | )
102 |
103 | def get_grade(self, field_name):
104 | grade_val = getattr(self, field_name)
105 | if not grade_val:
106 | return []
107 |
108 | # If the field has a json value, return it
109 | # Otherwise build same data structure from individual string fields
110 | try:
111 | vals = json.loads(grade_val)
112 | return [dict((k, x[k]) for k in ("graded_by", "grade")) for x in vals]
113 | except ValueError:
114 | return [{"graded_by": getattr(self.rel_collection, field_name), "grade": grade_val}]
115 |
116 | def serialize(self):
117 | grades = {"en": self.get_grade("englishgrade1"), "ar": self.get_grade("arabicgrade1")}
118 |
119 | return {
120 | "collection": self.collection,
121 | "bookNumber": self.bookNumber,
122 | "chapterId": str(self.babID),
123 | "hadithNumber": self.hadithNumber,
124 | "hadith": [
125 | {
126 | "lang": "en",
127 | "chapterNumber": self.englishBabNumber,
128 | "chapterTitle": cleanup_en_chapter_title(self.englishBabName),
129 | "urn": self.englishURN,
130 | "body": cleanup_en_text(self.englishText),
131 | "grades": grades["en"],
132 | },
133 | {
134 | "lang": "ar",
135 | "chapterNumber": self.arabicBabNumber,
136 | "chapterTitle": cleanup_chapter_title(self.arabicBabName),
137 | "urn": self.arabicURN,
138 | "body": cleanup_text(self.arabicText),
139 | "grades": grades["ar"],
140 | },
141 | ],
142 | }
143 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.black]
2 | line-length = 160
3 | target-version = ['py38']
4 | include = '\.py?$'
5 | exclude = '''
6 | (
7 | /(
8 | | \.git
9 | | \venv
10 | )/
11 | )
12 | '''
13 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | appdirs==1.4.4
2 | astroid==2.4.1
3 | attrs==19.3.0
4 | black==24.3.0
5 | cfgv==3.1.0
6 | click==7.1.2
7 | colorama==0.4.3
8 | distlib==0.3.1
9 | filelock==3.0.12
10 | flake8==3.8.3
11 | Flask==2.2.5
12 | Flask-SQLAlchemy==2.4.3
13 | identify==1.4.20
14 | importlib-metadata==1.7.0
15 | isort==4.3.21
16 | itsdangerous==1.1.0
17 | Jinja2==3.1.4
18 | lazy-object-proxy==1.4.3
19 | lxml==4.9.1
20 | MarkupSafe==1.1.1
21 | mccabe==0.6.1
22 | nodeenv==1.4.0
23 | pathspec==0.8.0
24 | pycodestyle==2.6.0
25 | pyflakes==2.2.0
26 | pylint==2.5.2
27 | PyMySQL==1.1.1
28 | python-dotenv==0.13.0
29 | PyYAML==5.4
30 | regex==2020.6.8
31 | six==1.15.0
32 | SQLAlchemy==1.3.17
33 | toml==0.10.1
34 | typed-ast==1.4.1
35 | virtualenv==20.0.25
36 | Werkzeug==3.0.6
37 | wrapt==1.12.1
38 | zipp==3.19.1
39 |
--------------------------------------------------------------------------------
/scripts/key-gen.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # This script requires aws-cli to be installed
3 | # Please visit https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html
4 | # After installation use 'aws configure' to setup the credentials and then execute the script
5 |
6 | if [ "$#" -eq 0 ]
7 | then
8 | echo "This script takes one command-line argument: the name of the key to be created"
9 | exit 1
10 | fi
11 |
12 | KEYNAME=$1
13 | KEYINFO=`aws apigateway create-api-key --name ${KEYNAME} --region us-west-2 --enabled 2>&1`
14 | # KEYVALUE=`aws apigateway get-api-key --api-key ${KEYNAME} --include-value --region us-west-2 | grep value | cut -d\" -f4 2>&1`
15 | echo "Attempting to run command aws apigateway create-api-key --name ${KEYNAME} --region us-west-2 --enabled 2>&1"
16 | # echo "Command returned ${KEYINFO}"
17 | KEYID=`echo ${KEYINFO} | grep id | cut -d\" -f4`
18 | KEYVALUE=`echo ${KEYINFO} | grep value | cut -d\" -f8`
19 | aws apigateway create-usage-plan-key --usage-plan-id b3rk95 --key-type API_KEY --key-id ${KEYID} --region us-west-2 2>&1
20 | echo -e "\nNew key successfully created: ${KEYVALUE}"
21 |
22 | if [ -f "apimail.php" ]; then
23 | php apimail.php ${KEYNAME} ${KEYVALUE}
24 | fi
25 |
--------------------------------------------------------------------------------
/spec.v1.yml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | version: "1.0"
4 | title: Sunnah.com API
5 | description: >-
6 | Sunnah.com API offers access to verified data of collections of hadith of
7 | the Prophet (SAWS). Here you will find a list of the available endpoints in
8 | our REST API.
9 |
10 |
11 | You will need an API key to access this data, which you may request by creating an
12 | issue on our GitHub repo. [Create an issue](https://github.com/sunnah-com/api/issues/new?template=request-for-api-access.md&title=Request+for+API+access%3A+[Your+Name])
13 | contact:
14 | name: Sunnah.com
15 | url: https://sunnah.com/contact
16 | security:
17 | - APIKey: []
18 | paths:
19 | /collections:
20 | get:
21 | summary: Get the list of collections
22 | description: ""
23 | responses:
24 | "200":
25 | description: Paginated list of available collections
26 | content:
27 | application/json:
28 | schema:
29 | type: object
30 | allOf:
31 | - properties:
32 | data:
33 | type: array
34 | items:
35 | $ref: "#/components/schemas/Collection"
36 | - $ref: "#/components/schemas/PaginatedResponse"
37 | parameters:
38 | - $ref: "#/components/parameters/limit"
39 | - $ref: "#/components/parameters/page"
40 | "/collections/{collectionName}":
41 | get:
42 | summary: Get a collection by name
43 | description: ""
44 | responses:
45 | "200":
46 | description: A collection
47 | content:
48 | application/json:
49 | schema:
50 | $ref: "#/components/schemas/Collection"
51 | parameters:
52 | - in: path
53 | name: collectionName
54 | description: Name of the collection
55 | required: true
56 | schema:
57 | type: string
58 | "/collections/{collectionName}/books":
59 | get:
60 | summary: Get the list of books for a collection
61 | description: ""
62 | responses:
63 | "200":
64 | description: Paginated list of available books of a collection
65 | content:
66 | application/json:
67 | schema:
68 | type: object
69 | allOf:
70 | - properties:
71 | data:
72 | type: array
73 | items:
74 | $ref: "#/components/schemas/Book"
75 | - $ref: "#/components/schemas/PaginatedResponse"
76 | parameters:
77 | - in: path
78 | name: collectionName
79 | description: Name of the collection
80 | required: true
81 | schema:
82 | type: string
83 | - $ref: "#/components/parameters/limit"
84 | - $ref: "#/components/parameters/page"
85 | "/collections/{collectionName}/books/{bookNumber}":
86 | get:
87 | summary: Get a book of a collection
88 | description: ""
89 | responses:
90 | "200":
91 | description: A book of a collection
92 | content:
93 | application/json:
94 | schema:
95 | $ref: "#/components/schemas/Book"
96 | parameters:
97 | - in: path
98 | name: collectionName
99 | description: Name of the collection
100 | required: true
101 | schema:
102 | type: string
103 | - in: path
104 | name: bookNumber
105 | description: Number of the book
106 | required: true
107 | schema:
108 | type: string
109 | "/collections/{collectionName}/books/{bookNumber}/chapters":
110 | get:
111 | summary: Get the list of chapters of a book for a collection
112 | description: ""
113 | responses:
114 | "200":
115 | description: Paginated list of chapters of a book of a collection
116 | content:
117 | application/json:
118 | schema:
119 | type: object
120 | allOf:
121 | - properties:
122 | data:
123 | type: array
124 | items:
125 | $ref: "#/components/schemas/Chapter"
126 | - $ref: "#/components/schemas/PaginatedResponse"
127 | parameters:
128 | - in: path
129 | name: collectionName
130 | description: Name of the collection
131 | required: true
132 | schema:
133 | type: string
134 | - in: path
135 | name: bookNumber
136 | description: Number of the book
137 | required: true
138 | schema:
139 | type: string
140 | - $ref: "#/components/parameters/limit"
141 | - $ref: "#/components/parameters/page"
142 | "/collections/{collectionName}/books/{bookNumber}/chapters/{chapterId}":
143 | get:
144 | summary: Get a chapter of a book for a collection
145 | description: ""
146 | responses:
147 | "200":
148 | description: A chapter of a book for a collection
149 | content:
150 | application/json:
151 | schema:
152 | $ref: "#/components/schemas/Chapter"
153 | parameters:
154 | - in: path
155 | name: collectionName
156 | description: Name of the collection
157 | required: true
158 | schema:
159 | type: string
160 | - in: path
161 | name: bookNumber
162 | description: Number of the book
163 | required: true
164 | schema:
165 | type: string
166 | - in: path
167 | name: chapterId
168 | description: Number of the book
169 | required: true
170 | schema:
171 | type: number
172 | format: float
173 | "/collections/{collectionName}/books/{bookNumber}/hadiths":
174 | get:
175 | summary: Get the list of hadith of a book for a collection
176 | description: ""
177 | responses:
178 | "200":
179 | description: Paginated list of hadiths of a book of a collection
180 | content:
181 | application/json:
182 | schema:
183 | type: object
184 | allOf:
185 | - properties:
186 | data:
187 | type: array
188 | items:
189 | $ref: "#/components/schemas/Hadith"
190 | - $ref: "#/components/schemas/PaginatedResponse"
191 | parameters:
192 | - in: path
193 | name: collectionName
194 | description: Name of the collection
195 | required: true
196 | schema:
197 | type: string
198 | - in: path
199 | name: bookNumber
200 | description: Number of the book
201 | required: true
202 | schema:
203 | type: string
204 | - $ref: "#/components/parameters/limit"
205 | - $ref: "#/components/parameters/page"
206 | "/collections/{collectionName}/hadiths/{hadithNumber}":
207 | get:
208 | summary: Get a hadith of a collection
209 | description: ""
210 | responses:
211 | "200":
212 | description: Hadith of a book of a collection
213 | content:
214 | application/json:
215 | schema:
216 | $ref: "#/components/schemas/Hadith"
217 | parameters:
218 | - in: path
219 | name: collectionName
220 | description: Name of the collection
221 | required: true
222 | schema:
223 | type: string
224 | - in: path
225 | name: hadithNumber
226 | description: Number of the hadith
227 | required: true
228 | schema:
229 | type: string
230 | "/hadiths/{urn}":
231 | get:
232 | summary: Get a hadith by its URN
233 | description: ""
234 | responses:
235 | "200":
236 | description: A hadith
237 | content:
238 | application/json:
239 | schema:
240 | $ref: "#/components/schemas/Hadith"
241 | parameters:
242 | - in: path
243 | name: urn
244 | required: true
245 | description: English or Arabic URN
246 | schema:
247 | type: integer
248 | /hadiths/random:
249 | get:
250 | summary: Get a randomly selected hadith
251 | description: ""
252 | responses:
253 | "200":
254 | description: A randomly selected hadith
255 | content:
256 | application/json:
257 | schema:
258 | $ref: "#/components/schemas/Hadith"
259 | servers:
260 | - url: https://api.sunnah.com/v1/
261 | components:
262 | parameters:
263 | limit:
264 | in: query
265 | name: limit
266 | description: Maximum number of items
267 | required: false
268 | schema:
269 | type: integer
270 | maximum: 100
271 | default: 50
272 | page:
273 | in: query
274 | name: page
275 | description: Offset for pagination
276 | required: false
277 | schema:
278 | type: integer
279 | default: 1
280 | securitySchemes:
281 | APIKey:
282 | name: X-API-Key
283 | type: apiKey
284 | in: header
285 | schemas:
286 | PaginatedResponse:
287 | type: object
288 | properties:
289 | total:
290 | type: integer
291 | description: Total number of results matching the dataset
292 | limit:
293 | type: integer
294 | description: Maximum number of results in the current dataset
295 | previous:
296 | type: integer
297 | nullable: true
298 | description: Number of the previous page
299 | next:
300 | type: integer
301 | nullable: true
302 | description: Number of the next page
303 | Hadith:
304 | type: object
305 | properties:
306 | collection:
307 | type: string
308 | description: Name of the collection
309 | bookNumber:
310 | type: string
311 | description: The number of the book this hadith belongs to
312 | chapterId:
313 | type: string
314 | description: The ID of the chapter this hadith belongs to
315 | hadithNumber:
316 | type: string
317 | hadith:
318 | type: array
319 | description: Language specific data of the hadith
320 | items:
321 | type: object
322 | properties:
323 | lang:
324 | type: string
325 | chapterNumber:
326 | type: string
327 | chapterTitle:
328 | type: string
329 | urn:
330 | type: integer
331 | body:
332 | type: string
333 | grades:
334 | type: array
335 | description: Hadith grade information
336 | items:
337 | type: object
338 | properties:
339 | graded_by:
340 | type: string
341 | grade:
342 | type: string
343 | Chapter:
344 | properties:
345 | bookNumber:
346 | type: string
347 | description: The number of the book this chapter belongs to
348 | chapterId:
349 | type: string
350 | description: The ID of the chapter
351 | chapter:
352 | type: array
353 | description: Language specific data of the chapter
354 | items:
355 | type: object
356 | properties:
357 | lang:
358 | type: string
359 | chapterNumber:
360 | type: string
361 | chapterTitle:
362 | type: string
363 | intro:
364 | type: string
365 | nullable: true
366 | ending:
367 | type: string
368 | nullable: true
369 | Book:
370 | properties:
371 | bookNumber:
372 | type: string
373 | description: Number of the book
374 | book:
375 | type: array
376 | description: Language specific data of the book
377 | items:
378 | type: object
379 | properties:
380 | lang:
381 | type: string
382 | name:
383 | type: string
384 | hadithStartNumber:
385 | type: integer
386 | description: The first hadith number that is available in this book
387 | hadithEndNumber:
388 | type: integer
389 | description: The last hadith number that is available in this book
390 | numberOfHadith:
391 | type: integer
392 | description: Total number of available hadith in this book
393 | Collection:
394 | properties:
395 | name:
396 | type: string
397 | description: Name of the collection
398 | hasBooks:
399 | type: boolean
400 | description: Whether the collection has books or not
401 | hasChapters:
402 | type: boolean
403 | description: Whether the collection has chapters or not
404 | collection:
405 | type: array
406 | description: Language specific data of the collection
407 | items:
408 | type: object
409 | properties:
410 | lang:
411 | type: string
412 | title:
413 | type: string
414 | shortIntro:
415 | type: string
416 | totalHadith:
417 | type: integer
418 | description: Total number of hadith in the collection
419 | totalAvailableHadith:
420 | type: integer
421 | description: Total number of available hadith in the collection
422 |
--------------------------------------------------------------------------------
/text_transform.py:
--------------------------------------------------------------------------------
1 | import re
2 | import lxml.html
3 | import lxml
4 |
5 |
6 | def fix_html(text, remove_wrapper=False):
7 | """Fix invalid html, remove unnecessary attribs, tags and whitespace"""
8 | text = text.strip()
9 | text = text.replace("\r", "") # remove \r as lxml escapes it
10 | doc = lxml.html.document_fromstring(text)
11 |
12 | anchors = doc.body.findall(".//a")
13 | for anchor in anchors:
14 | anchor.attrib.pop("id", None)
15 | anchor.attrib.pop("name", None)
16 |
17 | children = []
18 | for elem in doc.body:
19 | if not elem.text:
20 | continue
21 | children.append(lxml.etree.tostring(elem, encoding="unicode"))
22 | text = "\n".join(children)
23 | if remove_wrapper:
24 | text = re.sub(r"^", "", text)
25 | text = re.sub(r"
$", "", text)
26 | text = re.sub(r"?c_q\d+>", "", text) # remove like tags
27 | return text
28 |
29 |
30 | def standardize_terms(text):
31 | terms = [
32 | ("PBUH", "\ufdfa"),
33 | ("P.B.U.H.", "\ufdfa"),
34 | ("peace_be_upon_him", "\ufdfa"),
35 | ("(may peace be upon him)", "(\ufdfa)"),
36 | ("(saws)", "(\ufdfa)"),
37 | ("(SAW)", "(\ufdfa)"),
38 | ("(saw)", "(\ufdfa)"),
39 | ("he Apostle of Allah", "he Messenger of Allah"),
40 | ("he Apostle of Allaah", "he Messenger of Allah"),
41 | ("Allah's Apostle", "Allah's Messenger"),
42 | ("he Holy Prophet ", "he Prophet "),
43 | ]
44 |
45 | for old, new in terms:
46 | text = text.replace(old, new)
47 |
48 | text = re.sub(r"Allah\'s Messenger (?!\()", "Allah's Messenger (\ufdfa) ", text)
49 | text = re.sub(r"he Messenger of Allah (?!\()", "he Messenger of Allah (\ufdfa) ", text)
50 | text = re.sub(r"he Prophet (?!\()", "he Prophet (\ufdfa) ", text)
51 |
52 | return text
53 |
54 |
55 | def fix_hyperlinks(text):
56 | """Converts links to ayah and hadith to quran.com and sunnah.com respectively"""
57 | text = text.replace('href="/', 'href="https://sunnah.com/')
58 |
59 | quran_links = re.findall(r"javascript:openquran\((.+?)\)", text)
60 | for link_match in quran_links:
61 | surah, begin, end = link_match.split(",")
62 | text = text.replace("javascript:openquran({})".format(link_match), "https://quran.com/{}/{}-{}".format(int(surah) + 1, begin, end))
63 | return text
64 |
65 |
66 | def cleanup_text(text):
67 | if not text:
68 | return text
69 | text = re.sub(r"\n+", "\n", text)
70 | text = re.sub(r" +", " ", text)
71 | text = fix_html(text)
72 | text = fix_hyperlinks(text)
73 | text = text.strip()
74 | return text
75 |
76 |
77 | def cleanup_en_text(text):
78 | if not text:
79 | return text
80 | text = cleanup_text(text)
81 | text = standardize_terms(text)
82 | return text
83 |
84 |
85 | def cleanup_chapter_title(text):
86 | if not text:
87 | return text
88 | text = re.sub(r"\n+", "\n", text)
89 | text = re.sub(r" +", " ", text)
90 | text = fix_html(text, remove_wrapper=True)
91 | text = fix_hyperlinks(text)
92 | text = text.strip()
93 | return text
94 |
95 |
96 | def cleanup_en_chapter_title(text):
97 | if not text:
98 | return text
99 | text = cleanup_chapter_title(text)
100 | text = standardize_terms(text)
101 | return text
102 |
--------------------------------------------------------------------------------
/uwsgi.ini:
--------------------------------------------------------------------------------
1 | [uwsgi]
2 | module = main:app
3 | socket = 0.0.0.0:5001
4 | master = true
5 | processes = 4
6 |
--------------------------------------------------------------------------------