Merge pull request #419 develop-v4.6.0 into develop
This commit is contained in:
commit
c54f8a3836
329
lambda/daily-data-unreceive-check/daily-data-unreceive-check.py
Normal file
329
lambda/daily-data-unreceive-check/daily-data-unreceive-check.py
Normal file
@ -0,0 +1,329 @@
|
||||
import csv
|
||||
import datetime
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import boto3
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
# 環境変数
|
||||
CONFIG_BUCKET_NAME = os.environ["CONFIG_BUCKET_NAME"]
|
||||
MBJ_NOTICE_TOPIC = os.environ["MBJ_NOTICE_TOPIC"]
|
||||
PROCESSED_MESSAGE_DYNAMODB_TABLE_NAME = os.environ["PROCESSED_MESSAGE_DYNAMODB_TABLE_NAME"]
|
||||
PROCESSED_MESSAGE_EXPIRES_PERIOD = int(os.environ["PROCESSED_MESSAGE_EXPIRES_PERIOD"])
|
||||
LOG_LEVEL = os.environ["LOG_LEVEL"]
|
||||
TZ = os.environ["TZ"]
|
||||
|
||||
# 定数
|
||||
ROW_COMMENT_SYMBOL = '#'
|
||||
INDEX_REGEX = 0
|
||||
INDEX_DATA_NAME = 1
|
||||
INDEX_ROW_COMMENT_SYMBOL = 0
|
||||
INDEX_SPLIT_NUM = 1
|
||||
INDEX_LAST = -1
|
||||
|
||||
# メール本文に出力する不足ファイル名一覧のインデント
|
||||
MAIL_INDENT = ' '
|
||||
|
||||
# AWS操作クライアント
|
||||
s3_client = boto3.client('s3')
|
||||
sns_client = boto3.client('sns')
|
||||
dynamodb_client = boto3.client('dynamodb')
|
||||
|
||||
|
||||
# logger設定
|
||||
def log_datetime_convert_tz(*arg):
|
||||
"""ログに出力するタイムスタンプのロケールを変更する(JST指定)"""
|
||||
return datetime.datetime.now(ZoneInfo(TZ)).timetuple()
|
||||
|
||||
|
||||
logger = logging.getLogger()
|
||||
formatter = logging.Formatter(
|
||||
'[%(levelname)s]\t%(asctime)s\t%(message)s\n',
|
||||
'%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
formatter.converter = log_datetime_convert_tz
|
||||
for handler in logger.handlers:
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
level = logging.getLevelName(LOG_LEVEL)
|
||||
if not isinstance(level, int):
|
||||
level = logging.INFO
|
||||
logger.setLevel(level)
|
||||
|
||||
|
||||
def is_duplicate_message(message_id: str) -> bool:
|
||||
"""DynamoDBテーブルに処理済みのSQSメッセージIdが存在するかどうかを調査する
|
||||
|
||||
Args:
|
||||
message_id (str): SQSメッセージId
|
||||
|
||||
Returns:
|
||||
bool: 存在する場合はTrue
|
||||
"""
|
||||
return dynamodb_client.query(
|
||||
TableName=PROCESSED_MESSAGE_DYNAMODB_TABLE_NAME,
|
||||
Select='COUNT',
|
||||
KeyConditionExpression='message_id = :message_id',
|
||||
ExpressionAttributeValues={
|
||||
':message_id': {'S': message_id}
|
||||
}
|
||||
)["Count"] != 0
|
||||
|
||||
|
||||
def put_success_messages_to_dynamo_db(batch_success_items: list[str]) -> bool:
|
||||
"""処理済みのSQSメッセージIdをDynamoDBにPushする
|
||||
|
||||
Args:
|
||||
batch_success_items (list[str]): SQSメッセージIdのリスト
|
||||
|
||||
Returns:
|
||||
bool: 登録成功の場合、True
|
||||
"""
|
||||
|
||||
# レコードの有効期限を算出
|
||||
now = datetime.datetime.now(ZoneInfo(TZ))
|
||||
record_expiration_datetime = now + \
|
||||
datetime.timedelta(minutes=PROCESSED_MESSAGE_EXPIRES_PERIOD)
|
||||
record_expiration_time = record_expiration_datetime.timestamp()
|
||||
|
||||
for message_id in batch_success_items:
|
||||
dynamodb_client.put_item(
|
||||
TableName=PROCESSED_MESSAGE_DYNAMODB_TABLE_NAME,
|
||||
Item={
|
||||
'message_id': {'S': message_id},
|
||||
'record_expiration_time': {'N': f'{record_expiration_time}'}
|
||||
}
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def substitute_mail_template(mail_template: str, mail_msg: str) -> str:
|
||||
"""メールテンプレートのプレースホルダーを置き換える
|
||||
|
||||
Args:
|
||||
mail_template (str): 置き換え前のメールテンプレート
|
||||
mail_msg (str): メールテンプレートのプレースホルダーを置き換える文言(ファイル一覧)
|
||||
|
||||
Returns:
|
||||
str: 置き換え後のメール本文
|
||||
"""
|
||||
substitute_dict = {
|
||||
"notice_file_names": mail_msg
|
||||
}
|
||||
mail_str = mail_template.format_map(substitute_dict)
|
||||
return mail_str
|
||||
|
||||
|
||||
def make_failure_item_on_error(message_id: str) -> dict[str, str]:
|
||||
"""Report batch item failuresによる処理に失敗したメッセージの判別のためのレスポンスを作成する
|
||||
@see <https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#services-sqs-batchfailurereporting>
|
||||
Args:
|
||||
message_id (str): SQSメッセージId
|
||||
|
||||
Returns:
|
||||
dict[str, str]: Report batch item failuresで失敗したSQSメッセージを判別するための辞書オブジェクト
|
||||
"""
|
||||
return {"itemIdentifier": message_id}
|
||||
|
||||
|
||||
def daily_data_unreceive_check(records: list, execute_date: str) -> tuple[list[dict[str, str]], list[str]]:
|
||||
"""日次データ未受領チェック
|
||||
|
||||
Args:
|
||||
records (list): SQS Eventのレコードリスト
|
||||
execute_date (str): 処理稼働年月日
|
||||
|
||||
Returns:
|
||||
tuple[list[dict[str, str]], list[str]]: 失敗メッセージIdのリスト, 成功メッセージIdのリスト
|
||||
"""
|
||||
batch_failed_items = []
|
||||
batch_success_items = []
|
||||
|
||||
for record in records:
|
||||
# メール挿入用文言を格納するためのメモリを保持する
|
||||
mail_message = ''
|
||||
try:
|
||||
# SQSパラメータをJSONシリアライズし、Pythonの辞書オブジェクト(イベントパラメータ)を取得する。
|
||||
event_parameter = json.loads(record['body'])
|
||||
receive_date = execute_date.strftime('%Y/%m/%d')
|
||||
|
||||
try:
|
||||
# 1.SQSメッセージIDを取得する
|
||||
message_id = record["messageId"]
|
||||
# 2.DynamoDBテーブルからレコードを取得し、処理済みメッセージかどうかを判別する
|
||||
if is_duplicate_message(message_id):
|
||||
logger.info(f'I-02-02 受信したメッセージは既に処理済みのため、処理をスキップします。メッセージID: {message_id} バケットディレクトリ: {event_parameter["check_bucket_name"]}/{event_parameter["check_folder_prefix"]}/{receive_date}/')
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.exception(f"E-02-01 メッセージ重複チェック処理に失敗しました エラー内容:{e}")
|
||||
batch_failed_items.append(make_failure_item_on_error(message_id))
|
||||
continue
|
||||
|
||||
# ③ 設定ファイル[受領チェック対象ファイルリスト]を読み込む
|
||||
try:
|
||||
logger.info('I-03-01 ' +'受領チェック対象ファイルリスト読込 読込元:' + f'{CONFIG_BUCKET_NAME}/{event_parameter["check_target_file_list"]}')
|
||||
check_target_file_list_response = s3_client.get_object(
|
||||
Bucket=CONFIG_BUCKET_NAME,
|
||||
Key=f'{event_parameter["check_target_file_list"]}'
|
||||
)
|
||||
logger.info('I-03-02 受領チェック対象ファイルリストを読み込みました')
|
||||
except Exception as e:
|
||||
logger.exception(f"E-03-01 受領チェック対象ファイルリストの読み込みに失敗しました エラー内容:{e} 読込元:{CONFIG_BUCKET_NAME}/{event_parameter["check_target_file_list"]}")
|
||||
batch_failed_items.append(make_failure_item_on_error(message_id))
|
||||
continue
|
||||
|
||||
# ④ 受領チェック処理を行う
|
||||
logger.info(f'I-04-01 日次データ受領チェック ({event_parameter['data_source_name']}) 処理開始')
|
||||
object_prefix = f'{event_parameter["check_folder_prefix"]}/{receive_date}/'
|
||||
|
||||
# 1.日次データバックアップ保管バケットの処理稼働月に該当するサブフォルダにあるファイル一覧を取得する
|
||||
logger.info(f'I-04-02 オブジェクトリストの取得 取得先:{event_parameter['check_bucket_name']}/{object_prefix}')
|
||||
receive_file_list_response = s3_client.list_objects_v2(Bucket=event_parameter['check_bucket_name'], Prefix=object_prefix)
|
||||
receive_file_list = []
|
||||
for content in receive_file_list_response.get('Contents', []):
|
||||
# オブジェクトのキーからファイル名を切り出してリストに追加
|
||||
obj_key = content['Key'].rsplit('/', INDEX_SPLIT_NUM)
|
||||
receive_file_list.append(obj_key[INDEX_LAST])
|
||||
|
||||
# 2.I/Fファイルチェック処理
|
||||
logger.info(f'I-04-03 日次受信データ({event_parameter['data_source_name']}) 未受領チェック処理開始')
|
||||
check_target_file_name_body = io.TextIOWrapper(io.BytesIO(
|
||||
check_target_file_list_response["Body"].read()), encoding='utf-8')
|
||||
match_count = 0
|
||||
row_count = 0
|
||||
for tsv_row in csv.reader(check_target_file_name_body, delimiter='\t'):
|
||||
# 「④1.」で取得したリストが「③」で読み込んだファイル内に存在するか確認する
|
||||
is_file_not_exists = True
|
||||
for file_name in receive_file_list:
|
||||
match_result = re.fullmatch(tsv_row[INDEX_REGEX], file_name)
|
||||
# 「③」で読み込んだファイルに記載されている全てが「④1.」で取得したリストに存在した場合
|
||||
if match_result is not None:
|
||||
is_file_not_exists = False
|
||||
logger.info(f'I-04-04 I/Fファイルの受領を確認しました ファイル名:{file_name}')
|
||||
match_count += 1
|
||||
if is_file_not_exists:
|
||||
logger.info(f'I-04-06 I/Fファイルに不足があります ファイル名:{tsv_row[INDEX_DATA_NAME]}')
|
||||
mail_message += f'{MAIL_INDENT}{tsv_row[INDEX_DATA_NAME]}\n'
|
||||
|
||||
row_count += 1
|
||||
|
||||
if row_count == match_count:
|
||||
logger.info('I-04-05 I/Fファイルは全て受領していることを確認しました')
|
||||
|
||||
# ⑤ 「①」でメモリ保持しているメール挿入用文言に出力内容が存在するか確認する
|
||||
logger.info('I-05-01 メール送信処理開始')
|
||||
|
||||
if len(mail_message) == 0:
|
||||
logger.info(
|
||||
f'I-05-09 {execute_date} {event_parameter["data_source_name"]}データI/Fファイルに不足が無いため、メール送信処理をスキップします')
|
||||
batch_success_items.append(message_id)
|
||||
continue
|
||||
|
||||
# 1.存在した場合
|
||||
logger.info(f'I-05-02 {execute_date} {event_parameter["data_source_name"]} データI/Fファイルに不足があるため、メール送信処理を開始します')
|
||||
|
||||
try:
|
||||
logger.info('I-05-03 ' +f'通知メール(タイトル)テンプレートファイル読込 読込元:{CONFIG_BUCKET_NAME}/{event_parameter["notice_mail_title_template"]}')
|
||||
mail_title_response = s3_client.get_object(
|
||||
Bucket=CONFIG_BUCKET_NAME,
|
||||
Key=f'{event_parameter["notice_mail_title_template"]}'
|
||||
)
|
||||
mail_title_template = (mail_title_response['Body'].read().decode('utf-8'))
|
||||
# 改行を取り除く
|
||||
mail_title_without_line_break = mail_title_template.splitlines()[0]
|
||||
logger.info('I-05-04 通知メール(タイトル)テンプレートファイルを読み込みました')
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f'E-05-01 通知メール(タイトル)テンプレートファイルの読み込みに失敗しました エラー内容:{e} 読込元:{CONFIG_BUCKET_NAME}/{event_parameter["notice_mail_title_template"]}')
|
||||
batch_failed_items.append(
|
||||
make_failure_item_on_error(message_id))
|
||||
continue
|
||||
|
||||
try:
|
||||
logger.info('I-05-05 ' +f'通知メール(本文)テンプレートファイル読込 読込元:{CONFIG_BUCKET_NAME}/{event_parameter["notice_mail_body_template"]}')
|
||||
mail_body_template_response = s3_client.get_object(
|
||||
Bucket=CONFIG_BUCKET_NAME,
|
||||
Key=f'{event_parameter["notice_mail_body_template"]}'
|
||||
)
|
||||
mail_body_template = (mail_body_template_response['Body'].read().decode('utf-8'))
|
||||
# メール本文内のプレースホルダーを置き換える
|
||||
mail_body = substitute_mail_template(mail_body_template, mail_message)
|
||||
logger.info('I-05-06 通知メール(本文)テンプレートファイルを読み込みました')
|
||||
except Exception as e:
|
||||
logger.exception(f'E-05-02 通知メール(本文)テンプレートファイルの読み込みに失敗しました エラー内容:{e} 読込元:{CONFIG_BUCKET_NAME}/{event_parameter["notice_mail_body_template"]}')
|
||||
batch_failed_items.append(make_failure_item_on_error(message_id))
|
||||
continue
|
||||
|
||||
logger.info(f'I-05-07 メール送信指示をします 送信先トピック:{MBJ_NOTICE_TOPIC}')
|
||||
params = {
|
||||
'TopicArn': MBJ_NOTICE_TOPIC,
|
||||
'Subject': mail_title_without_line_break,
|
||||
'Message': mail_body
|
||||
}
|
||||
sns_client.publish(**params)
|
||||
logger.info('I-05-08 メール送信指示をしました')
|
||||
|
||||
batch_success_items.append(message_id)
|
||||
except Exception as e:
|
||||
logger.exception(f'E-99 想定外のエラーが発生しました エラー内容:{e}')
|
||||
batch_failed_items.append(make_failure_item_on_error(message_id))
|
||||
continue
|
||||
|
||||
return batch_failed_items, batch_success_items
|
||||
|
||||
|
||||
def lambda_handler(event, context):
|
||||
try:
|
||||
# ① 処理開始ログを出力する
|
||||
logger.info('I-01-01 処理開始 日次データ受領チェック処理')
|
||||
# 処理稼働年月を取得しメモリに保持する
|
||||
execute_date = datetime.date.today()
|
||||
# 処理成功メッセージIDリストをメモリに保持する(初期値=空のリスト)
|
||||
batch_success_items = []
|
||||
# 処理失敗メッセージIDリストをメモリに保持する(初期値=空のリスト)
|
||||
batch_failed_items = []
|
||||
|
||||
# ② SQSメッセージ重複排除処理を行う
|
||||
logger.info('I-02-01 メッセージ処理開始')
|
||||
batch_failed_items, batch_success_items = daily_data_unreceive_check(event["Records"], execute_date)
|
||||
logger.info('I-06-01 すべてのメッセージの処理完了')
|
||||
|
||||
# ⑦ メッセージを処理済として、以下のDynamoDBテーブルに記録する
|
||||
put_success_messages_to_dynamo_db(batch_success_items)
|
||||
logger.info('I-07-01 処理済みメッセージIDの記録完了')
|
||||
logger.info('I-07-02 処理終了 日次データ受領チェック処理')
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f'E-99 想定外のエラーが発生しました エラー内容:{e}')
|
||||
raise e
|
||||
|
||||
return batch_failed_items
|
||||
|
||||
|
||||
# 動作確認用のコード
|
||||
# if __name__ == '__main__':
|
||||
# lambda_handler({
|
||||
# "Records": [
|
||||
# {
|
||||
# "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
|
||||
# "receiptHandle": "MessageReceiptHandle",
|
||||
# "body": "{\"data_source_name\":\"data_source_name\",\"check_bucket_name\":\"check_bucket_name\",\"check_folder_prefix\":\"check_folder_prefix\",\"check_target_file_list\":\"check_target_file_list\",\"notice_mail_title_template\":\"notice_mail_title_template\",\"notice_mail_body_template\":\"notice_mail_body_template\"\r\n}",
|
||||
# "attributes": {
|
||||
# "ApproximateReceiveCount": "1",
|
||||
# "SentTimestamp": "1523232000000",
|
||||
# "SenderId": "123456789012",
|
||||
# "ApproximateFirstReceiveTimestamp": "1523232000001"
|
||||
# },
|
||||
# "messageAttributes": {},
|
||||
# "md5OfBody": "{{{md5_of_body}}}",
|
||||
# "eventSource": "aws:sqs",
|
||||
# "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue",
|
||||
# "awsRegion": "us-east-1"
|
||||
# }
|
||||
# ]
|
||||
# }, {})
|
||||
@ -8,8 +8,9 @@ import boto3
|
||||
|
||||
sns_client = boto3.client('sns')
|
||||
|
||||
|
||||
def lambda_handler(event, context):
|
||||
awslogs_dict = event.get('awslogs')
|
||||
awslogs_dict = event.get('awslogs')
|
||||
base64_data = awslogs_dict.get('data')
|
||||
try:
|
||||
decoded_gzip_data = base64.b64decode(base64_data)
|
||||
@ -18,12 +19,16 @@ def lambda_handler(event, context):
|
||||
log_event_str = gzip.GzipFile(fileobj=BytesIO(decoded_gzip_data)).read()
|
||||
log_event = json.loads(log_event_str)
|
||||
|
||||
# SNSのSubjectパラメータは100文字までという制限があるため、100文字に切り出す(切り捨てた分は「...」に変換)
|
||||
subject = f'Detect Error(or Warning) in {log_event.get("logGroup")}'
|
||||
subject = subject[:97] + '...' if len(subject) > 100 else subject
|
||||
|
||||
publish_message = {
|
||||
'Subject': f'Detect Error(or Warning) in {log_event.get("logGroup")}',
|
||||
'Subject': subject,
|
||||
'Message': '\n'.join([log.get('message') for log in log_event.get('logEvents')]),
|
||||
'TopicArn': os.environ.get('SNS_TOPIC_ARN')
|
||||
}
|
||||
|
||||
|
||||
print(publish_message)
|
||||
|
||||
|
||||
sns_client.publish(**publish_message)
|
||||
|
||||
19
lambda/transfer-medpass-data/Dockerfile
Normal file
19
lambda/transfer-medpass-data/Dockerfile
Normal file
@ -0,0 +1,19 @@
|
||||
# AWS公式のDockerイメージを利用
|
||||
FROM public.ecr.aws/lambda/python:3.12
|
||||
|
||||
# pythonの標準出力をバッファリングしないフラグ
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
# pythonのバイトコードを生成しないフラグ
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# 必要なファイルをイメージにコピー
|
||||
COPY Pipfile Pipfile.lock main.py ./
|
||||
|
||||
# ライブラリインストール
|
||||
RUN pip install --upgrade pip wheel setuptools && \
|
||||
pip install pipenv --no-cache-dir && \
|
||||
pipenv install --system --deploy && \
|
||||
pip uninstall -y pipenv virtualenv-clone virtualenv
|
||||
|
||||
# lambdaハンドラを起動
|
||||
CMD [ "main.handler" ]
|
||||
15
lambda/transfer-medpass-data/Pipfile
Normal file
15
lambda/transfer-medpass-data/Pipfile
Normal file
@ -0,0 +1,15 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
boto3 = "*"
|
||||
pyzipper = "*"
|
||||
|
||||
[dev-packages]
|
||||
autopep8 = "*"
|
||||
flake8 = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.12"
|
||||
172
lambda/transfer-medpass-data/Pipfile.lock
generated
Normal file
172
lambda/transfer-medpass-data/Pipfile.lock
generated
Normal file
@ -0,0 +1,172 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "d8b79fd5be60005b43448511c67536c114e5fd73722a17e77a5e60a9283aea25"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.12"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:0314e6598f59ee0f34eb4e6d1a0f69fa65c146d2b88a6e837a527a9956ec2731",
|
||||
"sha256:d41037e2c680ab8d6c61a0a4ee6bf1fdd9e857f43996672830a95d62d6f6fa79"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.34.136"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:7f7135178692b39143c8f152a618d2a3b71065a317569a7102d2306d4946f42f",
|
||||
"sha256:c63fe9032091fb9e9477706a3ebfa4d0c109b807907051d892ed574f9b573e61"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.34.136"
|
||||
},
|
||||
"jmespath": {
|
||||
"hashes": [
|
||||
"sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980",
|
||||
"sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"pycryptodomex": {
|
||||
"hashes": [
|
||||
"sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1",
|
||||
"sha256:108e5f1c1cd70ffce0b68739c75734437c919d2eaec8e85bffc2c8b4d2794305",
|
||||
"sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c",
|
||||
"sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458",
|
||||
"sha256:25cd61e846aaab76d5791d006497134602a9e451e954833018161befc3b5b9ed",
|
||||
"sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc",
|
||||
"sha256:59af01efb011b0e8b686ba7758d59cf4a8263f9ad35911bfe3f416cee4f5c08c",
|
||||
"sha256:5dcac11031a71348faaed1f403a0debd56bf5404232284cf8c761ff918886ebc",
|
||||
"sha256:62a5ec91388984909bb5398ea49ee61b68ecb579123694bffa172c3b0a107079",
|
||||
"sha256:645bd4ca6f543685d643dadf6a856cc382b654cc923460e3a10a49c1b3832aeb",
|
||||
"sha256:653b29b0819605fe0898829c8ad6400a6ccde096146730c2da54eede9b7b8baa",
|
||||
"sha256:69138068268127cd605e03438312d8f271135a33140e2742b417d027a0539427",
|
||||
"sha256:6e186342cfcc3aafaad565cbd496060e5a614b441cacc3995ef0091115c1f6c5",
|
||||
"sha256:76bd15bb65c14900d98835fcd10f59e5e0435077431d3a394b60b15864fddd64",
|
||||
"sha256:7805830e0c56d88f4d491fa5ac640dfc894c5ec570d1ece6ed1546e9df2e98d6",
|
||||
"sha256:7a710b79baddd65b806402e14766c721aee8fb83381769c27920f26476276c1e",
|
||||
"sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43",
|
||||
"sha256:82ee7696ed8eb9a82c7037f32ba9b7c59e51dda6f105b39f043b6ef293989cb3",
|
||||
"sha256:88afd7a3af7ddddd42c2deda43d53d3dfc016c11327d0915f90ca34ebda91499",
|
||||
"sha256:8af1a451ff9e123d0d8bd5d5e60f8e3315c3a64f3cdd6bc853e26090e195cdc8",
|
||||
"sha256:8ee606964553c1a0bc74057dd8782a37d1c2bc0f01b83193b6f8bb14523b877b",
|
||||
"sha256:91852d4480a4537d169c29a9d104dda44094c78f1f5b67bca76c29a91042b623",
|
||||
"sha256:9c682436c359b5ada67e882fec34689726a09c461efd75b6ea77b2403d5665b7",
|
||||
"sha256:bc3ee1b4d97081260d92ae813a83de4d2653206967c4a0a017580f8b9548ddbc",
|
||||
"sha256:bca649483d5ed251d06daf25957f802e44e6bb6df2e8f218ae71968ff8f8edc4",
|
||||
"sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e",
|
||||
"sha256:cbe71b6712429650e3883dc81286edb94c328ffcd24849accac0a4dbcc76958a",
|
||||
"sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781",
|
||||
"sha256:d3584623e68a5064a04748fb6d76117a21a7cb5eaba20608a41c7d0c61721794",
|
||||
"sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea",
|
||||
"sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b",
|
||||
"sha256:ff5c9a67f8a4fba4aed887216e32cbc48f2a6fb2673bb10a99e43be463e15913"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==3.20.0"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
"sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
|
||||
"sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.9.0.post0"
|
||||
},
|
||||
"pyzipper": {
|
||||
"hashes": [
|
||||
"sha256:0adca90a00c36a93fbe49bfa8c5add452bfe4ef85a1b8e3638739dd1c7b26bfc",
|
||||
"sha256:6d097f465bfa47796b1494e12ea65d1478107d38e13bc56f6e58eedc4f6c1a87"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.3.6"
|
||||
},
|
||||
"s3transfer": {
|
||||
"hashes": [
|
||||
"sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6",
|
||||
"sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.10.2"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3",
|
||||
"sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"
|
||||
],
|
||||
"markers": "python_version < '3.10'",
|
||||
"version": "==1.26.19"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"autopep8": {
|
||||
"hashes": [
|
||||
"sha256:8d6c87eba648fdcfc83e29b788910b8643171c395d9c4bcf115ece035b9c9dda",
|
||||
"sha256:a203fe0fcad7939987422140ab17a930f684763bf7335bdb6709991dd7ef6c2d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.3.1"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a",
|
||||
"sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.1.0"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
|
||||
"sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c",
|
||||
"sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.12.0"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f",
|
||||
"sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.2.0"
|
||||
},
|
||||
"tomli": {
|
||||
"hashes": [
|
||||
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
|
||||
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"version": "==2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
236
lambda/transfer-medpass-data/main.py
Normal file
236
lambda/transfer-medpass-data/main.py
Normal file
@ -0,0 +1,236 @@
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import boto3
|
||||
import pyzipper
|
||||
from pyzipper.zipfile import BadZipFile
|
||||
|
||||
# 環境変数
|
||||
DATA_IMPORT_BUCKET = os.environ["DATA_IMPORT_BUCKET"]
|
||||
HCP_WEB_TARGET_FOLDER = os.environ["HCP_WEB_TARGET_FOLDER"]
|
||||
HCP_WEB_BACKUP_BUCKET = os.environ["HCP_WEB_BACKUP_BUCKET"]
|
||||
BACKUP_ZIPFILE_FOLDER = os.environ["BACKUP_ZIPFILE_FOLDER"]
|
||||
BACKUP_DATA_IMPORT_FOLDER = os.environ["BACKUP_DATA_IMPORT_FOLDER"]
|
||||
DATA_IMPORT_FILENAME = os.environ["DATA_IMPORT_FILENAME"]
|
||||
MEDPASS_ZIP_PASSWORD_PARAMETER_STORE_KEY = os.environ["MEDPASS_ZIP_PASSWORD_PARAMETER_STORE_KEY"]
|
||||
|
||||
LOG_LEVEL = os.environ["LOG_LEVEL"]
|
||||
TZ = os.environ["TZ"]
|
||||
|
||||
# 定数
|
||||
# 多重起動抑制用のコントロールファイルの拡張子
|
||||
EXCLUSIVE_CONTROL_FILE_EXT = '.doing'
|
||||
# tmpフォルダパス
|
||||
PATH_TMP = '/tmp'
|
||||
# 拡張子
|
||||
ZIP_FILE_EXT = 'zip'
|
||||
CSV_FILE_EXT = 'csv'
|
||||
|
||||
# S3クライアント
|
||||
s3_client = boto3.client('s3')
|
||||
# SystemsManagerクライアント
|
||||
ssm_client = boto3.client('ssm')
|
||||
|
||||
# logger設定
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
||||
def log_datetime_convert_tz(*arg):
|
||||
"""ログに出力するタイムスタンプのロケールを変更する(JST指定)"""
|
||||
return datetime.datetime.now(ZoneInfo(TZ)).timetuple()
|
||||
|
||||
|
||||
formatter = logging.Formatter(
|
||||
'[%(levelname)s]\t%(asctime)s\t%(message)s\n',
|
||||
'%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
formatter.converter = log_datetime_convert_tz
|
||||
for handler in logger.handlers:
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
level = logging.getLevelName(LOG_LEVEL)
|
||||
if not isinstance(level, int):
|
||||
level = logging.INFO
|
||||
logger.setLevel(level)
|
||||
|
||||
|
||||
def extract_zip_with_password(zip_filepath: str, extract_to_folder: str, password: str) -> os.path:
|
||||
"""
|
||||
暗号化ZIPを解凍する。
|
||||
|
||||
:param zip_filepath: ZIPファイルが保管されているフォルダパス
|
||||
:param extract_to_folder: ZIPファイルの解凍先フォルダ
|
||||
:param password: ZIPパスワード
|
||||
|
||||
:return 解凍されたファイルパス
|
||||
"""
|
||||
# ZIPを解凍
|
||||
try:
|
||||
with pyzipper.AESZipFile(zip_filepath) as z:
|
||||
# ZIP内のファイルは1つのみ
|
||||
inner_filename = z.filelist[0].filename
|
||||
z.extractall(path=extract_to_folder, pwd=password.encode())
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
return os.path.join(extract_to_folder, inner_filename)
|
||||
|
||||
|
||||
def get_s3_event_parameter(event: dict) -> tuple[str, str, str, str]:
|
||||
s3_event = event["Records"][0]["s3"]
|
||||
event_bucket_name: str = s3_event["bucket"]["name"]
|
||||
event_object_key: str = s3_event["object"]["key"]
|
||||
event_file_name: str = os.path.basename(event_object_key)
|
||||
event_folder_name: str = os.path.dirname(event_object_key).split('/')[0]
|
||||
|
||||
return event_bucket_name, event_object_key, event_file_name, event_folder_name
|
||||
|
||||
|
||||
def get_ssm_params(parameter_key: str, with_decryption: bool = True) -> str:
|
||||
"""SSMパラメータストアから指定されたパラメータ名の値を取得する"""
|
||||
response = ssm_client.get_parameter(
|
||||
Name=parameter_key, WithDecryption=with_decryption)
|
||||
parameter_value: str = response['Parameter']['Value']
|
||||
return parameter_value
|
||||
|
||||
|
||||
def delete_doing_file(event: dict) -> None:
|
||||
""".doingファイルをバケット上から削除する"""
|
||||
# イベント情報を取得
|
||||
(
|
||||
event_bucket_name,
|
||||
event_object_key,
|
||||
_,
|
||||
_
|
||||
) = get_s3_event_parameter(event)
|
||||
# ⑨ メモリに保持したバケット名/フォルダ名内の「受信データファイル名.doing」ファイルを削除する
|
||||
s3_client.delete_object(
|
||||
Bucket=event_bucket_name, Key=f'{event_object_key}{EXCLUSIVE_CONTROL_FILE_EXT}')
|
||||
|
||||
|
||||
def handler(event, context) -> None:
|
||||
try:
|
||||
# ① 処理開始ログを出力する
|
||||
logger.info('I-01-01 処理開始 medパスデータ解凍・復号化・転送処理')
|
||||
|
||||
# ② 処理開始時に受け取ったイベント情報をログに出力する
|
||||
# バケット名・フォルダ名・受信データファイル名をメモリに保持
|
||||
(
|
||||
event_bucket_name,
|
||||
event_object_key,
|
||||
event_file_name,
|
||||
event_folder_name
|
||||
) = get_s3_event_parameter(event)
|
||||
logger.info(f'I-02-01 受信バケット:{event_bucket_name}')
|
||||
logger.info(f'I-02-01 フォルダ名:{event_folder_name}')
|
||||
logger.info(f'I-02-01 ファイル名:{event_file_name}')
|
||||
# ③ S3イベントによるLambdaの重複発火防止の為、メモリに保持したバケット名/フォルダ名内に、「受信データファイル名.doing」ファイルが存在するかチェックする
|
||||
try:
|
||||
s3_client.head_object(
|
||||
Bucket=event_bucket_name, Key=f'{event_object_key}{EXCLUSIVE_CONTROL_FILE_EXT}')
|
||||
logger.error(
|
||||
f'E-01-01 {event_bucket_name}/{event_object_key}は現在処理中です。処理を終了します。')
|
||||
return
|
||||
except Exception:
|
||||
# .doingファイルが見つからなかった場合は、処理を続行する
|
||||
# メモリに保持したバケット名/フォルダ名内に、「受信データファイル名.doing」ファイルを作成する
|
||||
logger.info('I-03-01 medパスデータの解凍・復号化・転送を開始します')
|
||||
s3_client.put_object(
|
||||
Bucket=event_bucket_name, Key=f'{event_object_key}{EXCLUSIVE_CONTROL_FILE_EXT}', Body=b'')
|
||||
|
||||
# ④ S3から暗号化ZIPファイルを読み込む
|
||||
try:
|
||||
logger.info(
|
||||
f'I-04-01 暗号化ZIPファイル読込 読込元:{event_bucket_name}/{event_object_key}')
|
||||
s3_client.download_file(
|
||||
event_bucket_name, event_object_key, os.path.join(PATH_TMP, event_file_name))
|
||||
logger.info('I-04-02 暗号化ZIPファイルをダウンロードしました')
|
||||
except Exception as e:
|
||||
logger.exception(f'E-04-01 暗号化ZIPファイルのダウンロードに失敗しました エラー内容:{e}')
|
||||
delete_doing_file(event)
|
||||
return
|
||||
|
||||
# ⑤ ZIP解凍パスワードをSSM パラメータストアから取得する
|
||||
try:
|
||||
logger.info('I-05-01 ZIP解凍パスワードを読込')
|
||||
zip_password = get_ssm_params(
|
||||
MEDPASS_ZIP_PASSWORD_PARAMETER_STORE_KEY)
|
||||
except Exception as e:
|
||||
logger.exception(f'E-05-01 ZIP解凍パスワードの読み込みに失敗しました エラー内容:{e}')
|
||||
delete_doing_file(event)
|
||||
return
|
||||
|
||||
# ⑥ ZIPファイルを解凍してローカルに保存
|
||||
try:
|
||||
logger.info(f'I-05-02 ZIP解凍開始')
|
||||
extracted_zip_file_path = extract_zip_with_password(
|
||||
os.path.join(PATH_TMP, event_file_name), PATH_TMP, zip_password)
|
||||
except RuntimeError as e:
|
||||
if 'password' in str(e).lower():
|
||||
# パスワードが間違っている場合のエラー
|
||||
logger.exception(
|
||||
f'E-05-02 ZIPのパスワードが不正のため、解凍に失敗しました エラー内容:{e}')
|
||||
delete_doing_file(event)
|
||||
return
|
||||
else:
|
||||
# 想定外のエラー
|
||||
raise e
|
||||
# ZIPファイルが壊れている場合のエラー
|
||||
except BadZipFile as e:
|
||||
logger.exception(f'E-05-03 ZIPの形式が不正のため、解凍に失敗しました エラー内容:{e}')
|
||||
delete_doing_file(event)
|
||||
return
|
||||
|
||||
# データ登録用にファイルをリネーム
|
||||
# ZIPファイル名がyyyymmdd.zipのため、年月日部分をデータ登録用ファイル名の末尾につけ、拡張子をCSVに変更
|
||||
data_import_file_name = f'{DATA_IMPORT_FILENAME}_{event_file_name.lower().replace(ZIP_FILE_EXT, CSV_FILE_EXT)}'
|
||||
logger.info(f'I-05-03 ZIP解凍成功')
|
||||
|
||||
# ⑥ 受信した暗号化ZIPファイルと解凍後のファイルをバックアップする
|
||||
backup_copy_source = {
|
||||
'Bucket': event_bucket_name, 'Key': event_object_key}
|
||||
execute_date_yyyymmdd = datetime.date.today().strftime('%Y/%m/%d')
|
||||
|
||||
# ZIPファイルのバックアップ
|
||||
s3_client.copy_object(
|
||||
Bucket=HCP_WEB_BACKUP_BUCKET,
|
||||
Key=f'{BACKUP_ZIPFILE_FOLDER}/{execute_date_yyyymmdd}/{event_file_name}',
|
||||
CopySource=backup_copy_source
|
||||
)
|
||||
logger.info(
|
||||
f'I-06-01 medパス受信データのバックアップ完了:{HCP_WEB_BACKUP_BUCKET}/{BACKUP_ZIPFILE_FOLDER}/{execute_date_yyyymmdd}/{event_file_name}')
|
||||
|
||||
# 解凍後ファイルのバックアップ
|
||||
s3_client.upload_file(
|
||||
extracted_zip_file_path,
|
||||
Bucket=HCP_WEB_BACKUP_BUCKET,
|
||||
Key=f'{BACKUP_DATA_IMPORT_FOLDER}/{execute_date_yyyymmdd}/{data_import_file_name}'
|
||||
)
|
||||
logger.info(
|
||||
f'I-06-02 medパス解凍後データのバックアップ完了:{HCP_WEB_BACKUP_BUCKET}/{BACKUP_DATA_IMPORT_FOLDER}/{execute_date_yyyymmdd}/{data_import_file_name}')
|
||||
|
||||
# ⑦ 解凍後のファイルをデータ登録バケットに転送する
|
||||
data_import_copy_source = {'Bucket': HCP_WEB_BACKUP_BUCKET,
|
||||
'Key': f'{BACKUP_DATA_IMPORT_FOLDER}/{execute_date_yyyymmdd}/{data_import_file_name}'}
|
||||
s3_client.copy_object(
|
||||
Bucket=DATA_IMPORT_BUCKET,
|
||||
Key=f'{HCP_WEB_TARGET_FOLDER}/{data_import_file_name}',
|
||||
CopySource=data_import_copy_source
|
||||
)
|
||||
|
||||
# アップロード後、元のバケットからは削除する
|
||||
s3_client.delete_object(Bucket=event_bucket_name, Key=event_object_key)
|
||||
logger.info(
|
||||
f'I-07-01 medパス解凍後データの転送完了:{DATA_IMPORT_BUCKET}/{HCP_WEB_TARGET_FOLDER}/{data_import_file_name}')
|
||||
|
||||
# ⑧ メモリに保持したバケット名/フォルダ名内の「受信データファイル名.doing」ファイルを削除する
|
||||
delete_doing_file(event)
|
||||
|
||||
logger.info('I-08-01 処理終了 medパスデータ解凍・復号化・転送処理')
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f'想定外のエラーが発生しました。処理を終了します。 例外内容:{e}')
|
||||
delete_doing_file(event)
|
||||
raise e
|
||||
@ -0,0 +1,214 @@
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import boto3
|
||||
|
||||
# 環境変数
|
||||
CONFIG_BUCKET_NAME = os.environ["CONFIG_BUCKET_NAME"]
|
||||
BUCKET_TRANSFER_SETTING_FILE_FOLDER = os.environ["BUCKET_TRANSFER_SETTING_FILE_FOLDER"]
|
||||
BUCKET_TRANSFER_SETTING_FILE_NAME = os.environ["BUCKET_TRANSFER_SETTING_FILE_NAME"]
|
||||
LOG_LEVEL = os.environ["LOG_LEVEL"]
|
||||
TZ = os.environ["TZ"]
|
||||
ENV = os.environ["ENV"]
|
||||
|
||||
# 定数
|
||||
EXCLUSIVE_CONTROL_FILE_EXT = '.doing'
|
||||
|
||||
# S3クライアント
|
||||
s3_client = boto3.client('s3')
|
||||
|
||||
# logger設定
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
||||
def log_datetime_convert_tz(*arg):
|
||||
"""ログに出力するタイムスタンプのロケールを変更する(JST指定)"""
|
||||
return datetime.datetime.now(ZoneInfo(TZ)).timetuple()
|
||||
|
||||
|
||||
formatter = logging.Formatter(
|
||||
'[%(levelname)s]\t%(asctime)s\t%(message)s\n',
|
||||
'%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
formatter.converter = log_datetime_convert_tz
|
||||
for handler in logger.handlers:
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
level = logging.getLevelName(LOG_LEVEL)
|
||||
if not isinstance(level, int):
|
||||
level = logging.INFO
|
||||
logger.setLevel(level)
|
||||
|
||||
|
||||
def get_s3_event_parameter(event: dict) -> tuple[str, str, str, str]:
|
||||
"""Lambdaに送信されたEvent情報からS3のイベントを取得する"""
|
||||
s3_event = event["Records"][0]["s3"]
|
||||
event_bucket_name: str = s3_event["bucket"]["name"]
|
||||
event_object_key: str = s3_event["object"]["key"]
|
||||
event_file_name: str = os.path.basename(event_object_key)
|
||||
event_folder_name: str = os.path.dirname(event_object_key).split('/')[0]
|
||||
|
||||
return event_bucket_name, event_object_key, event_file_name, event_folder_name
|
||||
|
||||
|
||||
def delete_doing_file(event: dict) -> None:
|
||||
""".doingファイルをバケット上から削除する"""
|
||||
# イベント情報を取得
|
||||
(
|
||||
event_bucket_name,
|
||||
event_object_key,
|
||||
_,
|
||||
_
|
||||
) = get_s3_event_parameter(event)
|
||||
# ⑨ メモリに保持したバケット名/フォルダ名内の「受信データファイル名.doing」ファイルを削除する
|
||||
s3_client.delete_object(
|
||||
Bucket=event_bucket_name, Key=f'{event_object_key}{EXCLUSIVE_CONTROL_FILE_EXT}')
|
||||
|
||||
|
||||
def lambda_handler(event, context):
|
||||
"""Lambdaハンドラー関数"""
|
||||
# ① 処理開始ログを出力する
|
||||
logger.info('I-01-01 処理開始 S3バケット間ファイル転送処理')
|
||||
|
||||
# ② 処理開始時に受け取ったイベント情報をログに出力する
|
||||
# バケット名・フォルダ名・受信データファイル名をメモリに保持
|
||||
(
|
||||
event_bucket_name,
|
||||
event_object_key,
|
||||
event_file_name,
|
||||
event_folder_name
|
||||
) = get_s3_event_parameter(event)
|
||||
|
||||
try:
|
||||
logger.info(f'I-02-01 受信バケット:{event_bucket_name}')
|
||||
logger.info(f'I-02-01 フォルダ名:{event_folder_name}')
|
||||
logger.info(f'I-02-01 受信ファイル名:{event_file_name}')
|
||||
|
||||
# ③ S3イベントによるLambdaの重複発火防止の為、メモリに保持したバケット名/フォルダ名内に、「受信データファイル名.doing」ファイルが存在するかチェックする
|
||||
try:
|
||||
s3_client.head_object(
|
||||
Bucket=event_bucket_name, Key=f'{event_object_key}{EXCLUSIVE_CONTROL_FILE_EXT}')
|
||||
logger.error(
|
||||
f'E-01-01 {event_bucket_name}/{event_object_key}は現在処理中です。処理を終了します。')
|
||||
return
|
||||
except Exception:
|
||||
# .doingファイルが見つからなかった場合は、処理を続行する
|
||||
# メモリに保持したバケット名/フォルダ名内に、「受信データファイル名.doing」ファイルを作成する
|
||||
logger.info(
|
||||
f'I-03-01 {event_bucket_name}/{event_object_key}を転送します')
|
||||
s3_client.put_object(
|
||||
Bucket=event_bucket_name, Key=f'{event_object_key}{EXCLUSIVE_CONTROL_FILE_EXT}', Body=b'')
|
||||
|
||||
# ④ バケット転送設定ファイルを特定する
|
||||
transfer_config_file_response = s3_client.get_object(
|
||||
Bucket=CONFIG_BUCKET_NAME, Key=f'{BUCKET_TRANSFER_SETTING_FILE_FOLDER}/{BUCKET_TRANSFER_SETTING_FILE_NAME}')
|
||||
transfer_config_file = json.loads(
|
||||
transfer_config_file_response['Body'].read().decode('utf8'))
|
||||
|
||||
# ⑤ バケット転送設定ファイルのキー[受信ファイル名正規表現パターン]と、[メモリに保持した受信ファイル名]と一致するものを取得する。
|
||||
|
||||
transfer_config = None
|
||||
for key in transfer_config_file.keys():
|
||||
filename_regex = re.compile(key)
|
||||
match_result = filename_regex.fullmatch(event_file_name)
|
||||
if match_result is not None:
|
||||
transfer_config = transfer_config_file[key]
|
||||
break
|
||||
|
||||
if transfer_config is None:
|
||||
logger.error(
|
||||
f'E-03-01 S3バケットの転送設定が見つかりません。{CONFIG_BUCKET_NAME}/{BUCKET_TRANSFER_SETTING_FILE_FOLDER}/{BUCKET_TRANSFER_SETTING_FILE_NAME}')
|
||||
delete_doing_file(event)
|
||||
return
|
||||
|
||||
# ⑥ 受信ファイルを、⑤でメモリ上に保持したJSONオブジェクトの設定内容に基づき、バックアップする
|
||||
if transfer_config.get('backup_setting', None) is not None:
|
||||
backup_setting = transfer_config['backup_setting']
|
||||
copy_source = {'Bucket': event_bucket_name,
|
||||
'Key': event_object_key}
|
||||
backup_bucket = backup_setting['backup_bucket'].format(env=ENV)
|
||||
backup_date_pattern = datetime.date.today().strftime(
|
||||
backup_setting['date_pattern'])
|
||||
s3_client.copy_object(
|
||||
Bucket=backup_bucket,
|
||||
Key=f'{event_folder_name}/{backup_date_pattern}/{event_file_name}',
|
||||
CopySource=copy_source
|
||||
)
|
||||
logger.info(
|
||||
f'I-04-01 受信ファイルのバックアップ完了::{backup_bucket}/{event_folder_name}/{backup_date_pattern}/{event_file_name}')
|
||||
|
||||
# ⑦ 受信ファイルを、⑤でメモリ上に保持したJSONオブジェクトの設定内容に基づき、移動する
|
||||
destination_bucket = transfer_config['destination_bucket'].format(
|
||||
env=ENV)
|
||||
destination_folder = transfer_config['destination_folder']
|
||||
s3_client.copy_object(
|
||||
Bucket=destination_bucket,
|
||||
Key=f"{destination_folder}/{event_file_name}",
|
||||
CopySource=copy_source
|
||||
)
|
||||
# コピー後、元のバケットからは削除する
|
||||
s3_client.delete_object(Bucket=event_bucket_name, Key=event_object_key)
|
||||
logger.info(
|
||||
f'I-05-01 受信ファイルの転送完了:{destination_bucket}/{destination_folder}/{event_file_name}')
|
||||
|
||||
# ⑧ メモリに保持したバケット名/フォルダ名内の「受信データファイル名.doing」ファイルを削除する
|
||||
delete_doing_file(event)
|
||||
|
||||
logger.info('I-06-01 処理終了 S3バケット間ファイル転送処理')
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f'E-99 想定外のエラーが発生しました。処理を終了します。ファイル名フルパス: {event_bucket_name}/{event_object_key} 例外内容:{e}'
|
||||
)
|
||||
delete_doing_file(event)
|
||||
raise e
|
||||
|
||||
return
|
||||
|
||||
# 動作確認用のコード
|
||||
# if __name__ == '__main__':
|
||||
# lambda_handler(
|
||||
# {
|
||||
# "Records": [
|
||||
# {
|
||||
# "eventVersion": "2.1",
|
||||
# "eventSource": "aws:s3",
|
||||
# "awsRegion": "ap-northeast-1",
|
||||
# "eventTime": "2024-07-16T07:10:33.021Z",
|
||||
# "eventName": "ObjectCreated:Put",
|
||||
# "userIdentity": {
|
||||
# "principalId": "AWS:AIDA4A3J5AIPDAT6MUJPZ"
|
||||
# },
|
||||
# "requestParameters": {
|
||||
# "sourceIPAddress": "118.238.231.215"
|
||||
# },
|
||||
# "responseElements": {
|
||||
# "x-amz-request-id": "0BST21P92A15BH55",
|
||||
# "x-amz-id-2": "db9n9RpQxHEnq5o5ZLCeIGpuka54ghMHcbJ2Rj9aCcpjf111D4dyTZn5w5VvzV6W56rU89cSx/ihzkEHs8wk30ckbtRMYQ0byJn0UfK6bjg="
|
||||
# },
|
||||
# "s3": {
|
||||
# "s3SchemaVersion": "1.0",
|
||||
# "configurationId": "accesslog-receive-event2",
|
||||
# "bucket": {
|
||||
# "name": "mbj-newdwh2021-staging-hcp-web-receive",
|
||||
# "ownerIdentity": {
|
||||
# "principalId": "A1YQ10QIZBI5OE"
|
||||
# },
|
||||
# "arn": "arn:aws:s3:::mbj-newdwh2021-staging-hcp-web-receive"
|
||||
# },
|
||||
# "object": {
|
||||
# "key": "palantir/hcpweb_accesslog_2024-07-16-071045.csv",
|
||||
# "size": 597820,
|
||||
# "eTag": "94299e880925b6f655c090521ff83d7a",
|
||||
# "sequencer": "0066961CE8E7A670F2"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# ]
|
||||
# },
|
||||
# None
|
||||
# )
|
||||
17
rds_mysql/stored_procedure/medaca_common/convert_to_date.sql
Normal file
17
rds_mysql/stored_procedure/medaca_common/convert_to_date.sql
Normal file
@ -0,0 +1,17 @@
|
||||
-- A5M2で実行時に[SQL] - [スラッシュ(/)のみの行でSQLを区切る]に変えてから実行する
|
||||
CREATE FUNCTION medaca_common.convert_to_date(date_string VARCHAR(255), _format VARCHAR(50)) RETURNS datetime
|
||||
DETERMINISTIC
|
||||
BEGIN
|
||||
DECLARE converted_date DATETIME;
|
||||
|
||||
DECLARE EXIT HANDLER FOR SQLEXCEPTION
|
||||
BEGIN
|
||||
-- エラー発生時にNULLを返す
|
||||
RETURN NULL;
|
||||
END;
|
||||
|
||||
-- 日付変換の実行
|
||||
SET converted_date = STR_TO_DATE(date_string, _format);
|
||||
|
||||
RETURN converted_date;
|
||||
END
|
||||
16
s3/config/bucket_transfer_config/bucket_transfer_config.json
Normal file
16
s3/config/bucket_transfer_config/bucket_transfer_config.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"hcpweb_accesslog_\\d{4}-\\d{2}-\\d{2}-\\d{6}\\.(csv|CSV)": {
|
||||
"Description": {
|
||||
"データソース名": "hcp_web",
|
||||
"送信元システム名": "Palantir",
|
||||
"データの論理名(説明)": "HCPウェブアクセスログデータ",
|
||||
"特記事項": "日〜金 AM1:00にMeDaCAへ連携"
|
||||
},
|
||||
"destination_bucket": "mbj-newdwh2021-{env}-data",
|
||||
"destination_folder": "hcp_web/target",
|
||||
"backup_setting": {
|
||||
"backup_bucket": "mbj-newdwh2021-{env}-backup-hcp-web",
|
||||
"date_pattern": "%Y/%m/%d"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
宛先各位
|
||||
PalantirのHCPウェブアクセスログデータを受領できておりません。
|
||||
|
||||
Palantir側の送信状況のご確認をお願いいたします。
|
||||
|
||||
尚、本メールはシステム自動送信のため、返信は出来ません。
|
||||
本件に関する問い合わせは、HCPウェブアクセスデータ担当者にお願いいたします。
|
||||
@ -0,0 +1 @@
|
||||
【MeDaCa連携エラー通知】Palantir HCPウェブアクセスログデータ未受領
|
||||
@ -0,0 +1,7 @@
|
||||
宛先各位
|
||||
medパス社のHCPウェブアクセスユーザ情報を受領できておりません。
|
||||
|
||||
medパス社へ送信状況のご確認をお願いいたします。
|
||||
|
||||
尚、本メールはシステム自動送信のため、返信は出来ません。
|
||||
本件に関する問い合わせは、HCPウェブアクセスデータ担当者にお願いいたします。
|
||||
@ -0,0 +1 @@
|
||||
【MeDaCa連携エラー通知】medパス HCPウェブアクセスユーザ情報未受領
|
||||
@ -0,0 +1 @@
|
||||
hcpweb_accesslog_\d{4}-\d{2}-\d{2}-\d{6}\.(csv|CSV) Palantir HCPウェブアクセスログデータ
|
||||
@ -0,0 +1 @@
|
||||
hcp_web_medpass_user_mst_\d{14}\.(CSV|csv) medパス社ユーザ情報
|
||||
@ -1,3 +1,3 @@
|
||||
{
|
||||
"check_target_schemas": ["custom01", "custom02", "custom03", "custom04"]
|
||||
"check_target_schemas": ["custom01", "custom02", "custom03", "custom04","custom05"]
|
||||
}
|
||||
|
||||
4
s3/data/hcp_web/settings/configmap.config
Normal file
4
s3/data/hcp_web/settings/configmap.config
Normal file
@ -0,0 +1,4 @@
|
||||
/* medパス社データ */
|
||||
hcp_web_medpass_user_mst_[0-9]{14}\.(CSV|csv) hcp_web_medpass_user_mst.txt
|
||||
/* Parantir ウェブアクセスログデータ */
|
||||
hcpweb_accesslog_[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}\.(CSV|csv) hcpweb_accesslog.txt
|
||||
19
s3/data/hcp_web/settings/hcp_web_accsess_log_ex.sql
Normal file
19
s3/data/hcp_web/settings/hcp_web_accsess_log_ex.sql
Normal file
@ -0,0 +1,19 @@
|
||||
/* date_time(文字型) を日付型に変換してセットする。 */
|
||||
/* date_time(文字型) はUTCの日付を取得するため、date_time(日付型)はMeDaCAのポリシーに合わせてJSTに変換する */
|
||||
UPDATE src06.hcp_web_access_log SET
|
||||
date_time = IF (
|
||||
medaca_common.convert_to_date(date_time_org, '%Y-%m-%d %H:%i:%s') IS NULL,
|
||||
NULL,
|
||||
medaca_common.convert_to_date(date_time_org, '%Y-%m-%d %H:%i:%s') + INTERVAL 9 HOUR
|
||||
)
|
||||
WHERE ins_date >= (now() - INTERVAL 1 YEAR);
|
||||
|
||||
/* post_evar29(文字型)を日付型に変換してセットする。 */
|
||||
/* post_evar29(文字型)はUTCの日付を取得するため、post_evar29(日付型)はMeDaCAのポリシーに合わせてJSTに変換する */
|
||||
UPDATE src06.hcp_web_access_log SET
|
||||
post_evar29 = IF (
|
||||
medaca_common.convert_to_date(post_evar29_org, '%Y-%m-%dT%H:%i:%s.%fZ') IS NULL,
|
||||
NULL,
|
||||
medaca_common.convert_to_date(post_evar29_org, '%Y-%m-%dT%H:%i:%s.%fZ') + INTERVAL 9 HOUR
|
||||
)
|
||||
WHERE ins_date >= (now() - INTERVAL 1 YEAR);
|
||||
13
s3/data/hcp_web/settings/hcp_web_medpass_user_mst.txt
Normal file
13
s3/data/hcp_web/settings/hcp_web_medpass_user_mst.txt
Normal file
@ -0,0 +1,13 @@
|
||||
hcp_web
|
||||
,
|
||||
utf-8
|
||||
"
|
||||
LF
|
||||
1
|
||||
39
|
||||
Sub,UserName,FamilyName,GivenName,FamilyNameKana,GivenNameKana,BirthDate,Gender,CreateDate,LastUpdate,Job,Job_CareMgr,FacilityPrefecture,Workplace,School,GraduationYear,DoctorLicenseRegistYear,Service,SpecialistQualification,RawMailAddress,TestUser,MDBFacilityCode,FacilityPrefectureCode,FacilityMunicipalityCode,FacilityFullName,FacilityNameAbbr,CustomizedFacilityName,FacilityAddress,FacilityTelNumber,MDBFacilityPostalCode,MDBPersonalCode,MDBService,MDBServiceCategory,MDBWorkingDepartment,MDBGender,MDBOldSchool,MDBSubmitYear,MDBAllFacilityCode,RpOriginalAttributes
|
||||
Sub,UserName,FamilyName,GivenName,FamilyNameKana,GivenNameKana,BirthDate_org,Gender,CreateDate_org,LastUpdate_org,Job,Job_CareMgr,FacilityPrefecture,Workplace,School,GraduationYear,DoctorLicenseRegistYear,Service,SpecialistQualification,RawMailAddress,TestUser,MDBFacilityCode,FacilityPrefectureCode,FacilityMunicipalityCode,FacilityFullName,FacilityNameAbbr,CustomizedFacilityName,FacilityAddress,FacilityTelNumber,MDBFacilityPostalCode,MDBPersonalCode,MDBService,MDBServiceCategory,MDBWorkingDepartment,MDBGender,MDBOldSchool,MDBSubmitYear,MDBAllFacilityCode,RpOriginalAttributes,dummy01
|
||||
src06.hcp_web_medpass_user_mst
|
||||
org06.hcp_web_medpass_user_mst
|
||||
hcp_web_medpass_user_mst_ex.sql
|
||||
|
||||
23
s3/data/hcp_web/settings/hcp_web_medpass_user_mst_ex.sql
Normal file
23
s3/data/hcp_web/settings/hcp_web_medpass_user_mst_ex.sql
Normal file
@ -0,0 +1,23 @@
|
||||
/* 生年月日(文字型) を日付型に変換してセットする。文字型のデータがNULLの場合はNULLをセットする */
|
||||
UPDATE src06.hcp_web_medpass_user_mst SET
|
||||
BirthDate = IF(
|
||||
BirthDate_org IS NULL,
|
||||
NULL,
|
||||
medaca_common.convert_to_date(BirthDate_org, '%Y-%m-%d')
|
||||
);
|
||||
|
||||
/* 登録日時(文字型)を日付型に変換してセットする。文字型のデータがNULLの場合はNULLをセットする */
|
||||
UPDATE src06.hcp_web_medpass_user_mst SET
|
||||
CreateDate = IF(
|
||||
CreateDate_org IS NULL,
|
||||
NULL,
|
||||
medaca_common.convert_to_date(CreateDate_org, '%Y-%m-%dT%H:%i:%s+09:00')
|
||||
);
|
||||
|
||||
/* 最終更新日時(文字型)を日付型に変換してセットする。文字型のデータがNULLの場合はNULLをセットする */
|
||||
UPDATE src06.hcp_web_medpass_user_mst SET
|
||||
LastUpdate = IF(
|
||||
LastUpdate_org IS NULL,
|
||||
NULL,
|
||||
medaca_common.convert_to_date(LastUpdate_org, '%Y-%m-%dT%H:%i:%s+09:00')
|
||||
);
|
||||
13
s3/data/hcp_web/settings/hcpweb_accesslog.txt
Normal file
13
s3/data/hcp_web/settings/hcpweb_accesslog.txt
Normal file
@ -0,0 +1,13 @@
|
||||
hcp_web
|
||||
,
|
||||
utf-8
|
||||
"
|
||||
LF
|
||||
1
|
||||
178
|
||||
id,browser,campaign,carrier,click_context,date_time,duplicate_events,duplicated_from,evar1,evar10,evar16,evar24,evar4,evar54,evar55,evar71,evar73,evar74,evar8,evar9,event_list,exclude_hit,filename,first_hit_page_url,first_hit_ref_type,first_hit_referrer,first_hit_time_gmt,geo_city,geo_country,geo_dma,geo_region,hit_source,hit_time_gmt,hitid_high,hitid_low,last_hit_time_gmt,mobile_id,mobiledevice,os,page_event,page_event_var1,page_event_var2,page_url,pagename,paid_search,post_campaign,post_channel,post_cust_hit_time_gmt,post_evar1,post_evar10,post_evar100,post_evar11,post_evar12,post_evar13,post_evar14,post_evar15,post_evar16,post_evar17,post_evar18,post_evar19,post_evar2,post_evar20,post_evar21,post_evar22,post_evar23,post_evar24,post_evar25,post_evar26,post_evar27,post_evar28,post_evar29,post_evar3,post_evar30,post_evar31,post_evar32,post_evar33,post_evar34,post_evar35,post_evar36,post_evar37,post_evar38,post_evar39,post_evar4,post_evar40,post_evar41,post_evar42,post_evar43,post_evar44,post_evar45,post_evar46,post_evar47,post_evar48,post_evar49,post_evar5,post_evar50,post_evar51,post_evar52,post_evar53,post_evar54,post_evar55,post_evar56,post_evar57,post_evar58,post_evar59,post_evar6,post_evar60,post_evar61,post_evar62,post_evar63,post_evar64,post_evar65,post_evar66,post_evar67,post_evar68,post_evar69,post_evar7,post_evar70,post_evar71,post_evar72,post_evar73,post_evar74,post_evar75,post_evar76,post_evar77,post_evar78,post_evar79,post_evar8,post_evar80,post_evar81,post_evar82,post_evar83,post_evar84,post_evar85,post_evar86,post_evar87,post_evar88,post_evar89,post_evar9,post_evar90,post_evar91,post_evar92,post_evar93,post_evar94,post_evar95,post_evar96,post_evar97,post_evar98,post_evar99,post_event_list,post_mobiledevice,post_page_event,post_page_url,post_pagename,post_referrer,post_search_engine,post_visid_high,post_visid_low,post_visid_type,ref_domain,ref_type,referrer,sampled_hit,secondary_hit,truncated_hit,user_agent,user_server,username,va_closer_detail,va_finder_detail,visid_new,visit_num,visit_page_num,visit_ref_domain,visit_ref_type,visit_referrer,visit_search_engine,visit_start_page_url,visit_start_time_gmt
|
||||
id,browser,campaign,carrier,click_context,date_time_org,duplicate_events,duplicated_from,evar1,evar10,evar16,evar24,evar4,evar54,evar55,evar71,evar73,evar74,evar8,evar9,event_list,exclude_hit,filename,first_hit_page_url,first_hit_ref_type,first_hit_referrer,first_hit_time_gmt,geo_city,geo_country,geo_dma,geo_region,hit_source,hit_time_gmt,hitid_high,hitid_low,last_hit_time_gmt,mobile_id,mobiledevice,os,page_event,page_event_var1,page_event_var2,page_url,pagename,paid_search,post_campaign,post_channel,post_cust_hit_time_gmt,post_evar1,post_evar10,post_evar100,post_evar11,post_evar12,post_evar13,post_evar14,post_evar15,post_evar16,post_evar17,post_evar18,post_evar19,post_evar2,post_evar20,post_evar21,post_evar22,post_evar23,post_evar24,post_evar25,post_evar26,post_evar27,post_evar28,post_evar29_org,post_evar3,post_evar30,post_evar31,post_evar32,post_evar33,post_evar34,post_evar35,post_evar36,post_evar37,post_evar38,post_evar39,post_evar4,post_evar40,post_evar41,post_evar42,post_evar43,post_evar44,post_evar45,post_evar46,post_evar47,post_evar48,post_evar49,post_evar5,post_evar50,post_evar51,post_evar52,post_evar53,post_evar54,post_evar55,post_evar56,post_evar57,post_evar58,post_evar59,post_evar6,post_evar60,post_evar61,post_evar62,post_evar63,post_evar64,post_evar65,post_evar66,post_evar67,post_evar68,post_evar69,post_evar7,post_evar70,post_evar71,post_evar72,post_evar73,post_evar74,post_evar75,post_evar76,post_evar77,post_evar78,post_evar79,post_evar8,post_evar80,post_evar81,post_evar82,post_evar83,post_evar84,post_evar85,post_evar86,post_evar87,post_evar88,post_evar89,post_evar9,post_evar90,post_evar91,post_evar92,post_evar93,post_evar94,post_evar95,post_evar96,post_evar97,post_evar98,post_evar99,post_event_list,post_mobiledevice,post_page_event,post_page_url,post_pagename,post_referrer,post_search_engine,post_visid_high,post_visid_low,post_visid_type,ref_domain,ref_type,referrer,sampled_hit,secondary_hit,truncated_hit,user_agent,user_server,username,va_closer_detail,va_finder_detail,visid_new,visit_num,visit_page_num,visit_ref_domain,visit_ref_type,visit_referrer,visit_search_engine,visit_start_page_url,visit_start_time_gmt
|
||||
src06.hcp_web_access_log
|
||||
org06.hcp_web_access_log
|
||||
hcp_web_accsess_log_ex.sql
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user