-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Scaffolding of a FLASK app for WhatsApp analytics #5
Changes from 20 commits
6c1ee3a
dd9f22e
a42c908
94f9f2f
0e9aa9f
701d27f
bb309cd
a21de5f
8bbf271
d45eac2
fb8ed03
d6652a5
8848671
21773af
a51fef1
88188c6
23bd540
63abb51
aef62ff
db8d89e
c48fa2b
5c47b4b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
FLASK_APP= | ||
DB_USER= | ||
DB_PASSWORD= | ||
DB_NAME= | ||
DB_HOST= | ||
DB_PORT= |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
name: pre-commit | ||
|
||
on: | ||
pull_request: | ||
push: | ||
branches: | ||
- develop | ||
- main | ||
|
||
jobs: | ||
pre-commit: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v2 | ||
- uses: actions/setup-python@v2 | ||
with: | ||
python-version: '3.9' | ||
- uses: pre-commit/[email protected] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
*.pyc | ||
__pycache__/ | ||
.mypy_cache | ||
|
||
# Environments | ||
.env | ||
.venv | ||
env/ | ||
venv/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
repos: | ||
- repo: https://github.com/pre-commit/pre-commit-hooks | ||
rev: v4.6.0 | ||
hooks: | ||
- id: trailing-whitespace | ||
- id: end-of-file-fixer | ||
- id: check-json | ||
- id: check-yaml | ||
- id: check-merge-conflict | ||
- id: check-added-large-files | ||
- id: debug-statements | ||
- id: requirements-txt-fixer | ||
- repo: https://github.com/pre-commit/mirrors-isort | ||
rev: 'v5.10.1' | ||
hooks: | ||
- id: isort | ||
- repo: https://github.com/pre-commit/mirrors-mypy | ||
rev: 'v1.9.0' | ||
hooks: | ||
- id: mypy | ||
exclude: alembic | ||
- repo: https://github.com/psf/black | ||
rev: 24.4.0 | ||
hooks: | ||
- id: black | ||
args: [--line-length=79] | ||
- repo: https://github.com/PyCQA/flake8 | ||
rev: 7.0.0 | ||
hooks: | ||
- id: flake8 | ||
exclude: __init__.py | ||
- repo: https://github.com/PyCQA/docformatter | ||
rev: v1.5.0 | ||
hooks: | ||
- id: docformatter |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,63 @@ | ||
### WhatsApp Webhook Analytics | ||
### WhatsApp Webhook Analytics | ||
|
||
Handling and processing Incoming webhook request configured at Glific. | ||
Handling and processing Incoming webhook request configured at Glific. | ||
|
||
## Installation | ||
|
||
### Prerequisite | ||
1. pyenv | ||
2. python 3.12 | ||
|
||
### Steps | ||
1. Clone the repository | ||
```sh | ||
git clone https://github.com/DostEducation/whatsapp-webhook-analytics.git | ||
``` | ||
2. Switch to project folder and setup the vertual environment | ||
```sh | ||
cd whatsapp-webhook-analytics | ||
python -m venv venv | ||
``` | ||
3. Activate the virtual environment | ||
|
||
**For mac-os:** | ||
```sh | ||
source ./venv/bin/activate | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's include Windows command as well. I believe we have different commands to active in MacOS vs Windows. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added command for win-os |
||
``` | ||
**For win os:** | ||
```sh | ||
venv\Scripts\Activate.ps1 | ||
``` | ||
4. Install the dependencies: | ||
```sh | ||
pip install -r requirements.txt | ||
``` | ||
5. Set up your .env file by copying .env.example | ||
```sh | ||
cp .env.example .env | ||
``` | ||
6. Add/update variables in your `.env` file for your environment. | ||
7. Run these commands to add environment variables in the system. | ||
|
||
**For Windows** | ||
```sh | ||
$env:FLASK_APP="manage.py" | ||
$env:PYTHONPATH="<Path of your project, eg: C:\Users\whatsapp-webhook-analytics>" | ||
``` | ||
**For Mac** | ||
```sh | ||
export FLASK_APP=manage.py | ||
export PYTHONPATH=path-of-the-project | ||
``` | ||
8. Upgrade DB to the latest version using this command. | ||
```sh | ||
flask db upgrade | ||
``` | ||
9. Run the following command to get started with pre-commit | ||
```sh | ||
pre-commit install | ||
``` | ||
10. Start the server by following command | ||
```sh | ||
functions_framework --target=handle_payload --debug | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from flask import Flask | ||
from flask_sqlalchemy import SQLAlchemy | ||
|
||
app = Flask(__name__) | ||
app.config.from_object("config") | ||
db = SQLAlchemy(app) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .db_helper import * |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import traceback | ||
|
||
from api import db | ||
from api.utils.loggingutils import logger | ||
|
||
|
||
def save(data): | ||
try: | ||
db.session.add(data) | ||
db.session.commit() | ||
except Exception as e: | ||
logger.error( | ||
"Error occurred while committing the data in the database." | ||
f"Error message: {e}" | ||
) | ||
logger.debug(traceback.format_exc()) | ||
db.session.rollback() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
from __future__ import absolute_import | ||
|
||
from datetime import datetime | ||
|
||
from api import db | ||
|
||
|
||
class TimestampMixin: | ||
created_on = db.Column(db.DateTime, default=datetime.now) | ||
updated_on = db.Column( | ||
db.DateTime, onupdate=datetime.now, default=datetime.now | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from sqlalchemy.ext.declarative import declarative_base | ||
|
||
Base = declarative_base() | ||
|
||
from .webhook_transaction_log import * |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
from api import db | ||
from api.mixins import TimestampMixin | ||
|
||
|
||
class WebhookTransactionLog(TimestampMixin, db.Model): | ||
|
||
__tablename__ = "webhook_transaction_log" | ||
id = db.Column(db.Integer, primary_key=True) | ||
payload = db.Column(db.Text) | ||
processed = db.Column(db.Boolean, nullable=False) | ||
attempts = db.Column(db.Integer, nullable=False, default="0") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .webhook_transaction_log_service import * |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import json | ||
|
||
from api import models | ||
from api.helpers import db_helper | ||
from api.utils.loggingutils import logger | ||
|
||
|
||
class WebhookTransactionLogService: | ||
def create_new_webhook_log(self, jsonData): | ||
try: | ||
data = json.dumps(jsonData) | ||
new_webhook_log = models.WebhookTransactionLog( | ||
payload=data, | ||
processed=False, | ||
attempts=0, | ||
) | ||
db_helper.save(new_webhook_log) | ||
return new_webhook_log | ||
except Exception as e: | ||
logger.error( | ||
f"Error while creating new webhook log. Webhook: {jsonData}." | ||
f"Error message: {e}" | ||
) | ||
return None | ||
|
||
def mark_webhook_log_as_processed(self, webhook_log): | ||
try: | ||
webhook_log.processed = True | ||
db_helper.save(webhook_log) | ||
except Exception as e: | ||
logger.error( | ||
f"Error while marking webhook log as processed." | ||
f"Error message: {e}" | ||
) | ||
return None |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .loggingutils import * |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import logging | ||
import os | ||
|
||
from google.cloud import logging as gcloud_logging | ||
|
||
from api import app | ||
|
||
logger = logging.getLogger() | ||
logging.basicConfig(level=logging.DEBUG) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we get the logging level from ENV? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, updating. |
||
|
||
if os.environ.get("FLASK_ENV", "development"): | ||
log_handler = logger.handlers[0] | ||
logger.addHandler(log_handler) | ||
else: | ||
log_client = gcloud_logging.Client() | ||
log_client.setup_logging() | ||
log_handler = log_client.get_default_handler() | ||
app.logger.addHandler(log_handler) |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,36 @@ | ||||||||||||||||||||
"""Flask configuration.""" | ||||||||||||||||||||
|
||||||||||||||||||||
import os | ||||||||||||||||||||
|
||||||||||||||||||||
FLASK_APP = os.environ.get("FLASK_APP", "development") | ||||||||||||||||||||
|
||||||||||||||||||||
if FLASK_APP == "development": | ||||||||||||||||||||
from os import path | ||||||||||||||||||||
|
||||||||||||||||||||
from dotenv import load_dotenv | ||||||||||||||||||||
|
||||||||||||||||||||
basedir = path.abspath(path.dirname(__file__)) | ||||||||||||||||||||
load_dotenv(path.join(basedir, ".env")) | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated |
||||||||||||||||||||
|
||||||||||||||||||||
|
||||||||||||||||||||
# Database configuration | ||||||||||||||||||||
POSTGRES = { | ||||||||||||||||||||
"user": os.environ.get("DB_USER"), | ||||||||||||||||||||
"password": os.environ.get("DB_PASSWORD"), | ||||||||||||||||||||
"database": os.environ.get("DB_NAME"), | ||||||||||||||||||||
"host": os.environ.get("DB_HOST"), | ||||||||||||||||||||
"port": os.environ.get("DB_PORT"), | ||||||||||||||||||||
"conn_str": os.environ.get("CONNECTION_NAME"), | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
SQLALCHEMY_DATABASE_URI = ( | ||||||||||||||||||||
"postgresql://%(user)s:%(password)s@%(host)s:%(port)s/%(database)s" | ||||||||||||||||||||
% POSTGRES | ||||||||||||||||||||
) | ||||||||||||||||||||
|
||||||||||||||||||||
# For socket based connection | ||||||||||||||||||||
if FLASK_APP in ("production", "staging"): | ||||||||||||||||||||
SQLALCHEMY_DATABASE_URI = ( | ||||||||||||||||||||
"postgresql://%(user)s:%(password)s@/%(database)s?host=%(conn_str)s/" | ||||||||||||||||||||
% POSTGRES | ||||||||||||||||||||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import functions_framework | ||
|
||
from api import app | ||
from api.services import WebhookTransactionLogService | ||
from api.utils.loggingutils import logger | ||
|
||
|
||
# Endpoint for Cloud function | ||
@functions_framework.http | ||
def handle_payload(request): | ||
if request.method == "POST": | ||
with app.app_context(): | ||
try: | ||
jsonData = request.get_json() | ||
if jsonData: | ||
handle_webhook(jsonData) | ||
except Exception as e: | ||
logger.error( | ||
f"Exception while handling the webhook payload: {jsonData}" | ||
f"Error: {e}" | ||
) | ||
return "Success" | ||
else: | ||
return "Currently, the system does not accept a GET request" | ||
|
||
|
||
def handle_webhook(jsonData): | ||
transaction_log_service = WebhookTransactionLogService() | ||
webhook_log = transaction_log_service.create_new_webhook_log(jsonData) | ||
transaction_log_service.mark_webhook_log_as_processed(webhook_log) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
from __future__ import absolute_import | ||
|
||
from flask.cli import FlaskGroup | ||
from flask_migrate import Migrate | ||
|
||
from api import app, db | ||
|
||
migrate = Migrate(app, db) | ||
cli = FlaskGroup(app) | ||
|
||
if __name__ == "__main__": | ||
cli() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Single-database configuration for Flask. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
# A generic, single database configuration. | ||
|
||
[alembic] | ||
# template used to generate migration files | ||
# file_template = %%(rev)s_%%(slug)s | ||
|
||
# set to 'true' to run the environment during | ||
# the 'revision' command, regardless of autogenerate | ||
# revision_environment = false | ||
|
||
|
||
# Logging configuration | ||
[loggers] | ||
keys = root,sqlalchemy,alembic,flask_migrate | ||
|
||
[handlers] | ||
keys = console | ||
|
||
[formatters] | ||
keys = generic | ||
|
||
[logger_root] | ||
level = WARN | ||
handlers = console | ||
qualname = | ||
|
||
[logger_sqlalchemy] | ||
level = WARN | ||
handlers = | ||
qualname = sqlalchemy.engine | ||
|
||
[logger_alembic] | ||
level = INFO | ||
handlers = | ||
qualname = alembic | ||
|
||
[logger_flask_migrate] | ||
level = INFO | ||
handlers = | ||
qualname = flask_migrate | ||
|
||
[handler_console] | ||
class = StreamHandler | ||
args = (sys.stderr,) | ||
level = NOTSET | ||
formatter = generic | ||
|
||
[formatter_generic] | ||
format = %(levelname)-5.5s [%(name)s] %(message)s | ||
datefmt = %H:%M:%S |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a few more hooks which can be helpful for our usecase
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a few more hooks in the
pre-commit config
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was it helpful so far? @Sachinbisht27
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, @Satendra-SR. For the code formatting and increasing readability of the code.