Skip to content

Commit

Permalink
Merge pull request #6 from NOAA-GSL/proxy-creating-existing
Browse files Browse the repository at this point in the history
nwsc_proxy: support creating Support Profile with "status": "existing"
  • Loading branch information
mackenzie-grimes-noaa authored Jan 2, 2025
2 parents 022c6fd + 39b4b0a commit 25ac1ff
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 30 deletions.
63 changes: 38 additions & 25 deletions python/nwsc_proxy/ncp_web_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,15 @@
from datetime import datetime, UTC
from argparse import ArgumentParser, Namespace

from flask import Flask, current_app, request, jsonify
from flask import Flask, Response, current_app, request, jsonify

from idsse.common.utils import to_iso

from src.profile_store import ProfileStore

# constants
GSL_KEY = '8209c979-e3de-402e-a1f5-556d650ab889'


def to_iso(date_time: datetime) -> str:
"""Format a datetime instance to an ISO string. Borrowed from idsse.commons.utils for now"""
return (f'{date_time.strftime("%Y-%m-%dT%H:%M")}:'
f'{(date_time.second + date_time.microsecond / 1e6):06.3f}'
'Z' if date_time.tzname() in [None, str(UTC)]
else date_time.strftime("%Z")[3:])


# pylint: disable=too-few-public-methods
class HealthRoute:
"""Handle requests to /health endpoint"""
Expand All @@ -49,29 +42,17 @@ class EventsRoute:
def __init__(self, base_dir: str):
self.profile_store = ProfileStore(base_dir)

# pylint: disable=too-many-return-statements
def handler(self):
"""Logic for requests to /all-events"""
"""Logic for requests to /all-events."""
# check that this request has proper key to get or add data
if request.headers.get('X-Api-Key') != current_app.config['GSL_KEY']:
return jsonify({'message': 'ERROR: Unauthorized'}), 401

if request.method == 'POST':
# request is saving new Support Profile event
request_body: dict = request.json
profile_id = self.profile_store.save(request_body)
if not profile_id:
return jsonify({'message': f'Profile {request_body.get("id")} already exists'}
), 400

return jsonify({'message': f'Profile {profile_id} saved'}), 201
return self._handle_create()

if request.method == 'DELETE':
profile_id = request.args.get('uuid', default=None, type=str)
is_deleted = self.profile_store.delete(profile_id)
if not is_deleted:
return jsonify({'message': f'Profile {profile_id} not found'}), 404
return jsonify({'message': f'Profile {profile_id} deleted'}), 204
return self._handle_delete()

# otherwise, must be 'GET' operation
data_source = request.args.get('dataSource', None, type=str)
Expand All @@ -98,6 +79,38 @@ def handler(self):

return jsonify({'profiles': profiles, 'errors': []}), 200

def _handle_delete(self) -> Response:
"""Logic for DELETE requests to /all-events. Returns Response with status_code: 204 on
success, 404 otherwise."""
profile_id = request.args.get('uuid', default=None, type=str)
is_deleted = self.profile_store.delete(profile_id)
if not is_deleted:
return jsonify({'message': f'Profile {profile_id} not found'}), 404
return jsonify({'message': f'Profile {profile_id} deleted'}), 204

def _handle_create(self) -> Response:
"""Logic for POST requests to /all-events. Returns Response with status_code: 201 on
success, 400 otherwise."""
request_body: dict = request.json

profile_data: dict | None = request_body.get('data')
status: str | None = request_body.get('status')
if not profile_data or not status:
return jsonify({'message': 'Missing one of required attributes: [data, status]'}), 400

if status == 'new':
is_new = True
elif status == 'existing':
is_new = False
else:
return jsonify({'message': 'Status must be one of [new, existing]'}), 400

profile_id = self.profile_store.save(profile_data, is_new)
if not profile_id:
return jsonify({'message': f'Profile {profile_data.get("id")} already exists'}), 400

return jsonify({'message': f'Profile {profile_id} saved'}), 201


class AppWrapper:
"""Web server class wrapping Flask operations"""
Expand Down
13 changes: 10 additions & 3 deletions python/nwsc_proxy/src/profile_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,15 @@ def get_all(self, filter_new_profiles = False) -> list[dict]:
return [cached_profile.data for cached_profile in self.profile_cache
if cached_profile.is_new == filter_new_profiles]

def save(self, profile: dict) -> str | None:
def save(self, profile: dict, is_new = True) -> str | None:
"""Persist a new Support Profile Profile to this API
Args:
profile (dict): the JSON data of the Support Profile to store.
is_new (optional, bool): whether to store the Profile as "new" or "existing". This
will only control whether this SupportProfile will be returned to calls to the
`get_all()` method (if it is classified as new vs. existing). Defaults to True.
Returns:
str | None: UUID of saved Support Profile on success, otherwise None
"""
Expand All @@ -103,10 +109,11 @@ def save(self, profile: dict) -> str | None:
logger.warning('Cannot save profile; already exists %s', existing_profile.id)
return None

cached_profile = CachedProfile(profile, is_new=True)
cached_profile = CachedProfile(profile, is_new=is_new)

# save Profile JSON to filesystem
filepath = os.path.join(self._new_dir, f'{cached_profile.id}.json')
file_dir = self._new_dir if is_new else self._existing_dir
filepath = os.path.join(file_dir, f'{cached_profile.id}.json')
logger.info('Now saving profile to path: %s', filepath)
with open(filepath, 'w', encoding='utf-8') as file:
json.dump(profile, file)
Expand Down
31 changes: 29 additions & 2 deletions python/nwsc_proxy/test/test_ncp_web_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,41 @@ def test_get_new_profiles(wrapper: AppWrapper, mock_request: Mock, mock_profile_
assert mark_existing_call_args[0][1][0] == example_profile['id']


def test_create_profile_success(wrapper: AppWrapper, mock_request: Mock, mock_profile_store: Mock):
def test_create_profile_new(wrapper: AppWrapper, mock_request: Mock, mock_profile_store: Mock):
mock_request.method = 'POST'
mock_request.json = {'id': EXAMPLE_UUID, 'name': 'My Profile'}
example_profile = {'id': EXAMPLE_UUID, 'name': 'My Profile'}
mock_request.json = {'status': 'new', 'data': example_profile}
mock_profile_store.return_value.save.return_value = EXAMPLE_UUID # save() success

result: tuple[Response, int] = wrapper.app.view_functions['events']()

assert result[1] == 201
# should have saved profile with is_new: True
mock_profile_store.return_value.save.assert_called_once_with(example_profile, True)


def test_create_profile_existing(wrapper: AppWrapper, mock_request: Mock, mock_profile_store: Mock):
mock_request.method = 'POST'
example_profile = {'id': EXAMPLE_UUID, 'name': 'My Profile'}
mock_request.json = {'status': 'existing', 'data': example_profile}
mock_profile_store.return_value.save.return_value = EXAMPLE_UUID # save() success

result: tuple[Response, int] = wrapper.app.view_functions['events']()

assert result[1] == 201
# should have saved profile with is_new: False
mock_profile_store.return_value.save.assert_called_once_with(example_profile, False)


def test_create_profile_invalid(wrapper: AppWrapper, mock_request: Mock, mock_profile_store: Mock):
mock_request.method = 'POST'
example_profile = {'id': EXAMPLE_UUID, 'name': 'My Profile'}
mock_request.json = {'status': 'foobar', 'data': example_profile}

result: tuple[Response, int] = wrapper.app.view_functions['events']()

assert result[1] == 400
mock_profile_store.return_value.save.assert_not_called()


def test_create_previous_profile_failure(wrapper: AppWrapper,
Expand Down

0 comments on commit 25ac1ff

Please sign in to comment.