Skip to content

Commit

Permalink
Changes for EPIC integration (#2208)
Browse files Browse the repository at this point in the history
* incorporated existing comment period updates

* incorporated existing comment period updates
  • Loading branch information
saravanpa-aot authored Sep 18, 2023
1 parent fa7f4d2 commit 4685f63
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 82 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""tracking id for metadata
Revision ID: b1196306955f
Revises: 7ebd9ecfccdd
Create Date: 2023-09-17 22:11:54.456358
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = 'b1196306955f'
down_revision = '7ebd9ecfccdd'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('engagement_metadata', sa.Column('project_tracking_id', sa.String(length=100), nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('engagement_metadata', 'project_tracking_id')
# ### end Alembic commands ###
6 changes: 6 additions & 0 deletions met-api/src/met_api/models/engagement_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ class EngagementMetadataModel(BaseModel):
engagement_id = db.Column(db.Integer, ForeignKey('engagement.id', ondelete='CASCADE'), primary_key=True)
project_id = db.Column(db.String(50), unique=False, nullable=True)
project_metadata = db.Column(postgresql.JSONB(astext_type=db.Text()), unique=False, nullable=True)
project_tracking_id = db.Column(db.String(100), unique=False, nullable=True)

@classmethod
def find_by_engagement_id(cls, engagement_id):
"""Return engagement slug by engagement id."""
return cls.query.filter_by(engagement_id=engagement_id).first()

@classmethod
def update(cls, engagement_metadata_data: dict) -> Optional[EngagementMetadataModel]:
Expand Down
6 changes: 3 additions & 3 deletions met-api/src/met_api/services/engagement_metadata_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from met_api.models.engagement_metadata import EngagementMetadataModel
from met_api.schemas.engagement_metadata import EngagementMetadataSchema
from met_api.services import authorization
from met_api.utils import eao_util
from met_api.services.project_service import ProjectService
from met_api.utils.roles import Role


Expand Down Expand Up @@ -43,7 +43,7 @@ def create_metadata(request_json: dict):
metadata_model.commit()
updated_metadata: EngagementMetadataModel = metadata_model.find_by_id(metadata_model.engagement_id)
# publish changes to EPIC
eao_util.publish_to_epic(updated_metadata.project_id, updated_metadata.engagement_id)
ProjectService.update_project_info(updated_metadata.project_id, updated_metadata.engagement_id)
return updated_metadata

@staticmethod
Expand Down Expand Up @@ -81,7 +81,7 @@ def update_metadata(data: dict):
updated_metadata = EngagementMetadataService._create_metadata_model(data)

# publish changes to EPIC
eao_util.publish_to_epic(updated_metadata.project_id, updated_metadata.engagement_id)
ProjectService.update_project_info(updated_metadata.project_id, updated_metadata.engagement_id)

return updated_metadata

Expand Down
78 changes: 78 additions & 0 deletions met-api/src/met_api/services/project_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Service for project management."""
import logging
from http import HTTPStatus
from flask import current_app

from met_api.models.engagement import Engagement as EngagementModel
from met_api.models.engagement_metadata import EngagementMetadataModel
from met_api.services.email_verification_service import EmailVerificationService
from met_api.services.rest_service import RestService
from met_api.utils import notification


class ProjectService:
"""Project management service."""

@staticmethod
def update_project_info(project_id: str, eng_id: str) -> EngagementModel:
"""Publish new comment period to EPIC/EAO system."""
logger = logging.getLogger(__name__)

try:
is_eao_environment = current_app.config.get('IS_EAO_ENVIRONMENT')
if not is_eao_environment:
return

engagement, engagement_metadata = ProjectService._get_engagement_and_metadata(eng_id)

epic_comment_period_payload = ProjectService._construct_epic_payload(engagement, project_id)

eao_service_account_token = ProjectService._get_eao_service_account_token()

if engagement_metadata and engagement_metadata.project_tracking_id:
update_url = f'{current_app.config.get("EPIC_URL")}/{engagement_metadata.project_tracking_id}'
RestService.put(endpoint=update_url, token=eao_service_account_token, data=epic_comment_period_payload,
raise_for_status=False)
else:
create_url = f'{current_app.config.get("EPIC_URL")}'
api_response = RestService.post(endpoint=create_url, token=eao_service_account_token,
data=epic_comment_period_payload, raise_for_status=False)
response_data = api_response.json()

if api_response.status_code == HTTPStatus.OK:
tracking_number = response_data.get('accountNumber')
engagement_metadata.project_tracking_id = tracking_number
engagement_metadata.commit()

except Exception as e: # NOQA # pylint:disable=broad-except
logger.error('Error in update_project_info: %s', str(e))

@staticmethod
def _get_engagement_and_metadata(eng_id: str):
engagement = EngagementModel.find_by_id(eng_id)
engagement_metadata = EngagementMetadataModel.find_by_engagement_id(eng_id)
return engagement, engagement_metadata

@staticmethod
def _construct_epic_payload(engagement, project_id):
site_url = notification.get_tenant_site_url(engagement.tenant_id)
epic_comment_period_payload = {
'isMet': True,
'metURL': f'{site_url}{EmailVerificationService.get_engagement_path(engagement)}',
'dateCompleted': engagement.end_date,
'dateStarted': engagement.start_date,
'instructions': '',
'commentTip': '',
'milestone': current_app.config.get('EPIC_MILESTONE'),
'openHouse': '',
'relatedDocuments': '',
'project': project_id
}
return epic_comment_period_payload

@staticmethod
def _get_eao_service_account_token():
kc_service_id = current_app.config.get('EPIC_KEYCLOAK_SERVICE_ACCOUNT_ID')
kc_secret = current_app.config.get('EPIC_KEYCLOAK_SERVICE_ACCOUNT_SECRET')
issuer_url = current_app.config.get('EPIC_JWT_OIDC_ISSUER')
return RestService.get_service_account_token(kc_service_id, kc_secret, issuer_url)
69 changes: 57 additions & 12 deletions met-api/src/met_api/services/rest_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
# limitations under the License.
"""Service to invoke Rest services."""
import json
from typing import Iterable

import requests
from flask import current_app
from flask import current_app, request
from requests.exceptions import ConnectTimeout, HTTPError
# pylint:disable=ungrouped-imports
from requests.exceptions import ConnectionError as ReqConnectionError
from requests.exceptions import ConnectTimeout, HTTPError

from met_api.utils.enums import AuthHeaderType, ContentType

Expand All @@ -27,42 +28,81 @@ class RestService:
"""Service to invoke Rest services which uses OAuth 2.0 implementation."""

@staticmethod
def post(endpoint, token=None, # pylint: disable=too-many-arguments
auth_header_type: AuthHeaderType = AuthHeaderType.BEARER,
content_type: ContentType = ContentType.JSON, data=None, raise_for_status: bool = True):
"""POST service."""
current_app.logger.debug('<post')
def _invoke(rest_method, endpoint, token=None, # pylint: disable=too-many-arguments
auth_header_type: AuthHeaderType = AuthHeaderType.BEARER,
content_type: ContentType = ContentType.JSON, data=None, raise_for_status: bool = True,
additional_headers: dict = None, generate_token: bool = True):
"""Invoke different method depending on the input."""
# just to avoid the duplicate code for PUT and POSt
current_app.logger.debug(f'<_invoke-{rest_method}')

headers = {
'Authorization': auth_header_type.value.format(token),
'Content-Type': content_type.value
}

if not token and generate_token:
token = _get_token()

if token:
headers.update({'Authorization': auth_header_type.value.format(token)})

if additional_headers:
headers.update(additional_headers)

if content_type == ContentType.JSON:
data = json.dumps(data)

current_app.logger.debug(f'Endpoint : {endpoint}')
current_app.logger.debug(f'headers : {headers}')
response = None
try:
response = requests.post(endpoint, data=data, headers=headers,
timeout=current_app.config.get('CONNECT_TIMEOUT'))
invoke_rest_method = getattr(requests, rest_method)
response = invoke_rest_method(endpoint, data=data, headers=headers,
timeout=current_app.config.get('CONNECT_TIMEOUT', 60))
if raise_for_status:
response.raise_for_status()
except (ReqConnectionError, ConnectTimeout) as exc:
current_app.logger.error('---Error on POST---')
current_app.logger.error(exc)
raise Exception(exc) from exc
except HTTPError as exc:
current_app.logger.error(f'HTTPError on POST with status code {response.status_code if response else ""}')
current_app.logger.error(f"HTTPError on POST with status code {response.status_code if response else ''}")
if response and response.status_code >= 500:
raise Exception(exc) from exc
raise exc
finally:
current_app.logger.info(f'response : {response.text if response else ""}')
RestService.__log_response(response)

current_app.logger.debug('>post')
return response

@staticmethod
def __log_response(response):
if response is not None:
current_app.logger.info(f'Response Headers {response.headers}')
if response.headers and isinstance(response.headers, Iterable) and \
'Content-Type' in response.headers and \
response.headers['Content-Type'] == ContentType.JSON.value:
current_app.logger.info(f"response : {response.text if response else ''}")

@staticmethod
def post(endpoint, token=None, # pylint: disable=too-many-arguments
auth_header_type: AuthHeaderType = AuthHeaderType.BEARER,
content_type: ContentType = ContentType.JSON, data=None, raise_for_status: bool = True,
additional_headers: dict = None, generate_token: bool = True):
"""POST service."""
current_app.logger.debug('<post')
return RestService._invoke('post', endpoint, token, auth_header_type, content_type, data, raise_for_status,
additional_headers, generate_token)

@staticmethod
def put(endpoint, token=None, # pylint: disable=too-many-arguments
auth_header_type: AuthHeaderType = AuthHeaderType.BEARER,
content_type: ContentType = ContentType.JSON, data=None, raise_for_status: bool = True):
"""POST service."""
current_app.logger.debug('<post')
return RestService._invoke('put', endpoint, token, auth_header_type, content_type, data, raise_for_status)

@staticmethod
def get_service_account_token(kc_service_id: str = None, kc_secret: str = None, issuer_url: str = None) -> str:
"""Generate a service account token."""
Expand All @@ -78,3 +118,8 @@ def get_service_account_token(kc_service_id: str = None, kc_secret: str = None,
'Content-Type': ContentType.FORM_URL_ENCODED.value}, data='grant_type=client_credentials')
auth_response.raise_for_status()
return auth_response.json().get('access_token')


def _get_token() -> str:
token: str = request.headers['Authorization'] if request and 'Authorization' in request.headers else None
return token.replace('Bearer ', '') if token else None
67 changes: 0 additions & 67 deletions met-api/src/met_api/utils/eao_util.py

This file was deleted.

0 comments on commit 4685f63

Please sign in to comment.