Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions .github/workflows/bad-name-notifier-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ on:
- "false"
- "true"

permissions:
id-token: write
contents: write

jobs:
namex-bad-name-notifier-cd:
uses: bcgov/bcregistry-sre/.github/workflows/backend-job-cd.yaml@main
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/bad-name-notifier-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ defaults:
shell: bash
working-directory: ./jobs/bad-name-notifier

permissions:
id-token: write
contents: write

jobs:
namex-bad-name-notifier-ci:
uses: bcgov/bcregistry-sre/.github/workflows/backend-ci.yaml@main
with:
app_name: "namex-bad-name-notifier"
working_directory: "./jobs/bad-name-notifier"
codecov_flag: "namexbadnamenotifier"
skip_isort: "true"
skip_black: "true"
codecov_flag: "namex-bad-name-notifier"
8 changes: 8 additions & 0 deletions jobs/bad-name-notifier/.flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[flake8]
max-line-length = 80
extend-ignore = E501
exclude =
.venv,
.git,
migrations,
tests
32 changes: 30 additions & 2 deletions jobs/bad-name-notifier/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,33 @@ Bad-Name-Notifier is a Python application designed to identify names with specia

1. Clone the repository:
```bash
git clone https://github.com/your-repo/bad-name-notifier.git
cd bad-name-notifier
git clone https://github.com/your-repo/namex.git
cd jobs/bad-name-notifier

2 ### Install the dependencies
```bash
poetry install
```

3 ### Configure the .env
(see .env.sample)

```bash
eval $(poetry env activate)
```

4 ### Run the job
```bash
python src/bad_name_notifier/app.py
OR: ./run.sh
```

5 ### Run Linting
```bash
poetry run ruff check --fix
```

6 ### Run unit tests
```bash
poetry run pytest
```
Empty file.
56 changes: 56 additions & 0 deletions jobs/bad-name-notifier/coverage.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?xml version="1.0" ?>
<coverage version="7.13.4" timestamp="1771437447922" lines-valid="37" lines-covered="36" line-rate="0.973" branches-valid="4" branches-covered="3" branch-rate="0.75" complexity="0">
<!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.13.4 -->
<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
<sources>
<source>/mnt/c/dsk01/lab/temp/namex/jobs/bad-name-notifier/src</source>
</sources>
<packages>
<package name="." line-rate="0.973" branch-rate="0.75" complexity="0">
<classes>
<class name="config.py" filename="config.py" complexity="0" line-rate="0.973" branch-rate="0.75">
<methods/>
<lines>
<line number="4" hits="1"/>
<line number="6" hits="1"/>
<line number="9" hits="1"/>
<line number="12" hits="1"/>
<line number="15" hits="1"/>
<line number="19" hits="1"/>
<line number="20" hits="1"/>
<line number="21" hits="1"/>
<line number="22" hits="1"/>
<line number="23" hits="1"/>
<line number="25" hits="1"/>
<line number="26" hits="1"/>
<line number="27" hits="1"/>
<line number="29" hits="1" branch="true" condition-coverage="50% (1/2)" missing-branches="34"/>
<line number="32" hits="1"/>
<line number="34" hits="0"/>
<line number="39" hits="1"/>
<line number="40" hits="1"/>
<line number="45" hits="1"/>
<line number="46" hits="1"/>
<line number="47" hits="1"/>
<line number="50" hits="1"/>
<line number="51" hits="1"/>
<line number="54" hits="1"/>
<line number="57" hits="1"/>
<line number="58" hits="1"/>
<line number="61" hits="1"/>
<line number="64" hits="1"/>
<line number="65" hits="1"/>
<line number="68" hits="1"/>
<line number="71" hits="1"/>
<line number="72" hits="1"/>
<line number="76" hits="1"/>
<line number="84" hits="1"/>
<line number="89" hits="1" branch="true" condition-coverage="100% (2/2)"/>
<line number="90" hits="1"/>
<line number="91" hits="1"/>
</lines>
</class>
</classes>
</package>
</packages>
</coverage>
375 changes: 370 additions & 5 deletions jobs/bad-name-notifier/poetry.lock

Large diffs are not rendered by default.

31 changes: 25 additions & 6 deletions jobs/bad-name-notifier/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
[tool.poetry]
name = "bad-name-notifier"
version = "0.2.0"
version = "0.3.0"
description = "An app to detect and notify about bad names."
authors = ["Your Name <your.email@example.com>"]
license = "Apache-2.0"
readme = "README.md"
packages = [
{ include = "services", from = "src" },
{ include = "src" }
]
packages = [{ include = "*", from = "src" }]

[tool.poetry.dependencies]
python = ">=3.12,<3.13"
namex = { git = "https://github.com/bcgov/namex.git", subdirectory = "api", branch = "main" }
namex = { git = "https://github.com/bcgov/namex.git", subdirectory = "api", branch = "main" }
[tool.poetry.group.test.dependencies]
pytest = "^7.4.4"
pytest-cov = "^5.0.0"
pytest-mock = "^3.14.0"

[tool.poetry.group.dev.dependencies]
pylint = "^3.3.1"
flake8 = "^7.1.1"
isort = "^5.13.2"
autopep8 = "^2.3.2"

[tool.coverage.run]
branch = true
source = [
"namex_solr_importer",
]
omit = [
"*/.venv/*",
"*/__init__.py",
"*/app.py",
"*/services/*",
]
22 changes: 14 additions & 8 deletions jobs/bad-name-notifier/src/app.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
from config import APP_CONFIG
"""Application factory and configuration setup for the Bad Name Notifier service."""

from flask import Flask, current_app
from structured_logging import StructuredLogging

from config import get_named_config
from services.database_service import get_bad_names
from services.email_service import send_email_notification
from structured_logging import StructuredLogging


def create_app(config_name="default"):
"""Creates and configures the Flask app."""

app = Flask(__name__) # NOSONAR
app.config.from_object(APP_CONFIG[config_name])
flask_app = Flask(__name__) # NOSONAR
flask_app.config.from_object(get_named_config(config_name))

# Configure Structured Logging
structured_logger = StructuredLogging()
structured_logger.init_app(app)
app.logger = structured_logger.get_logger()
structured_logger.init_app(flask_app)
flask_app.logger = structured_logger.get_logger()

return flask_app

return app

def run_task():
"""Executes the task to query bad names and send an email."""
Expand All @@ -26,9 +31,10 @@ def run_task():
# Step 2: Send email
send_email_notification(bad_names)
current_app.logger.info("Notification sent successfully.")
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
current_app.logger.error(f"An error occurred: {e}")


if __name__ == "__main__":
app = create_app()
with app.app_context(): # Ensures Flask app context is available
Expand Down
46 changes: 30 additions & 16 deletions jobs/bad-name-notifier/src/config.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,47 @@
"""Application configuration module for the Bad Name Notifier service."""

# pylint: disable=too-few-public-methods
import os

from dotenv import find_dotenv, load_dotenv
from dotenv import load_dotenv

# Get the project root directory
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Load the .env file from the project root
load_dotenv(os.path.join(BASE_DIR, '.env'))
load_dotenv(os.path.join(BASE_DIR, ".env"))


class Config:
"""Base configuration class."""

# Database Configuration
DB_USER = os.getenv('DATABASE_USERNAME', 'postgres')
DB_PASSWORD = os.getenv('DATABASE_PASSWORD', 'postgres')
DB_NAME = os.getenv('DATABASE_NAME', 'unittesting')
DB_HOST = os.getenv('DATABASE_HOST', 'localhost')
DB_PORT = os.getenv('DATABASE_PORT', '5432')

DB_SCHEMA = os.getenv('DATABASE_SCHEMA', 'public')
DB_IP_TYPE = os.getenv('DATABASE_IP_TYPE', 'private')
DB_OWNER = os.getenv('DATABASE_OWNER', 'postgres')

if DB_INSTANCE_CONNECTION_NAME := os.getenv('DATABASE_INSTANCE_CONNECTION_NAME', None):
SQLALCHEMY_DATABASE_URI = 'postgresql+pg8000://'
DB_USER = os.getenv("DATABASE_USERNAME", "")
DB_PASSWORD = os.getenv("DATABASE_PASSWORD", "")
DB_NAME = os.getenv("DATABASE_NAME", "")
DB_HOST = os.getenv("DATABASE_HOST", "")
DB_PORT = os.getenv("DATABASE_PORT", "5432")

DB_SCHEMA = os.getenv("DATABASE_SCHEMA", "")
DB_IP_TYPE = os.getenv("DATABASE_IP_TYPE", "")
DB_OWNER = os.getenv("DATABASE_OWNER", "")

if DB_INSTANCE_CONNECTION_NAME := os.getenv(
"DATABASE_INSTANCE_CONNECTION_NAME", None
):
SQLALCHEMY_DATABASE_URI = "postgresql+pg8000://"
else:
SQLALCHEMY_DATABASE_URI = f'postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}'
SQLALCHEMY_DATABASE_URI = (
f"postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
)

# Email Configuration
EMAIL_RECIPIENTS = os.getenv("EMAIL_RECIPIENTS", "").split(",")
NOTIFY_API_URL = f"{os.getenv("NOTIFY_API_URL", "") + os.getenv("NOTIFY_API_VERSION", "")}/notify"
NOTIFY_API_URL = (
f"{os.getenv('NOTIFY_API_URL', '')}"
f"{os.getenv('NOTIFY_API_VERSION', '')}"
"/notify"
)
ACCOUNT_SVC_AUTH_URL = os.getenv("KEYCLOAK_AUTH_TOKEN_URL", "")
ACCOUNT_SVC_CLIENT_ID = os.getenv("KEYCLOAK_CLIENT_ID", "")
ACCOUNT_SVC_CLIENT_SECRET = os.getenv("KEYCLOAK_CLIENT_SECRET", "")
Expand All @@ -42,18 +53,21 @@ class Config:

class DevConfig(Config):
"""Development-specific configuration."""

DEBUG = True
TESTING = False


class TestConfig(Config):
"""Testing-specific configuration."""

DEBUG = True
TESTING = True


class ProdConfig(Config):
"""Production-specific configuration."""

DEBUG = False
TESTING = False

Expand Down
2 changes: 1 addition & 1 deletion jobs/bad-name-notifier/src/services/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# services/__init__.py
"""Service layer package for the Bad Name Notifier application."""

from .database_service import get_bad_names
from .email_service import send_email_notification
Expand Down
39 changes: 22 additions & 17 deletions jobs/bad-name-notifier/src/services/database_service.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
from cloud_sql_connector import DBConfig, getconn
"""Database service for retrieving bad names from the data source."""

from flask import current_app
from google.cloud.sql.connector import Connector

from .utils import get_yesterday_utc_range


def getconn():
"""Create and return a database connection using Cloud SQL Connector.
Returns:
tuple: A tuple containing (connection, connector instance).
"""
connector = Connector()
conn = connector.connect(
current_app.config.get("DB_INSTANCE_CONNECTION_NAME"),
"pg8000", # driver
user=current_app.config.get("DB_USER"),
db=current_app.config.get("DB_NAME"),
ip_type=current_app.config.get("DB_IP_TYPE", "private"),
enable_iam_auth=True, # 🔥 REQUIRED
)
return conn, connector


def get_bad_names() -> list[dict]:
"""
Fetch bad names from the database and return as a list of dictionaries.
Expand All @@ -14,22 +33,7 @@ def get_bad_names() -> list[dict]:
- Contains non-standard ASCII characters
- Event occurred after the start of yesterday UTC
"""
schema = current_app.config.get("DB_SCHEMA", "public")

db_config = DBConfig(
instance_name=current_app.config.get("DB_INSTANCE_CONNECTION_NAME"),
database=current_app.config.get("DB_NAME"),
user=current_app.config.get("DB_USER"),
ip_type=current_app.config.get("DB_IP_TYPE", "private"),
schema=schema,
pool_recycle=300,
)

# Ensure required fields are set
if not all([db_config.instance_name, db_config.database, db_config.user]):
raise ValueError("DBConfig fields instance_name, database, and user must be set")

conn = getconn(db_config)
conn, connector = getconn()
cursor = conn.cursor()

start_of_yesterday_utc, start_of_today_utc = get_yesterday_utc_range()
Expand Down Expand Up @@ -73,3 +77,4 @@ def get_bad_names() -> list[dict]:
finally:
cursor.close()
conn.close()
connector.close()
Loading
Loading