Skip to content
Merged
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
134 changes: 82 additions & 52 deletions src/ogd/apis/models/APIRequest.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,88 @@
import logging
import requests
from typing import Any, Dict, Optional
from urllib.parse import urlparse, urlunparse, ParseResult

import requests
from flask import current_app

from ogd.apis.models.enums.RESTType import RESTType
from ogd.apis.models.enums.ResponseStatus import ResponseStatus
from ogd.apis.models.APIResponse import APIResponse

class APIRequest:
def __init__(self, url:str, request_type:str | RESTType, params:Optional[Dict[str, Any]]=None, body:Optional[Dict[str, Any]]=None, timeout:int=1):
"""Utility function to make it easier to send requests to a remote server during unit testing.

This function does some basic sanity checking of the target URL,
maps the request type to the appropriate `requests` function call,
and performs basic error handling to notify what error occurred.

:param url: The target URL for the web request
:type url: str
:param request: Whether to perform a "GET", "POST", or "PUT" request
:type request: str
:param params: A mapping of request parameter names to values. Defaults to {}
:type params: Dict[str, Any], optional
:param body: The body of the request to send. Defaults to None
:type body: Dict[str, Any], optional
:param logger: A logger to use for debug/error outputs. Defaults to None
:type logger: logging.Logger, optional
:raises err: Currently, any exceptions that occur during the request will be raised up.
If verbose logging is on, a simple debug message indicating the request type and URL is printed first.
:return: The `Response` object from the request, or None if an error occurred.
:rtype: requests.Response
"""
params = params or {}

self._request_type : RESTType

if not url.startswith("http://") or url.startswith("https://"):
url = f"https://{url}"
if isinstance(request_type, RESTType):
self._request_type = request_type
else:
try:
self._request_type = RESTType[request_type]
except KeyError:
current_app.logger.warning(f"Bad request type {request_type}, defaulting to GET")
self._request_type = RESTType.GET

self._url = url
self._params = params
self._body = body
self._timeout = timeout


def Execute(self, logger:Optional[logging.Logger]=None) -> APIResponse:
ret_val : APIResponse

if logger is None and current_app:
logger = current_app.logger

def APIRequest(url:str, request:str, params:Optional[Dict[str, Any]]=None, body:Optional[Dict[str, Any]]=None, timeout:int=1, logger:Optional[logging.Logger]=None) -> requests.Response:
"""Utility function to make it easier to send requests to a remote server during unit testing.

This function does some basic sanity checking of the target URL,
maps the request type to the appropriate `requests` function call,
and performs basic error handling to notify what error occurred.

:param url: The target URL for the web request
:type url: str
:param request: Whether to perform a "GET", "POST", or "PUT" request
:type request: str
:param params: A mapping of request parameter names to values. Defaults to {}
:type params: Dict[str, Any], optional
:param body: The body of the request to send. Defaults to None
:type body: Dict[str, Any], optional
:param logger: A logger to use for debug/error outputs. Defaults to None
:type logger: logging.Logger, optional
:raises err: Currently, any exceptions that occur during the request will be raised up.
If verbose logging is on, a simple debug message indicating the request type and URL is printed first.
:return: The `Response` object from the request, or None if an error occurred.
:rtype: requests.Response
"""
ret_val : requests.Response

params = params or {}

if not (url.startswith("https://") or url.startswith("http://")):
url = f"https://{url}" # give url a default scheme
try:
match (request.upper()):
case "GET":
ret_val = requests.get(url, params=params, timeout=timeout)
case "POST":
ret_val = requests.post(url, params=params, data=body, timeout=timeout)
case "PUT":
ret_val = requests.put(url, params=params, data=body, timeout=timeout)
case _:
if logger:
logger.warning(f"Bad request type {request}, defaulting to GET")
ret_val = requests.get(url, params=params, timeout=timeout)
except Exception as err:
if logger:
logger.error(f"Error on {request} request to {url} : {err}")
raise err
else:
if logger:
out = logger.debug if ret_val.status_code == ResponseStatus.OK else logger.warning
out(f"Request sent to: {url}")
out(f"Response received from: {ret_val.url}")
out(f" Status: {ret_val.status_code}")
out(f" Data: {ret_val.text}")
response : requests.Response
try:
match (self._request_type):
case RESTType.GET:
response = requests.get( self._url, params=self._params, timeout=self._timeout)
case RESTType.POST:
response = requests.post(self._url, params=self._params, data=self._body, timeout=self._timeout)
case RESTType.PUT:
response = requests.put( self._url, params=self._params, data=self._body, timeout=self._timeout)
case _:
if logger:
logger.warning(f"Bad request type {self._request_type}, defaulting to GET")
response = requests.get(self._url, params=self._params, timeout=self._timeout)
except Exception as err:
if logger:
logger.error(f"Error on {self._request_type} request to {self._url} : {err}")
raise err
else:
ret_val = APIResponse.FromResponse(response)
if logger:
out = logger.debug if ret_val.Status == ResponseStatus.OK else logger.warning
out(f"Request sent to: {self._url}")
out(f"Response received from: {self._url}")
out(f" Status: {ret_val.Status}")
out(f" Msg: {ret_val.Message}")
out(f" Value: {ret_val.Value}")
return ret_val
23 changes: 21 additions & 2 deletions src/ogd/apis/models/APIResponse.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from typing import Any, Dict, Optional

# import 3rd-party libraries
import requests
from flask import Response

# import OGD libraries
Expand All @@ -21,8 +22,14 @@
from ogd.apis.models.enums.ResponseStatus import ResponseStatus

class APIResponse:
def __init__(self, req_type:Optional[RESTType], val:Optional[Map], msg:str, status:ResponseStatus):
self._type : Optional[RESTType] = req_type
def __init__(self, req_type:Optional[RESTType | str], val:Optional[Map], msg:str, status:ResponseStatus):
self._type : Optional[RESTType]
if isinstance(req_type, RESTType):
self._type = req_type
elif isinstance(req_type, str):
self._type = RESTType[req_type]
else:
self._type = None
self._val : Optional[Map] = val
self._msg : str = msg
self._status : ResponseStatus = status
Expand Down Expand Up @@ -63,6 +70,18 @@ def FromRequestResult(result:RequestResult.RequestResult, req_type:RESTType) ->
_status = ResponseStatus.INTERNAL_ERR
ret_val = APIResponse(req_type=req_type, val={"session_count":result.SessionCount, "duration":str(result.Duration)}, msg=result.Message, status=_status)
return ret_val

@staticmethod
def FromResponse(result:requests.Response) -> "APIResponse":
ret_val : APIResponse

try:
raw = result.json()
ret_val = APIResponse(req_type=raw.get("type"), val=raw.get("val"), msg=raw.get("msg"), status=ResponseStatus(result.status_code))
except requests.exceptions.JSONDecodeError:
ret_val = APIResponse(req_type=None, val=None, msg=result.text, status=ResponseStatus(result.status_code))

return ret_val

@staticmethod
def FromDict(all_elements:Dict[str, Any], status:Optional[ResponseStatus]=None) -> Optional["APIResponse"]:
Expand Down
48 changes: 16 additions & 32 deletions tests/cases/apis/HelloAPI/remote/t_Hello.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# import libraries
import json
import logging
from unittest import TestCase
# import 3rd-party libraries
# import ogd-core libraries.
from ogd.common.configs.TestConfig import TestConfig
from ogd.common.utils.Logger import Logger
# import locals
from src.ogd.apis.models.APIRequest import APIRequest
from ogd.apis.models.APIRequest import APIRequest
from tests.config.t_config import settings

class t_Hello_remote(TestCase):
Expand All @@ -24,54 +23,39 @@ def setUpClass(cls) -> None:
def test_get(self):
_url = f"{self.base_url}/hello"
try:
result = APIRequest(url=_url, request="GET", params={}, logger=Logger.std_logger)
result = APIRequest(url=_url, request_type="GET", params={}).Execute(logger=Logger.std_logger)
except Exception as err:
self.fail(str(err))
else:
self.assertNotEqual(result, None, f"No response from {_url}")
self.assertEqual(result.status_code, 200, f"Bad status from {_url}")
try:
body = json.loads(result.text)
except json.decoder.JSONDecodeError as err:
Logger.Log(f"Could not parse json from {result.text}", logging.ERROR)
body = {}
self.assertEqual(body.get("type"), "GET", f"Bad type from {_url}")
self.assertEqual(body.get("val"), None, f"Bad val from {_url}")
self.assertEqual(body.get("msg"), "Hello! You GETted successfully!", f"Bad msg from {_url}")
self.assertEqual(result.Status.value, 200, f"Bad status from {_url}")
self.assertEqual(str(result.Type), "GET", f"Bad type from {_url}")
self.assertEqual(result.Value, None, f"Bad val from {_url}")
self.assertEqual(result.Message, "Hello! You GETted successfully!", f"Bad msg from {_url}")

def test_post(self):
_url = f"{self.base_url}/hello"
try:
result = APIRequest(url=_url, request="POST", params={}, logger=Logger.std_logger)
result = APIRequest(url=_url, request_type="POST", params={}).Execute(logger=Logger.std_logger)
except Exception as err:
self.fail(str(err))
else:
self.assertNotEqual(result, None, f"No response from {_url}")
self.assertEqual(result.status_code, 200, f"Bad status from {_url}")
try:
body = json.loads(result.text)
except json.decoder.JSONDecodeError as err:
Logger.Log(f"Could not parse json from {result.text}", logging.ERROR)
body = {}
self.assertEqual(body.get("type"), "POST", f"Bad type from {_url}")
self.assertEqual(body.get("val"), None, f"Bad val from {_url}")
self.assertEqual(body.get("msg"), "Hello! You POSTed successfully!", f"Bad msg from {_url}")
self.assertEqual(result.Status.value, 200, f"Bad status from {_url}")
self.assertEqual(str(result.Type), "POST", f"Bad type from {_url}")
self.assertEqual(result.Value, None, f"Bad val from {_url}")
self.assertEqual(result.Message, "Hello! You POSTed successfully!", f"Bad msg from {_url}")

def test_put(self):
_url = f"{self.base_url}/hello"
Logger.Log(f"PUT test at {_url}", logging.DEBUG)
try:
result = APIRequest(url=_url, request="PUT", params={}, logger=Logger.std_logger)
result = APIRequest(url=_url, request_type="PUT", params={}).Execute(logger=Logger.std_logger)
except Exception as err:
self.fail(str(err))
else:
self.assertNotEqual(result, None, f"No response from {_url}")
self.assertEqual(result.status_code, 200, f"Bad status from {_url}")
try:
body = json.loads(result.text)
except json.decoder.JSONDecodeError as err:
Logger.Log(f"Could not parse json from {result.text}", logging.ERROR)
body = {}
self.assertEqual(body.get("type"), "PUT", f"Bad type from {_url}")
self.assertEqual(body.get("val"), None, f"Bad val from {_url}")
self.assertEqual(body.get("msg"), "Hello! You PUTted successfully!", f"Bad msg from {_url}")
self.assertEqual(result.Status.value, 200, f"Bad status from {_url}")
self.assertEqual(str(result.Type), "PUT", f"Bad type from {_url}")
self.assertEqual(result.Value, None, f"Bad val from {_url}")
self.assertEqual(result.Message, "Hello! You PUTted successfully!", f"Bad msg from {_url}")
48 changes: 16 additions & 32 deletions tests/cases/apis/HelloAPI/remote/t_ParamHello.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# import libraries
import json
import logging
from unittest import TestCase
# import 3rd-party libraries
# import ogd-core libraries.
from ogd.common.configs.TestConfig import TestConfig
from ogd.common.utils.Logger import Logger
# import locals
from src.ogd.apis.models.APIRequest import APIRequest
from ogd.apis.models.APIRequest import APIRequest
from tests.config.t_config import settings

class t_ParamHello_remote(TestCase):
Expand All @@ -25,54 +24,39 @@ def setUpClass(cls) -> None:
def test_get(self):
_url = f"{self.base_url}/p_hello/{self.param}"
try:
result = APIRequest(url=_url, request="GET", params={}, logger=Logger.std_logger)
result = APIRequest(url=_url, request_type="GET", params={}).Execute(logger=Logger.std_logger)
except Exception as err:
self.fail(str(err))
else:
self.assertNotEqual(result, None, f"No response from {_url}")
self.assertEqual(result.status_code, 200, f"Bad status from {_url}")
try:
body = json.loads(result.text)
except json.decoder.JSONDecodeError:
Logger.Log(f"Could not parse json result '{result.text}' from {_url}", logging.ERROR)
body = {}
self.assertEqual(body.get("type"), "GET", f"Bad type from {_url}")
self.assertEqual(body.get("val"), None, f"Bad val from {_url}")
self.assertEqual(body.get("msg"), f"Hello {self.param}! You GETted successfully!", f"Bad msg from {_url}")
self.assertEqual(result.Status, 200, f"Bad status from {_url}")
self.assertEqual(str(result.Type), "GET", f"Bad type from {_url}")
self.assertEqual(result.Value, None, f"Bad val from {_url}")
self.assertEqual(result.Message, f"Hello {self.param}! You GETted successfully!", f"Bad msg from {_url}")

def test_post(self):
_url = f"{self.base_url}/p_hello/{self.param}"
try:
result = APIRequest(url=_url, request="POST", params={}, logger=Logger.std_logger)
result = APIRequest(url=_url, request_type="POST", params={}).Execute(logger=Logger.std_logger)
except Exception as err:
self.fail(str(err))
else:
self.assertNotEqual(result, None, f"No response from {_url}")
self.assertEqual(result.status_code, 200, f"Bad status from {_url}")
try:
body = json.loads(result.text)
except json.decoder.JSONDecodeError:
Logger.Log(f"Could not parse json result '{result.text}' from {_url}", logging.ERROR)
body = {}
self.assertEqual(body.get("type"), "POST", f"Bad type from {_url}")
self.assertEqual(body.get("val"), None, f"Bad val from {_url}")
self.assertEqual(body.get("msg"), f"Hello {self.param}! You POSTed successfully!", f"Bad msg from {_url}")
self.assertEqual(result.Status, 200, f"Bad status from {_url}")
self.assertEqual(str(result.Type), "POST", f"Bad type from {_url}")
self.assertEqual(result.Value, None, f"Bad val from {_url}")
self.assertEqual(result.Message, f"Hello {self.param}! You POSTed successfully!", f"Bad msg from {_url}")

def test_put(self):
_url = f"{self.base_url}/p_hello/{self.param}"
Logger.Log(f"PUT test at {_url}", logging.DEBUG)
try:
result = APIRequest(url=_url, request="PUT", params={}, logger=Logger.std_logger)
result = APIRequest(url=_url, request_type="PUT", params={}).Execute(logger=Logger.std_logger)
except Exception as err:
self.fail(str(err))
else:
self.assertNotEqual(result, None, f"No response from {_url}")
self.assertEqual(result.status_code, 200, f"Bad status from {_url}")
try:
body = json.loads(result.text)
except json.decoder.JSONDecodeError:
Logger.Log(f"Could not parse json result '{result.text}' from {_url}", logging.ERROR)
body = {}
self.assertEqual(body.get("type"), "PUT", f"Bad type from {_url}")
self.assertEqual(body.get("val"), None, f"Bad val from {_url}")
self.assertEqual(body.get("msg"), f"Hello {self.param}! You PUTted successfully!", f"Bad msg from {_url}")
self.assertEqual(result.Status, 200, f"Bad status from {_url}")
self.assertEqual(str(result.Type), "PUT", f"Bad type from {_url}")
self.assertEqual(result.Value, None, f"Bad val from {_url}")
self.assertEqual(result.Message, f"Hello {self.param}! You PUTted successfully!", f"Bad msg from {_url}")
4 changes: 2 additions & 2 deletions tests/cases/apis/HelloAPI/remote/t_Version.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from ogd.common.utils.Logger import Logger
# Logger.InitializeLogger(level=logging.INFO, use_logfile=False)
# import locals
from src.ogd.apis.configs.ServerConfig import ServerConfig
from src.ogd.apis.HelloAPI import HelloAPI
from ogd.apis.configs.ServerConfig import ServerConfig
from ogd.apis.HelloAPI import HelloAPI
from tests.config.t_config import settings

class t_Version_remote(TestCase):
Expand Down
6 changes: 3 additions & 3 deletions tests/cases/models/t_APIResponse.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
from ogd.common.configs.TestConfig import TestConfig
from ogd.common.utils.Logger import Logger
# import locals
from src.ogd.apis.models.enums.ResponseStatus import ResponseStatus
from src.ogd.apis.models.enums.RESTType import RESTType
from src.ogd.apis.models.APIResponse import APIResponse
from ogd.apis.models.enums.ResponseStatus import ResponseStatus
from ogd.apis.models.enums.RESTType import RESTType
from ogd.apis.models.APIResponse import APIResponse
from tests.config.t_config import settings


Expand Down
Loading