Skip to content
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

Merged
merged 22 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6c1ee3a
scaffolding fastapi app for whatsapp analytics
Sachinbisht27 Apr 13, 2024
dd9f22e
merged requirements files
Sachinbisht27 Apr 15, 2024
a42c908
created database conectivity to the app
Sachinbisht27 Apr 15, 2024
94f9f2f
added logger in the application
Sachinbisht27 Apr 15, 2024
0e9aa9f
added logger in the application
Sachinbisht27 Apr 15, 2024
701d27f
Updated CI checks for the better coding standards
Sachinbisht27 Apr 15, 2024
bb309cd
Fixed error: Invalid base class 'Base'.
Sachinbisht27 Apr 15, 2024
a21de5f
Updated gitignore
Sachinbisht27 Apr 15, 2024
8bbf271
Added migrations for maintaining the database schemas through automation
Sachinbisht27 Apr 15, 2024
d45eac2
Added migrations for maintaining the database schemas through automation
Sachinbisht27 Apr 15, 2024
fb8ed03
Upgraded the packages to the latest
Sachinbisht27 Apr 15, 2024
d6652a5
Updated return messages
Sachinbisht27 Apr 16, 2024
8848671
Updating FastAPI to Flask
Sachinbisht27 Apr 17, 2024
21773af
upgrded fastapi app to flask app
Sachinbisht27 Apr 18, 2024
a51fef1
upgrded fastapi app to flask app
Sachinbisht27 Apr 18, 2024
88188c6
upgrded fastapi app to flask app
Sachinbisht27 Apr 18, 2024
23bd540
added flask migration
Sachinbisht27 Apr 18, 2024
63abb51
added more pre-commit hooks
Sachinbisht27 Apr 18, 2024
aef62ff
updated readme.md
Sachinbisht27 Apr 18, 2024
db8d89e
updated readme.md
Sachinbisht27 Apr 18, 2024
c48fa2b
updated suggested chages
Sachinbisht27 Apr 18, 2024
5c47b4b
updated suggested chages
Sachinbisht27 Apr 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ENVIRONMENT=
DB_USER=
DB_PASSWORD=
DB_NAME=
DB_HOST=
DB_PORT=
16 changes: 16 additions & 0 deletions .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: pre-commit

on:
pull_request:
push:
branches: [main]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add develop branch as well

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated


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]
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
*.pyc
__pycache__/
.mypy_cache

# Environments
.env
.venv
env/
venv/
alembic.ini
25 changes: 25 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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
Comment on lines +5 to +10
Copy link
Member

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

  1. flake8 - Purpose: Runs the Flake8 code linter to check for PEP 8 violations and other style issues.
  2. black - Purpose: Applies the Black code formatter to format Python code according to PEP 8 standards.
  3. isort - Purpose: Sorts import statements in Python files according to PEP 8 guidelines.
  4. mypy - Purpose: Runs the MyPy static type checker to find type errors in Python code.
  5. docformatter - Purpose: Formats docstrings in Python files to adhere to PEP 257 guidelines.

Copy link
Member Author

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.

Copy link
Member

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

Copy link
Member Author

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.

- 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
42 changes: 40 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
### 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
```sh
source ./venv/bin/activate
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added command for win-os

```
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 the following command to get started with pre-commit
```sh
pre-commit install
```
8. Start the server by following command
```sh
functions_framework --target=handle_payload --debug
```
1 change: 1 addition & 0 deletions alembic/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Generic single-database configuration.
75 changes: 75 additions & 0 deletions alembic/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from logging.config import fileConfig

from sqlalchemy import engine_from_config, pool

from alembic import context

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)

# add your model's MetaData object here
# for 'autogenerate' support
from app import models

target_metadata = models.Base.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.


def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.

This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.

Calls to context.execute() here emit the given string to the
script output.

"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)

with context.begin_transaction():
context.run_migrations()


def run_migrations_online() -> None:
"""Run migrations in 'online' mode.

In this scenario we need to create an Engine
and associate a connection with the context.

"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)

with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)

with context.begin_transaction():
context.run_migrations()


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
26 changes: 26 additions & 0 deletions alembic/script.py.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
${imports if imports else ""}

# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}


def upgrade() -> None:
${upgrades if upgrades else "pass"}


def downgrade() -> None:
${downgrades if downgrades else "pass"}
40 changes: 40 additions & 0 deletions alembic/versions/19b23a59f196_initial_migrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""initial migrations

Revision ID: 19b23a59f196
Revises:
Create Date: 2024-04-15 18:22:25.267651

"""

from typing import Sequence, Union

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "19b23a59f196"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"webhook_transaction_log",
sa.Column("created_on", sa.DateTime(), nullable=True),
sa.Column("updated_on", sa.DateTime(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("payload", sa.Text(), nullable=True),
sa.Column("processed", sa.Boolean(), nullable=False),
sa.Column("attempts", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("webhook_transaction_log")
# ### end Alembic commands ###
1 change: 1 addition & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from app.mixins import *
1 change: 1 addition & 0 deletions app/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from app.helpers.db_helper import *
10 changes: 10 additions & 0 deletions app/helpers/db_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

from config import SQLALCHEMY_DATABASE_URL

engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
db = SessionLocal()
13 changes: 13 additions & 0 deletions app/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from datetime import datetime

from sqlalchemy import Column, DateTime
from sqlalchemy.ext.declarative import declared_attr


class TimestampMixin:
created_on = Column(DateTime, default=datetime.utcnow)
updated_on = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

@declared_attr
def __tablename__(cls):
return cls.__name__.lower()
2 changes: 2 additions & 0 deletions app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from app.models.base import *
from app.models.webhook_transaction_log import *
3 changes: 3 additions & 0 deletions app/models/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()
13 changes: 13 additions & 0 deletions app/models/webhook_transaction_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from sqlalchemy import Boolean, Column, Integer, Text

from app import TimestampMixin
from app.models.base import Base


class WebhookTransactionLog(TimestampMixin, Base):
__tablename__ = "webhook_transaction_log"

id = Column(Integer, primary_key=True)
payload = Column(Text)
processed = Column(Boolean, nullable=False)
attempts = Column(Integer, nullable=False, default=0)
1 change: 1 addition & 0 deletions app/services/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from app.services.webhook_transaction_log_service import *
37 changes: 37 additions & 0 deletions app/services/webhook_transaction_log_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import json

from app import models
from app.helpers import db
from app.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.add(new_webhook_log)
db.commit()
return new_webhook_log
except Exception as e:
logger.error(
f"Error while creating new webhook log. Webhook: {jsonData}. Error message: {e}"
)
finally:
db.close()

def mark_webhook_log_as_processed(self, webhook_log):
try:
webhook_log.processed = True
db.add(webhook_log)
db.commit()
except Exception as e:
logger.error(
f"Error while marking webhook log as processed. Error message: {e}"
)
finally:
db.close()
1 change: 1 addition & 0 deletions app/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from app.utils.loggingutils import *
17 changes: 17 additions & 0 deletions app/utils/loggingutils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import logging

from google.cloud import logging as gcloud_logging

from config import ENVIRONMENT

logger = logging.getLogger()
logging.basicConfig(level=logging.DEBUG)

if ENVIRONMENT == "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()
logger.addHandler(log_handler)
20 changes: 20 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import os

from dotenv import load_dotenv

load_dotenv()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think we need this on cloud function. Can we out it under if condition? If ENV is development, then load this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, @Satendra-SR Moved the load_dotenv() to the environment condition.


ENVIRONMENT = os.environ.get("ENVIRONMENT", "development")

# Database configuration
POSTGRES = {
"user": os.getenv("DB_USER"),
"password": os.getenv("DB_PASSWORD"),
"database": os.getenv("DB_NAME"),
"host": os.getenv("DB_HOST"),
"port": os.getenv("DB_PORT"),
}

SQLALCHEMY_DATABASE_URL = (
"postgresql://%(user)s:%(password)s@%(host)s:%(port)s/%(database)s" % POSTGRES
)
Loading
Loading