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

K-allagbe/issue21-semantic-versioning #23

Closed
wants to merge 3 commits into from
Closed
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
File renamed without changes.
Empty file added app/api/common/__init__.py
Empty file.
File renamed without changes.
9 changes: 9 additions & 0 deletions app/api/common/api_version/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from dataclasses import dataclass


@dataclass
class ApiVersion:
full: str
release_date: str
deprecated: bool
supported_until: str | None = None
File renamed without changes.
13 changes: 13 additions & 0 deletions app/api/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from flask import Blueprint

from .blueprints.monitor import monitor_blueprint
from .blueprints.search import search_blueprint
from .blueprints.version import VERSION, version_blueprint


def get_blueprint():
bp = Blueprint(VERSION.full, __name__)
bp.register_blueprint(monitor_blueprint, url_prefix="/health")
bp.register_blueprint(search_blueprint, url_prefix="/search")
bp.register_blueprint(version_blueprint, url_prefix="/version")
return bp
Empty file.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from flask import Blueprint, current_app, jsonify, request
from index_search import AzureIndexSearchQueryError, search

from app.ailab_db import DBError, ailab_db_search
from app.finesse_data import FinesseDataFetchException, fetch_data
from app.api.common.ailab_db import DBError, ailab_db_search
from app.api.common.finesse_data import FinesseDataFetchException, fetch_data
from app.utils import sanitize

search_blueprint = Blueprint("finesse", __name__)
Expand Down
13 changes: 13 additions & 0 deletions app/api/v1/blueprints/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from dataclasses import asdict

from flask import Blueprint, jsonify

from app.api.common.api_version import ApiVersion

VERSION = ApiVersion(full="1", release_date="2023-12", deprecated=False)
version_blueprint = Blueprint("version", __name__)


@version_blueprint.route("", methods=["GET"])
def version():
return jsonify({"version": asdict(VERSION)})
13 changes: 13 additions & 0 deletions app/api/v2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from flask import Blueprint

from .blueprints.monitor import monitor_blueprint
from .blueprints.search import search_blueprint
from .blueprints.version import VERSION, version_blueprint


def get_blueprint():
bp = Blueprint(VERSION.full, __name__)
bp.register_blueprint(monitor_blueprint, url_prefix="/health")
bp.register_blueprint(search_blueprint, url_prefix="/search")
bp.register_blueprint(version_blueprint, url_prefix="/version")
return bp
Empty file.
8 changes: 8 additions & 0 deletions app/api/v2/blueprints/monitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from flask import Blueprint

monitor_blueprint = Blueprint("monitor", __name__)


@monitor_blueprint.route("", methods=["GET"])
def health():
return "ok", 200
65 changes: 65 additions & 0 deletions app/api/v2/blueprints/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from functools import wraps

from flask import Blueprint, current_app, jsonify, request
from index_search import AzureIndexSearchQueryError, search

from app.api.common.ailab_db import DBError, ailab_db_search
from app.api.common.finesse_data import FinesseDataFetchException, fetch_data
from app.utils import sanitize

search_blueprint = Blueprint("finesse", __name__)


def require_non_empty_query(f):
@wraps(f)
def decorated_function(*args, **kwargs):
query = request.json.get("query")
if not query:
return jsonify({"message": current_app.config["ERROR_EMPTY_QUERY"]}), 400
return f(*args, **kwargs)

return decorated_function


@search_blueprint.route("/azure", methods=["POST"])
@require_non_empty_query
def search_azure():
query = request.json["query"]
query = sanitize(query, current_app.config["SANITIZE_PATTERN"])
try:
results = search(query, current_app.config["AZURE_CONFIG"])
return jsonify(results)
except AzureIndexSearchQueryError:
return jsonify({"error": current_app.config["ERROR_AZURE_FAILED"]}), 500
except Exception:
return jsonify({"error": current_app.config["ERROR_UNEXPECTED"]}), 500


@search_blueprint.route("/static", methods=["POST"])
@require_non_empty_query
def search_static():
finesse_data_url = current_app.config["FINESSE_DATA_URL"]
query = request.json["query"]
query = sanitize(query, current_app.config["SANITIZE_PATTERN"])
match_threshold = current_app.config["FUZZY_MATCH_THRESHOLD"]
try:
data = fetch_data(finesse_data_url, query, match_threshold)
return jsonify(data)
except FinesseDataFetchException:
return jsonify({"error": current_app.config["ERROR_FINESSE_DATA_FAILED"]}), 500
except Exception:
return jsonify({"error": current_app.config["ERROR_UNEXPECTED"]}), 500


@search_blueprint.route("/ailab", methods=["POST"])
@require_non_empty_query
def search_ailab_db():
query = request.json["query"]
query = sanitize(query, current_app.config["SANITIZE_PATTERN"])
try:
results = ailab_db_search(query)
return jsonify(results)
except DBError:
return jsonify({"error": current_app.config["ERROR_AILAB_FAILED"]}), 500
except Exception:
return jsonify({"error": current_app.config["ERROR_UNEXPECTED"]}), 500
13 changes: 13 additions & 0 deletions app/api/v2/blueprints/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from dataclasses import asdict

from flask import Blueprint, jsonify

from app.api.common.api_version import ApiVersion

VERSION = ApiVersion(full="2", release_date="2024-01", deprecated=False)
version_blueprint = Blueprint("version", __name__)


@version_blueprint.route("", methods=["GET"])
def version():
return jsonify({"version": asdict(VERSION)})
9 changes: 5 additions & 4 deletions app/app_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ def create_app(config: Config):
CORS(app)
app.config.from_object(config)

from .blueprints.monitor import monitor_blueprint
from .blueprints.search import search_blueprint
from .api import v1, v2

app.register_blueprint(
monitor_blueprint, url_prefix="/health", strict_slashes=False
v1.get_blueprint(), url_prefix=f"/api/v{v1.VERSION.full}", strict_slashes=False
)
app.register_blueprint(
v2.get_blueprint(), url_prefix=f"/api/v{v2.VERSION.full}", strict_slashes=False
)
app.register_blueprint(search_blueprint, url_prefix="/search", strict_slashes=False)

return app
36 changes: 0 additions & 36 deletions tests/test_ailab_search.py

This file was deleted.

115 changes: 115 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import unittest
from unittest.mock import patch

from app.api.common.ailab_db import DBError
from app.app_creator import create_app
from tests.common import TestConfig


class TestApiV1(unittest.TestCase):
def setUp(self):
self.config = TestConfig()
self.app = create_app(self.config)
self.client = self.app.test_client()
self.static_search_url = "/api/v1/search/static"
self.azure_search_url = "/api/v1/search/azure"
self.ailab_search_url = "/api/v1/search/ailab"
self.health_check_url = "/api/v1/health"
self.version_url = "/api/v1/version"

### Static ###
def test_search_static_success(self):
with patch("app.api.v1.blueprints.search.fetch_data") as mock_fetch:
mock_fetch.return_value = {"some": "data"}
json = {"query": "test query"}
response = self.client.post(self.static_search_url, json=json)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, {"some": "data"})

def test_search_static_no_query(self):
response = self.client.post(self.static_search_url, json={})
self.assertEqual(response.status_code, 400)

def test_search_static_no_match(self):
with patch("app.api.v1.blueprints.search.fetch_data") as mock_fetch:
mock_fetch.return_value = None
json = {"query": "test query"}
response = self.client.post(self.static_search_url, json=json)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, None)

def test_search_static_error(self):
with patch("app.api.v1.blueprints.search.fetch_data") as mock_fetch:
mock_fetch.side_effect = Exception("API request failed")
json = {"query": "test query"}
response = self.client.post(self.static_search_url, json=json)
self.assertEqual(response.status_code, 500)

### Azure ###
def test_search_azure_success(self):
with patch("app.api.v1.blueprints.search.search") as mock_search:
mock_search.return_value = {"some": "azure data"}
json = {"query": "azure query"}
response = self.client.post(self.azure_search_url, json=json)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, {"some": "azure data"})

def test_search_azure_no_query(self):
response = self.client.post(self.azure_search_url, json={})
self.assertEqual(response.status_code, 400)

def test_search_azure_error(self):
with patch("app.api.v1.blueprints.search.search") as mock_search:
mock_search.side_effect = Exception("Azure search failed")
json = {"query": "azure query"}
response = self.client.post(self.azure_search_url, json=json)
self.assertEqual(response.status_code, 500)

### Ailab ###
def test_search_ailab_success(self):
with patch("app.api.v1.blueprints.search.ailab_db_search") as mock_search:
mock_search.return_value = {"some": "ailab data"}
json = {"query": "ailab query"}
response = self.client.post(self.ailab_search_url, json=json)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, {"some": "ailab data"})

def test_search_ailab_no_query(self):
response = self.client.post(self.ailab_search_url, json={})
self.assertEqual(response.status_code, 400)

def test_search_ailab_db_error(self):
with patch("app.api.v1.blueprints.search.ailab_db_search") as mock_search:
mock_search.side_effect = DBError("Ailab DB failed")
json = {"query": "ailab query"}
response = self.client.post(self.ailab_search_url, json=json)
self.assertEqual(response.status_code, 500)

def test_search_ailab_unexpected_error(self):
with patch("app.api.v1.blueprints.search.ailab_db_search") as mock_search:
mock_search.side_effect = Exception("Unexpected error")
json = {"query": "ailab query"}
response = self.client.post(self.ailab_search_url, json=json)
self.assertEqual(response.status_code, 500)

### Health ###
def test_health_route(self):
response = self.client.get(self.health_check_url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data.decode(), "ok")

### Version ###
def test_version_route(self):
response = self.client.get(self.version_url)
self.assertEqual(response.status_code, 200)
self.assertDictEqual(
response.json,
{
"version": {
"deprecated": False,
"full": "1",
"release_date": "2023-12",
"supported_until": None,
}
},
)
29 changes: 0 additions & 29 deletions tests/test_azure_search.py

This file was deleted.

10 changes: 5 additions & 5 deletions tests/test_finesse_data_fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import requests

from app.finesse_data import (
from app.api.common.finesse_data import (
EmptyQueryError,
FinesseDataFetchException,
fetch_data,
Expand All @@ -26,18 +26,18 @@ def setUp(self):
"Product Catalog",
]

@patch("app.finesse_data.requests.get")
@patch("app.api.common.finesse_data.requests.get")
def test_fetch_data_empty_query(self, mock_get):
with self.assertRaises(EmptyQueryError):
fetch_data(self.finesse_data_url, "", self.match_threshold)

@patch("app.finesse_data.requests.get")
@patch("app.api.common.finesse_data.requests.get")
def test_fetch_data_no_match_found(self, mock_get):
mock_get.return_value = Mock(status_code=200, json=lambda: self.files)
result = fetch_data(self.finesse_data_url, "bad query", self.match_threshold)
self.assertIsNone(result)

@patch("app.finesse_data.requests.get")
@patch("app.api.common.finesse_data.requests.get")
def test_fetch_data_success(self, mock_get):
mock_get.side_effect = [
Mock(status_code=200, json=lambda: self.files),
Expand All @@ -46,7 +46,7 @@ def test_fetch_data_success(self, mock_get):
result = fetch_data(self.finesse_data_url, "file1", self.match_threshold)
self.assertEqual(result, {"data": "content"})

@patch("app.finesse_data.requests.get")
@patch("app.api.common.finesse_data.requests.get")
def test_fetch_data_request_exception(self, mock_get):
mock_get.side_effect = requests.RequestException()
with self.assertRaises(FinesseDataFetchException):
Expand Down
Loading
Loading