From 41b85bc714598b74b05003d488d2f5db222573e5 Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Tue, 13 Sep 2022 10:33:31 -0700 Subject: [PATCH 01/22] implement email_weekly_roundup.py and email template --- scripts/email_weekly_roundup.jinja2 | 8 +++ scripts/email_weekly_roundup.py | 101 ++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 scripts/email_weekly_roundup.jinja2 create mode 100644 scripts/email_weekly_roundup.py diff --git a/scripts/email_weekly_roundup.jinja2 b/scripts/email_weekly_roundup.jinja2 new file mode 100644 index 00000000..01527f07 --- /dev/null +++ b/scripts/email_weekly_roundup.jinja2 @@ -0,0 +1,8 @@ +

DataCite DOI Weekly Roundup

+

The following DOIs were reserved, updated or released during the week {{ first_date.strftime('%m/%d') }} through {{ last_date.strftime('%m/%d') }}:

+ +

Contact Us

diff --git a/scripts/email_weekly_roundup.py b/scripts/email_weekly_roundup.py new file mode 100644 index 00000000..aca2812b --- /dev/null +++ b/scripts/email_weekly_roundup.py @@ -0,0 +1,101 @@ +import os.path +from datetime import datetime, timedelta, date +from email.message import EmailMessage +from typing import List, Dict + +import jinja2 +from pkg_resources import resource_filename + +from pds_doi_service.core.db.doi_database import DOIDataBase +from pds_doi_service.core.entities.doi import DoiRecord +from pds_doi_service.core.util.config_parser import DOIConfigUtil +from pds_doi_service.core.util.emailer import Emailer as PDSEmailer +from pds_doi_service.core.util.general_util import get_logger + + +def get_start_of_local_week() -> datetime: + """Return the start of the local-timezones week as a tz-aware datetime""" + today = datetime.now().date() + start_of_today = datetime(today.year, today.month, today.day) + return start_of_today.astimezone() - timedelta(days=today.weekday()) + + +def fetch_dois_modified_between(begin: datetime, end: datetime, db_filepath) -> List[DoiRecord]: + doi_records = DOIDataBase(db_filepath).select_latest_records({}) + return [r for r in doi_records if begin <= r.date_added < end or begin <= r.date_updated < end] + + +def get_email_content_template(template_filename: str = "email_weekly_roundup.jinja2"): + template_filepath = resource_filename(__name__, template_filename) + logging.info(f'Using template {template_filepath}') + with open(template_filepath, 'r') as infile: + template = jinja2.Template(infile.read()) + return template + + +def prepare_doi_record_for_template(record: DoiRecord) -> Dict[str, str]: + update_type = 'submitted' if record.date_added == record.date_updated else 'updated' + prepared_record = { + 'datacite_id': record.doi, + 'pds_id': record.identifier, + 'update_type': update_type, + 'last_modified': record.date_updated, + 'status': record.status.title() + } + + return prepared_record + + +def prepare_email_content(first_date: date, last_date: date, modified_doi_records: List[DoiRecord]) -> str: + template = get_email_content_template() + template_dict = { + 'first_date': first_date, + 'last_date': last_date, + 'doi_records': [prepare_doi_record_for_template(r) for r in modified_doi_records] + } + + full_content = template.render(template_dict) + logging.info(full_content) + return full_content + + +def prepare_email_message( + sender_email: str, receiver_email: str, + first_date: date, last_date: date, modified_doi_records: List[DoiRecord]) -> EmailMessage: + email_subject = f"DOI WEEKLY ROUNDUP: {first_date} through {last_date}" + + msg = EmailMessage() + msg["From"] = sender_email + msg["Subject"] = email_subject + msg["To"] = receiver_email + + email_content = prepare_email_content(first_date, last_date, modified_doi_records) + msg.set_content(email_content) + + return msg + + +def run(db_filepath: str, sender_email: str, receiver_email: str) -> None: + target_week_begin = get_start_of_local_week() - timedelta(days=7) + target_week_end = target_week_begin + timedelta(days=7, microseconds=-1) + last_date_of_week = (target_week_end - timedelta(microseconds=1)).date() + + modified_doi_records = fetch_dois_modified_between(target_week_begin, target_week_end, db_filepath) + + msg = prepare_email_message(sender_email, receiver_email, target_week_begin.date(), last_date_of_week, + modified_doi_records) + + emailer = PDSEmailer() + emailer.send_message(msg) + + +if __name__ == '__main__': + logging = get_logger('email_weekly_roundup') + config = DOIConfigUtil.get_config() + db_filepath = os.path.abspath(config['OTHER']['db_file']) + sender_email_address = config['OTHER']['emailer_sender'] + receiver_email_address = config['OTHER']['emailer_receivers'] + + run(db_filepath, sender_email_address, receiver_email_address) + + logging.info('Completed DOI weekly roundup email transmission') From c6ba671960aad3e8a692906ad1060d1691ed57d9 Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Tue, 13 Sep 2022 12:31:15 -0700 Subject: [PATCH 02/22] implement DoiRecord.to_json_dict() --- src/pds_doi_service/core/entities/doi.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/pds_doi_service/core/entities/doi.py b/src/pds_doi_service/core/entities/doi.py index d4848cc7..122eb27c 100644 --- a/src/pds_doi_service/core/entities/doi.py +++ b/src/pds_doi_service/core/entities/doi.py @@ -11,7 +11,8 @@ Contains the dataclass and enumeration definitions for Doi objects. """ -from dataclasses import dataclass +import json +from dataclasses import dataclass, asdict from dataclasses import field from datetime import datetime from enum import Enum @@ -149,3 +150,13 @@ class DoiRecord: doi: str transaction_key: str is_latest: bool + + def to_json_dict(self) -> str: + """Return a json-serializable dict equivalent to this DoiRecord""" + d = asdict(self) + for k, v in d.items(): + if type(v) is datetime: + d[k] = v.isoformat() + elif issubclass(type(v), Enum): + d[k] = v.title() + return json.dumps(d) \ No newline at end of file From 7690eace0520e8e1e545c5aca9b01053aeddc99f Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Tue, 13 Sep 2022 12:34:51 -0700 Subject: [PATCH 03/22] fix type hint --- src/pds_doi_service/core/util/emailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pds_doi_service/core/util/emailer.py b/src/pds_doi_service/core/util/emailer.py index 2201ba2f..b9c52f4d 100644 --- a/src/pds_doi_service/core/util/emailer.py +++ b/src/pds_doi_service/core/util/emailer.py @@ -70,7 +70,7 @@ def send_message(self, message): Parameters ---------- - message : email.message.EmailMessage + message : Union[email.message.Message, A message object with the 'From' and 'To:' and 'Subject' already filled in. From 18ad81a13155f9717a0e92a4f859df167be7642f Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Tue, 13 Sep 2022 12:35:48 -0700 Subject: [PATCH 04/22] add records json attachment to weekly roundup email --- scripts/email_weekly_roundup.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/scripts/email_weekly_roundup.py b/scripts/email_weekly_roundup.py index aca2812b..50aacfc9 100644 --- a/scripts/email_weekly_roundup.py +++ b/scripts/email_weekly_roundup.py @@ -1,6 +1,10 @@ +import json import os.path from datetime import datetime, timedelta, date -from email.message import EmailMessage +from email import encoders +from email.mime.base import MIMEBase +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText from typing import List, Dict import jinja2 @@ -59,18 +63,30 @@ def prepare_email_content(first_date: date, last_date: date, modified_doi_record return full_content +def attach_json_data(filename: str, doi_records: List[DoiRecord], msg: MIMEMultipart) -> None: + part = MIMEBase('application', "octet-stream") + data = json.dumps([r.to_json_dict() for r in doi_records]) + part.set_payload(data) + encoders.encode_base64(part) + part.add_header('Content-Disposition', + f'attachment; filename={filename}') + msg.attach(part) + + def prepare_email_message( sender_email: str, receiver_email: str, - first_date: date, last_date: date, modified_doi_records: List[DoiRecord]) -> EmailMessage: + first_date: date, last_date: date, modified_doi_records: List[DoiRecord]) -> MIMEMultipart: email_subject = f"DOI WEEKLY ROUNDUP: {first_date} through {last_date}" - msg = EmailMessage() + msg = MIMEMultipart() msg["From"] = sender_email msg["Subject"] = email_subject msg["To"] = receiver_email email_content = prepare_email_content(first_date, last_date, modified_doi_records) - msg.set_content(email_content) + msg.attach(MIMEText(email_content)) + attachment_filename = f'updated_dois_{first_date.isoformat()}_{last_date.isoformat()}.json' + attach_json_data(attachment_filename, modified_doi_records, msg) return msg From 93d155a689eeb2c7135c4b7677e9082504e0c9c9 Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Tue, 13 Sep 2022 14:01:44 -0700 Subject: [PATCH 05/22] fix email MIMEText type --- scripts/email_weekly_roundup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/email_weekly_roundup.py b/scripts/email_weekly_roundup.py index 50aacfc9..c4ed8eaa 100644 --- a/scripts/email_weekly_roundup.py +++ b/scripts/email_weekly_roundup.py @@ -84,7 +84,7 @@ def prepare_email_message( msg["To"] = receiver_email email_content = prepare_email_content(first_date, last_date, modified_doi_records) - msg.attach(MIMEText(email_content)) + msg.attach(MIMEText(email_content, 'html')) attachment_filename = f'updated_dois_{first_date.isoformat()}_{last_date.isoformat()}.json' attach_json_data(attachment_filename, modified_doi_records, msg) From 79b65716bbb66cda40d9c852e12228d15ca7a5d8 Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Tue, 13 Sep 2022 14:01:57 -0700 Subject: [PATCH 06/22] update email template --- scripts/email_weekly_roundup.jinja2 | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/scripts/email_weekly_roundup.jinja2 b/scripts/email_weekly_roundup.jinja2 index 01527f07..8e81fb57 100644 --- a/scripts/email_weekly_roundup.jinja2 +++ b/scripts/email_weekly_roundup.jinja2 @@ -1,8 +1,13 @@

DataCite DOI Weekly Roundup

-

The following DOIs were reserved, updated or released during the week {{ first_date.strftime('%m/%d') }} through {{ last_date.strftime('%m/%d') }}:

-
    - {% for doi in doi_records %} -
  • {{ doi.datacite_id }} {{ doi.pds_id }} ({{ doi.status }}) {{ doi.update_type }} {{ doi.last_modified.strftime('%Y-%m-%d') }}
  • - {% endfor %} -
+{% if doi_records %} +

The following DOIs were reserved, updated or released during the week {{ first_date.strftime('%m/%d') }} through {{ last_date.strftime('%m/%d') }}:

+
    + {% for doi in doi_records %} +
  • {{ doi.datacite_id }} {{ doi.pds_id }} ({{ doi.status }}) {{ doi.update_type }} {{ doi.last_modified.strftime('%Y-%m-%d') }}
  • + {% endfor %} +
+{% else %} +

No DOI records were reserved, updated or released modified during the week {{ first_date.strftime('%m/%d') }} through {{ last_date.strftime('%m/%d') }}.

+{% endif %} +

Contact Us

From b1f22859d373a4b0e1649b8b8d6008ad8da254f4 Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Tue, 13 Sep 2022 14:58:52 -0700 Subject: [PATCH 07/22] extract email test stubs to module --- .../core/actions/test/check_test.py | 48 ++------------ .../core/actions/test/util/__init__.py | 0 .../core/actions/test/util/email.py | 65 +++++++++++++++++++ 3 files changed, 69 insertions(+), 44 deletions(-) create mode 100644 src/pds_doi_service/core/actions/test/util/__init__.py create mode 100644 src/pds_doi_service/core/actions/test/util/email.py diff --git a/src/pds_doi_service/core/actions/test/check_test.py b/src/pds_doi_service/core/actions/test/check_test.py index 945cfd7f..721cc92e 100644 --- a/src/pds_doi_service/core/actions/test/check_test.py +++ b/src/pds_doi_service/core/actions/test/check_test.py @@ -16,6 +16,8 @@ import pds_doi_service.core.outputs.datacite.datacite_web_client import pds_doi_service.core.outputs.osti.osti_web_client from pds_doi_service.core.actions import DOICoreActionCheck +from pds_doi_service.core.actions.test.util.email import capture_email +from pds_doi_service.core.actions.test.util.email import get_local_smtp_patched_config from pds_doi_service.core.db.doi_database import DOIDataBase from pds_doi_service.core.entities.doi import DoiRecord from pds_doi_service.core.entities.doi import DoiStatus @@ -188,26 +190,7 @@ def test_check_for_pending_entries_w_no_change(self): self.assertEqual(pending_record["doi"], "10.17189/29348") self.assertEqual(pending_record["identifier"], "urn:nasa:pds:lab_shocked_feldspars::1.0") - def get_config_patch(self): - """ - Return a modified default config that points to a local test smtp - server for use with the email test - """ - parser = configparser.ConfigParser() - - # default configuration - conf_default = "conf.ini.default" - conf_default_path = abspath(join(dirname(__file__), os.pardir, os.pardir, "util", conf_default)) - - parser.read(conf_default_path) - parser["OTHER"]["emailer_local_host"] = "localhost" - parser["OTHER"]["emailer_port"] = "1025" - - parser = DOIConfigUtil._resolve_relative_path(parser) - - return parser - - @patch.object(pds_doi_service.core.util.config_parser.DOIConfigUtil, "get_config", get_config_patch) + @patch.object(pds_doi_service.core.util.config_parser.DOIConfigUtil, "get_config", get_local_smtp_patched_config) @patch.object( pds_doi_service.core.outputs.osti.osti_web_client.DOIOstiWebClient, "query_doi", webclient_query_patch_nominal ) @@ -221,30 +204,7 @@ def test_email_receipt(self): # Create a new check action so our patched config is pulled in action = DOICoreActionCheck(self.db_name) - with tempfile.TemporaryFile() as temp_file: - # Stand up a subprocess running a debug smtpd server - # By default, all this server is does is echo email payloads to - # standard out, so provide a temp file to capture it - debug_email_proc = subprocess.Popen( - ["python", "-u", "-m", "smtpd", "-n", "-c", "DebuggingServer", "localhost:1025"], stdout=temp_file - ) - - # Give the debug smtp server a chance to start listening - time.sleep(1) - - try: - # Run the check action and have it send an email w/ attachment - action.run(email=True, attachment=True, submitter="email-test@email.com") - - # Read the raw email contents (payload) from the subprocess - # into a string - temp_file.seek(0) - email_contents = temp_file.read() - message = message_from_bytes(email_contents).get_payload() - finally: - # Send the debug smtp server a ctrl+C and wait for it to stop - os.kill(debug_email_proc.pid, signal.SIGINT) - debug_email_proc.wait() + message = capture_email(lambda: action.run(email=True, attachment=True, submitter="email-test@email.com")) # Run some string searches on the email body to ensure what we expect # made it in diff --git a/src/pds_doi_service/core/actions/test/util/__init__.py b/src/pds_doi_service/core/actions/test/util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/pds_doi_service/core/actions/test/util/email.py b/src/pds_doi_service/core/actions/test/util/email.py new file mode 100644 index 00000000..cb64db7d --- /dev/null +++ b/src/pds_doi_service/core/actions/test/util/email.py @@ -0,0 +1,65 @@ +import configparser +import os +import signal +import subprocess +import tempfile +import time +from email import message_from_bytes +from email.message import Message +from typing import Callable + +from pds_doi_service.core.util.config_parser import DOIConfigUtil + + +def get_local_smtp_patched_config(self): + """ + Return a modified default config that points to a local test smtp + server for use with the email test + """ + parser = configparser.ConfigParser() + + # default configuration + conf_default = "conf.ini.default" + conf_default_path = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir, "util", conf_default) + ) + + parser.read(conf_default_path) + parser["OTHER"]["emailer_local_host"] = "localhost" + parser["OTHER"]["emailer_port"] = "1025" + + parser = DOIConfigUtil._resolve_relative_path(parser) + + return parser + + +def capture_email(f: Callable[[], None], port: int = 1025) -> Message: + """ + Stand up a transient smtpd server, capture the first message sent through it, and return that message + :param f: a function which sends an email to the SMTP server + :param port: the port on which the sending process attempts to connect to the SMTP server + """ + with tempfile.TemporaryFile() as temp_file: + # By default, all this server is does is echo email payloads to + # standard out, so provide a temp file to capture it + debug_email_proc = subprocess.Popen( + ["python", "-u", "-m", "smtpd", "-n", "-c", "DebuggingServer", f"localhost:{port}"], stdout=temp_file + ) + + # Give the debug smtp server a chance to start listening + time.sleep(1) + + try: + # Run the check action and have it send an email w/ attachment + f() + # Read the raw email contents (payload) from the subprocess + # into a string + temp_file.seek(0) + email_contents = temp_file.read() + message = message_from_bytes(email_contents).get_payload() + finally: + # Send the debug smtp server a ctrl+C and wait for it to stop + os.kill(debug_email_proc.pid, signal.SIGINT) + debug_email_proc.wait() + + return message From 61664acfab65baa27bd70b62a61fe3160048b0ab Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Tue, 13 Sep 2022 15:00:08 -0700 Subject: [PATCH 08/22] lint --- .../api/controllers/authentication.py | 22 +++++++++---------- src/pds_doi_service/core/entities/doi.py | 5 +++-- .../core/outputs/schemaentities/rights.py | 3 +-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/pds_doi_service/api/controllers/authentication.py b/src/pds_doi_service/api/controllers/authentication.py index 7f874319..98f6068a 100644 --- a/src/pds_doi_service/api/controllers/authentication.py +++ b/src/pds_doi_service/api/controllers/authentication.py @@ -1,16 +1,19 @@ -import time import logging -from jose import JWTError, jwt +import time + from flask import current_app -from werkzeug.exceptions import Unauthorized +from jose import jwt +from jose import JWTError from pds_doi_service.core.util.config_parser import DOIConfigUtil +from werkzeug.exceptions import Unauthorized config = DOIConfigUtil().get_config() -JWT_ISSUER = config.get('API_AUTHENTICATION', 'jwt_issuer') -JSON_WEB_KEY_SET = config.get('API_AUTHENTICATION', 'json_web_key_set') -JWT_LIFETIME_SECONDS = config.get('API_AUTHENTICATION', 'jwt_lifetime_seconds') -JWT_ALGORITHM = config.get('API_AUTHENTICATION', 'jwt_algorithm') +JWT_ISSUER = config.get("API_AUTHENTICATION", "jwt_issuer") +JSON_WEB_KEY_SET = config.get("API_AUTHENTICATION", "json_web_key_set") +JWT_LIFETIME_SECONDS = config.get("API_AUTHENTICATION", "jwt_lifetime_seconds") +JWT_ALGORITHM = config.get("API_AUTHENTICATION", "jwt_algorithm") + def decode_token(token): try: @@ -20,10 +23,7 @@ def decode_token(token): JSON_WEB_KEY_SET, algorithms=[JWT_ALGORITHM], issuer=JWT_ISSUER, - options={ - 'verify_signature': True, - 'verify_iss': True - } + options={"verify_signature": True, "verify_iss": True}, ) except JWTError as e: current_app.logger.error("authentication exception") diff --git a/src/pds_doi_service/core/entities/doi.py b/src/pds_doi_service/core/entities/doi.py index 122eb27c..fa0f15ae 100644 --- a/src/pds_doi_service/core/entities/doi.py +++ b/src/pds_doi_service/core/entities/doi.py @@ -12,7 +12,8 @@ Contains the dataclass and enumeration definitions for Doi objects. """ import json -from dataclasses import dataclass, asdict +from dataclasses import asdict +from dataclasses import dataclass from dataclasses import field from datetime import datetime from enum import Enum @@ -159,4 +160,4 @@ def to_json_dict(self) -> str: d[k] = v.isoformat() elif issubclass(type(v), Enum): d[k] = v.title() - return json.dumps(d) \ No newline at end of file + return json.dumps(d) diff --git a/src/pds_doi_service/core/outputs/schemaentities/rights.py b/src/pds_doi_service/core/outputs/schemaentities/rights.py index d1a3031a..a89ec4ac 100644 --- a/src/pds_doi_service/core/outputs/schemaentities/rights.py +++ b/src/pds_doi_service/core/outputs/schemaentities/rights.py @@ -1,4 +1,3 @@ -from abc import ABC from dataclasses import asdict from dataclasses import dataclass from typing import Dict @@ -79,4 +78,4 @@ def from_endpoint_data(cls, data: Dict[str, str]): identifier_scheme="SPDX", identifier="CC0-1.0", uri="http://creativecommons.org/publicdomain/zero/1.0/", -) \ No newline at end of file +) From 5bf29b215cb0839fe237f4f768b33882d57db29f Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Tue, 13 Sep 2022 18:09:52 -0700 Subject: [PATCH 09/22] fix DoiRecord.to_json_dict() --- src/pds_doi_service/core/entities/doi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pds_doi_service/core/entities/doi.py b/src/pds_doi_service/core/entities/doi.py index fa0f15ae..7ee2768d 100644 --- a/src/pds_doi_service/core/entities/doi.py +++ b/src/pds_doi_service/core/entities/doi.py @@ -18,7 +18,7 @@ from datetime import datetime from enum import Enum from enum import unique -from typing import List +from typing import List, Dict from typing import Optional from pds_doi_service.core.outputs.schemaentities.rights import CC0_LICENSE @@ -152,7 +152,7 @@ class DoiRecord: transaction_key: str is_latest: bool - def to_json_dict(self) -> str: + def to_json_dict(self) -> Dict: """Return a json-serializable dict equivalent to this DoiRecord""" d = asdict(self) for k, v in d.items(): @@ -160,4 +160,4 @@ def to_json_dict(self) -> str: d[k] = v.isoformat() elif issubclass(type(v), Enum): d[k] = v.title() - return json.dumps(d) + return d From fc927d85489ae18f0dfde5fdd2f3aab0ea81b4db Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Tue, 13 Sep 2022 15:01:40 -0700 Subject: [PATCH 10/22] move weekly roundup implementation from script into package to facilitate tests --- scripts/email_weekly_roundup.py | 105 +---------------- src/pds_doi_service/core/actions/roundup.py | 106 ++++++++++++++++++ .../templates}/email_weekly_roundup.jinja2 | 0 3 files changed, 108 insertions(+), 103 deletions(-) create mode 100644 src/pds_doi_service/core/actions/roundup.py rename {scripts => src/pds_doi_service/core/actions/templates}/email_weekly_roundup.jinja2 (100%) diff --git a/scripts/email_weekly_roundup.py b/scripts/email_weekly_roundup.py index c4ed8eaa..86028b2c 100644 --- a/scripts/email_weekly_roundup.py +++ b/scripts/email_weekly_roundup.py @@ -1,110 +1,9 @@ -import json import os.path -from datetime import datetime, timedelta, date -from email import encoders -from email.mime.base import MIMEBase -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -from typing import List, Dict +from pds_doi_service.core.actions.roundup import run as run_weekly_roundup -import jinja2 -from pkg_resources import resource_filename - -from pds_doi_service.core.db.doi_database import DOIDataBase -from pds_doi_service.core.entities.doi import DoiRecord from pds_doi_service.core.util.config_parser import DOIConfigUtil -from pds_doi_service.core.util.emailer import Emailer as PDSEmailer from pds_doi_service.core.util.general_util import get_logger - -def get_start_of_local_week() -> datetime: - """Return the start of the local-timezones week as a tz-aware datetime""" - today = datetime.now().date() - start_of_today = datetime(today.year, today.month, today.day) - return start_of_today.astimezone() - timedelta(days=today.weekday()) - - -def fetch_dois_modified_between(begin: datetime, end: datetime, db_filepath) -> List[DoiRecord]: - doi_records = DOIDataBase(db_filepath).select_latest_records({}) - return [r for r in doi_records if begin <= r.date_added < end or begin <= r.date_updated < end] - - -def get_email_content_template(template_filename: str = "email_weekly_roundup.jinja2"): - template_filepath = resource_filename(__name__, template_filename) - logging.info(f'Using template {template_filepath}') - with open(template_filepath, 'r') as infile: - template = jinja2.Template(infile.read()) - return template - - -def prepare_doi_record_for_template(record: DoiRecord) -> Dict[str, str]: - update_type = 'submitted' if record.date_added == record.date_updated else 'updated' - prepared_record = { - 'datacite_id': record.doi, - 'pds_id': record.identifier, - 'update_type': update_type, - 'last_modified': record.date_updated, - 'status': record.status.title() - } - - return prepared_record - - -def prepare_email_content(first_date: date, last_date: date, modified_doi_records: List[DoiRecord]) -> str: - template = get_email_content_template() - template_dict = { - 'first_date': first_date, - 'last_date': last_date, - 'doi_records': [prepare_doi_record_for_template(r) for r in modified_doi_records] - } - - full_content = template.render(template_dict) - logging.info(full_content) - return full_content - - -def attach_json_data(filename: str, doi_records: List[DoiRecord], msg: MIMEMultipart) -> None: - part = MIMEBase('application', "octet-stream") - data = json.dumps([r.to_json_dict() for r in doi_records]) - part.set_payload(data) - encoders.encode_base64(part) - part.add_header('Content-Disposition', - f'attachment; filename={filename}') - msg.attach(part) - - -def prepare_email_message( - sender_email: str, receiver_email: str, - first_date: date, last_date: date, modified_doi_records: List[DoiRecord]) -> MIMEMultipart: - email_subject = f"DOI WEEKLY ROUNDUP: {first_date} through {last_date}" - - msg = MIMEMultipart() - msg["From"] = sender_email - msg["Subject"] = email_subject - msg["To"] = receiver_email - - email_content = prepare_email_content(first_date, last_date, modified_doi_records) - msg.attach(MIMEText(email_content, 'html')) - attachment_filename = f'updated_dois_{first_date.isoformat()}_{last_date.isoformat()}.json' - attach_json_data(attachment_filename, modified_doi_records, msg) - - return msg - - -def run(db_filepath: str, sender_email: str, receiver_email: str) -> None: - target_week_begin = get_start_of_local_week() - timedelta(days=7) - target_week_end = target_week_begin + timedelta(days=7, microseconds=-1) - last_date_of_week = (target_week_end - timedelta(microseconds=1)).date() - - modified_doi_records = fetch_dois_modified_between(target_week_begin, target_week_end, db_filepath) - - msg = prepare_email_message(sender_email, receiver_email, target_week_begin.date(), last_date_of_week, - modified_doi_records) - - emailer = PDSEmailer() - emailer.send_message(msg) - - if __name__ == '__main__': logging = get_logger('email_weekly_roundup') config = DOIConfigUtil.get_config() @@ -112,6 +11,6 @@ def run(db_filepath: str, sender_email: str, receiver_email: str) -> None: sender_email_address = config['OTHER']['emailer_sender'] receiver_email_address = config['OTHER']['emailer_receivers'] - run(db_filepath, sender_email_address, receiver_email_address) + run_weekly_roundup(db_filepath, sender_email_address, receiver_email_address) logging.info('Completed DOI weekly roundup email transmission') diff --git a/src/pds_doi_service/core/actions/roundup.py b/src/pds_doi_service/core/actions/roundup.py new file mode 100644 index 00000000..60ce1501 --- /dev/null +++ b/src/pds_doi_service/core/actions/roundup.py @@ -0,0 +1,106 @@ +import json +import logging +import os +from datetime import date +from datetime import datetime +from datetime import timedelta +from email import encoders +from email.mime.base import MIMEBase +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from typing import Dict +from typing import List + +import jinja2 +from pds_doi_service.core.db.doi_database import DOIDataBase +from pds_doi_service.core.entities.doi import DoiRecord +from pds_doi_service.core.util.emailer import Emailer as PDSEmailer +from pkg_resources import resource_filename + + +def get_start_of_local_week() -> datetime: + """Return the start of the local-timezones week as a tz-aware datetime""" + today = datetime.now().date() + start_of_today = datetime(today.year, today.month, today.day) + return start_of_today.astimezone() - timedelta(days=today.weekday()) + + +def fetch_dois_modified_between(begin: datetime, end: datetime, db_filepath) -> List[DoiRecord]: + doi_records = DOIDataBase(db_filepath).select_latest_records({}) + return [r for r in doi_records if begin <= r.date_added < end or begin <= r.date_updated < end] + + +def get_email_content_template(template_filename: str = "email_weekly_roundup.jinja2"): + template_filepath = resource_filename(__name__, os.path.join("templates", template_filename)) + logging.info(f"Using template {template_filepath}") + with open(template_filepath, "r") as infile: + template = jinja2.Template(infile.read()) + return template + + +def prepare_doi_record_for_template(record: DoiRecord) -> Dict[str, str]: + update_type = "submitted" if record.date_added == record.date_updated else "updated" + prepared_record = { + "datacite_id": record.doi, + "pds_id": record.identifier, + "update_type": update_type, + "last_modified": record.date_updated, + "status": record.status.title(), + } + + return prepared_record + + +def prepare_email_content(first_date: date, last_date: date, modified_doi_records: List[DoiRecord]) -> str: + template = get_email_content_template() + template_dict = { + "first_date": first_date, + "last_date": last_date, + "doi_records": [prepare_doi_record_for_template(r) for r in modified_doi_records], + } + + full_content = template.render(template_dict) + logging.info(full_content) + return full_content + + +def attach_json_data(filename: str, doi_records: List[DoiRecord], msg: MIMEMultipart) -> None: + data = json.dumps([r.to_json_dict() for r in doi_records]) + part = MIMEText(data, "plain", "utf-8") + part.set_charset("utf-8") + encoders.encode_base64(part) + part.add_header("Content-Disposition", f"attachment; filename={filename}") + msg.attach(part) + + +def prepare_email_message( + sender_email: str, receiver_email: str, first_date: date, last_date: date, modified_doi_records: List[DoiRecord] +) -> MIMEMultipart: + email_subject = f"DOI WEEKLY ROUNDUP: {first_date} through {last_date}" + + msg = MIMEMultipart() + msg["From"] = sender_email + msg["Subject"] = email_subject + msg["To"] = receiver_email + + email_content = prepare_email_content(first_date, last_date, modified_doi_records) + msg.attach(MIMEText(email_content, "html")) + attachment_filename = f"updated_dois_{first_date.isoformat()}_{last_date.isoformat()}.json" + attach_json_data(attachment_filename, modified_doi_records, msg) + + return msg + + +def run(db_filepath: str, sender_email: str, receiver_email: str) -> None: + target_week_begin = get_start_of_local_week() - timedelta(days=7) + target_week_end = target_week_begin + timedelta(days=7, microseconds=-1) + last_date_of_week = (target_week_end - timedelta(microseconds=1)).date() + + modified_doi_records = fetch_dois_modified_between(target_week_begin, target_week_end, db_filepath) + + msg = prepare_email_message( + sender_email, receiver_email, target_week_begin.date(), last_date_of_week, modified_doi_records + ) + + emailer = PDSEmailer() + emailer.send_message(msg) diff --git a/scripts/email_weekly_roundup.jinja2 b/src/pds_doi_service/core/actions/templates/email_weekly_roundup.jinja2 similarity index 100% rename from scripts/email_weekly_roundup.jinja2 rename to src/pds_doi_service/core/actions/templates/email_weekly_roundup.jinja2 From 2ceac29343c6a39b867d38e4ff6d716430135ecd Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Tue, 13 Sep 2022 15:40:47 -0700 Subject: [PATCH 11/22] modify roundup.run() to take DOIDataBase instead of db filepath --- scripts/email_weekly_roundup.py | 4 +++- src/pds_doi_service/core/actions/roundup.py | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/email_weekly_roundup.py b/scripts/email_weekly_roundup.py index 86028b2c..d91fae5d 100644 --- a/scripts/email_weekly_roundup.py +++ b/scripts/email_weekly_roundup.py @@ -1,5 +1,6 @@ import os.path from pds_doi_service.core.actions.roundup import run as run_weekly_roundup +from pds_doi_service.core.db.doi_database import DOIDataBase from pds_doi_service.core.util.config_parser import DOIConfigUtil from pds_doi_service.core.util.general_util import get_logger @@ -10,7 +11,8 @@ db_filepath = os.path.abspath(config['OTHER']['db_file']) sender_email_address = config['OTHER']['emailer_sender'] receiver_email_address = config['OTHER']['emailer_receivers'] + db = DOIDataBase(db_filepath) - run_weekly_roundup(db_filepath, sender_email_address, receiver_email_address) + run_weekly_roundup(db, sender_email_address, receiver_email_address) logging.info('Completed DOI weekly roundup email transmission') diff --git a/src/pds_doi_service/core/actions/roundup.py b/src/pds_doi_service/core/actions/roundup.py index 60ce1501..1599fd0b 100644 --- a/src/pds_doi_service/core/actions/roundup.py +++ b/src/pds_doi_service/core/actions/roundup.py @@ -25,8 +25,8 @@ def get_start_of_local_week() -> datetime: return start_of_today.astimezone() - timedelta(days=today.weekday()) -def fetch_dois_modified_between(begin: datetime, end: datetime, db_filepath) -> List[DoiRecord]: - doi_records = DOIDataBase(db_filepath).select_latest_records({}) +def fetch_dois_modified_between(begin: datetime, end: datetime, database: DOIDataBase) -> List[DoiRecord]: + doi_records = database.select_latest_records({}) return [r for r in doi_records if begin <= r.date_added < end or begin <= r.date_updated < end] @@ -91,12 +91,12 @@ def prepare_email_message( return msg -def run(db_filepath: str, sender_email: str, receiver_email: str) -> None: +def run(database: DOIDataBase, sender_email: str, receiver_email: str) -> None: target_week_begin = get_start_of_local_week() - timedelta(days=7) target_week_end = target_week_begin + timedelta(days=7, microseconds=-1) last_date_of_week = (target_week_end - timedelta(microseconds=1)).date() - modified_doi_records = fetch_dois_modified_between(target_week_begin, target_week_end, db_filepath) + modified_doi_records = fetch_dois_modified_between(target_week_begin, target_week_end, database) msg = prepare_email_message( sender_email, receiver_email, target_week_begin.date(), last_date_of_week, modified_doi_records From 6463c5443917ee538af89a0a326fe3c2e018f2e9 Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Tue, 13 Sep 2022 15:45:36 -0700 Subject: [PATCH 12/22] move loose template files to new templates directory --- src/pds_doi_service/core/actions/check.py | 10 +++++++--- .../actions/{ => templates}/email_template_body.txt | 0 .../actions/{ => templates}/email_template_header.txt | 0 3 files changed, 7 insertions(+), 3 deletions(-) rename src/pds_doi_service/core/actions/{ => templates}/email_template_body.txt (100%) rename src/pds_doi_service/core/actions/{ => templates}/email_template_header.txt (100%) diff --git a/src/pds_doi_service/core/actions/check.py b/src/pds_doi_service/core/actions/check.py index cfe49995..2d6487ce 100644 --- a/src/pds_doi_service/core/actions/check.py +++ b/src/pds_doi_service/core/actions/check.py @@ -12,6 +12,7 @@ Contains the definition for the Check action of the Core PDS DOI Service. """ import json +import os from copy import deepcopy from datetime import date from datetime import datetime @@ -54,9 +55,12 @@ def __init__(self, db_name=None): self._email = True self._attachment = True - self.email_header_template_file = resource_filename(__name__, "email_template_header.txt") - - self.email_body_template_file = resource_filename(__name__, "email_template_body.txt") + self.email_header_template_file = resource_filename( + __name__, os.path.join("templates", "email_template_header.txt") + ) + self.email_body_template_file = resource_filename( + __name__, os.path.join("templates", "email_template_body.txt") + ) # Make sure templates are where we expect them to be if not exists(self.email_header_template_file) or not exists(self.email_body_template_file): diff --git a/src/pds_doi_service/core/actions/email_template_body.txt b/src/pds_doi_service/core/actions/templates/email_template_body.txt similarity index 100% rename from src/pds_doi_service/core/actions/email_template_body.txt rename to src/pds_doi_service/core/actions/templates/email_template_body.txt diff --git a/src/pds_doi_service/core/actions/email_template_header.txt b/src/pds_doi_service/core/actions/templates/email_template_header.txt similarity index 100% rename from src/pds_doi_service/core/actions/email_template_header.txt rename to src/pds_doi_service/core/actions/templates/email_template_header.txt From 65d0477757522541427ff23d466b882372c3ae24 Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Tue, 13 Sep 2022 16:34:41 -0700 Subject: [PATCH 13/22] alter capture_email() to return Message instead of str --- .../core/actions/test/check_test.py | 15 ++++++++------- .../core/actions/test/util/email.py | 17 +++++++++++------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/pds_doi_service/core/actions/test/check_test.py b/src/pds_doi_service/core/actions/test/check_test.py index 721cc92e..41fa659c 100644 --- a/src/pds_doi_service/core/actions/test/check_test.py +++ b/src/pds_doi_service/core/actions/test/check_test.py @@ -205,18 +205,19 @@ def test_email_receipt(self): action = DOICoreActionCheck(self.db_name) message = capture_email(lambda: action.run(email=True, attachment=True, submitter="email-test@email.com")) + html_part, attachment_part = message.get_payload() + attachment = attachment_part.get_payload()[0].get_payload()[0] - # Run some string searches on the email body to ensure what we expect - # made it in - - # Email address provided to check action should be present - self.assertIn("email-test@email.com", message) + # Email address provided to check action should be present in recipients + recipients = message["To"].strip().split(", ") + self.assertIn("email-test@email.com", recipients) # Subject line should be present - self.assertIn("DOI Submission Status Report For Node", message) + self.assertTrue(message["Subject"].strip().startswith("DOI Submission Status Report For Node")) # Attachment should also be provided - self.assertIn("Content-Disposition: attachment; filename=doi_status_", message) + self.assertTrue(attachment["Content-Disposition"].startswith("attachment; filename=doi_status_")) + self.assertTrue(attachment["Content-Disposition"].endswith(".json")) if __name__ == "__main__": diff --git a/src/pds_doi_service/core/actions/test/util/email.py b/src/pds_doi_service/core/actions/test/util/email.py index cb64db7d..fa5c3b49 100644 --- a/src/pds_doi_service/core/actions/test/util/email.py +++ b/src/pds_doi_service/core/actions/test/util/email.py @@ -4,8 +4,8 @@ import subprocess import tempfile import time -from email import message_from_bytes from email.message import Message +from email.parser import Parser from typing import Callable from pds_doi_service.core.util.config_parser import DOIConfigUtil @@ -52,14 +52,19 @@ def capture_email(f: Callable[[], None], port: int = 1025) -> Message: try: # Run the check action and have it send an email w/ attachment f() - # Read the raw email contents (payload) from the subprocess - # into a string + + # Isolate the payload from the SMTP subprocess stdout and parse it temp_file.seek(0) - email_contents = temp_file.read() - message = message_from_bytes(email_contents).get_payload() + message_lines = temp_file.readlines()[1:-1] # strip the leading/trailing server messages + cleaned_message_lines = [ + bytes.decode(l)[2:-2] for l in message_lines + ] # strip the leading "b'" and trailing "'" from each line + email_contents = "\n".join(cleaned_message_lines) + + message = Parser().parsestr(email_contents) finally: # Send the debug smtp server a ctrl+C and wait for it to stop os.kill(debug_email_proc.pid, signal.SIGINT) debug_email_proc.wait() - return message + return message From 9e150d3fa3e17f38c12171e934a46ecfd8f85cb1 Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Tue, 13 Sep 2022 15:01:47 -0700 Subject: [PATCH 14/22] implement roundup_test.py --- .../roundup/roundup_email_attachment.json | 30 ++ .../test/data/roundup/roundup_email_body.html | 13 + .../core/actions/test/roundup_test.py | 323 ++++++++++++++++++ 3 files changed, 366 insertions(+) create mode 100644 src/pds_doi_service/core/actions/test/data/roundup/roundup_email_attachment.json create mode 100644 src/pds_doi_service/core/actions/test/data/roundup/roundup_email_body.html create mode 100644 src/pds_doi_service/core/actions/test/roundup_test.py diff --git a/src/pds_doi_service/core/actions/test/data/roundup/roundup_email_attachment.json b/src/pds_doi_service/core/actions/test/data/roundup/roundup_email_attachment.json new file mode 100644 index 00000000..64b5c3bf --- /dev/null +++ b/src/pds_doi_service/core/actions/test/data/roundup/roundup_email_attachment.json @@ -0,0 +1,30 @@ +[ + { + "identifier": "urn:nasa:pds:product_11111::1.0", + "status": "Pending", + "date_added": "2022-09-09T17:44:30.165920+00:00", + "date_updated": "2022-09-09T17:44:30.165920+00:00", + "submitter": "img-submitter@jpl.nasa.gov", + "title": "Laboratory Shocked Feldspars Bundle", + "type": "Collection", + "subtype": "PDS4 Collection", + "node_id": "img", + "doi": "10.17189/11111", + "transaction_key": "./transaction_history/img/2020-06-15T18:42:45.653317", + "is_latest": true + }, + { + "identifier": "urn:nasa:pds:product_22222::1.0", + "status": "Pending", + "date_added": "2022-08-14T17:44:30.165940+00:00", + "date_updated": "2022-09-09T17:44:30.165940+00:00", + "submitter": "img-submitter@jpl.nasa.gov", + "title": "Laboratory Shocked Feldspars Bundle", + "type": "Collection", + "subtype": "PDS4 Collection", + "node_id": "img", + "doi": "10.17189/22222", + "transaction_key": "./transaction_history/img/2020-06-15T18:42:45.653317", + "is_latest": true + } +] diff --git a/src/pds_doi_service/core/actions/test/data/roundup/roundup_email_body.html b/src/pds_doi_service/core/actions/test/data/roundup/roundup_email_body.html new file mode 100644 index 00000000..985ebbdf --- /dev/null +++ b/src/pds_doi_service/core/actions/test/data/roundup/roundup_email_body.html @@ -0,0 +1,13 @@ +

DataCite DOI Weekly Roundup

+ +

The following DOIs were reserved, updated or released during the week 09/05 through 09/11:

+
    + +
  • 10.17189/11111 urn:nasa:pds:product_11111::1.0 (Pending) submitted 2022-09-09
  • + +
  • 10.17189/22222 urn:nasa:pds:product_22222::1.0 (Pending) updated 2022-09-09
  • + +
+ + +

Contact Us

diff --git a/src/pds_doi_service/core/actions/test/roundup_test.py b/src/pds_doi_service/core/actions/test/roundup_test.py new file mode 100644 index 00000000..e20bd4e1 --- /dev/null +++ b/src/pds_doi_service/core/actions/test/roundup_test.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python +import base64 +import configparser +import json +import os +import shutil +import signal +import subprocess +import tempfile +import time +import unittest +import uuid +from datetime import datetime +from datetime import timedelta +from email import message_from_bytes +from email.message import Message +from email.parser import BytesParser +from unittest.mock import patch + +import pds_doi_service.core.outputs.datacite.datacite_web_client +import pds_doi_service.core.outputs.osti.osti_web_client +from pds_doi_service.core.actions import DOICoreActionCheck +from pds_doi_service.core.actions.roundup import run as do_roundup +from pds_doi_service.core.actions.test.util.email import capture_email +from pds_doi_service.core.db.doi_database import DOIDataBase +from pds_doi_service.core.entities.doi import DoiRecord +from pds_doi_service.core.entities.doi import DoiStatus +from pds_doi_service.core.entities.doi import ProductType +from pds_doi_service.core.outputs.doi_record import CONTENT_TYPE_XML +from pds_doi_service.core.outputs.service import DOIServiceFactory +from pds_doi_service.core.outputs.service import SERVICE_TYPE_DATACITE +from pds_doi_service.core.outputs.service import SERVICE_TYPE_OSTI +from pds_doi_service.core.util.config_parser import DOIConfigUtil +from pkg_resources import resource_filename + + +class WeeklyRoundupEmailNotificationTestCase(unittest.TestCase): + tests_dir = os.path.abspath(resource_filename(__name__, "")) + resources_dir = os.path.join(tests_dir, "data", "roundup") + + temp_dir = tempfile.mkdtemp() + db_filepath = os.path.join(temp_dir, "doi_temp.sqlite") + + _database_obj: DOIDataBase + _message: Message + + sender = "test_sender@jpl.nasa.gov" + recipient = "test_recipient@jpl.nasa.gov" + + @classmethod + def setUpClass(cls): + cls._database_obj = DOIDataBase(cls.db_filepath) + + # Write some example DOIs to the test db + doi_records = [ + cls.generate_doi_record(uid="11111", added_last_week=True, updated_last_week=True), + cls.generate_doi_record(uid="22222", added_last_week=False, updated_last_week=True), + cls.generate_doi_record(uid="33333", added_last_week=False, updated_last_week=False), + ] + + for record in doi_records: + cls._database_obj.write_doi_info_to_database(record) + + cls._message = capture_email(lambda: do_roundup(cls._database_obj, cls.sender, cls.recipient)) + + @classmethod + def tearDownClass(cls): + cls._database_obj.close_database() + shutil.rmtree(cls.temp_dir, ignore_errors=True) + + def test_roundup_email_sender_correct(self): + self.assertEqual(self.sender, self._message["From"]) + + def test_roundup_email_recipients_correct(self): + self.assertEqual(self.recipient, self._message["To"]) + + def test_html_content(self): + html_content = self._message.get_payload(0).get_payload() + expected_content_filepath = os.path.join(self.resources_dir, "roundup_email_body.html") + with open(expected_content_filepath, "r") as infile: + expected_html_content = infile.read() + self.assertEqual(expected_html_content.replace(" ", ""), html_content.replace(" ", "")) + + def test_attachment_content(self): + attachment_content = self._message.get_payload(1).get_payload() + attachment_data = json.loads(base64.b64decode(attachment_content)) + expected_content_filepath = os.path.join(self.resources_dir, "roundup_email_attachment.json") + with open(expected_content_filepath, "r") as infile: + expected_data = json.load(infile) + + # Remove fields whose values are non-deterministic after confirming that they exist + for k in ["date_added", "date_updated"]: + for results in [attachment_data, expected_data]: + for r in results: + self.assertIn(k, r.keys()) + r.pop(k) + + self.assertEqual(expected_data, attachment_data) + + @staticmethod + def generate_doi_record(uid: str, added_last_week: bool, updated_last_week: bool): + if added_last_week: + assert updated_last_week + now = datetime.now() + last_week = now - timedelta(days=4) + ages_ago = now - timedelta(days=30) + + pds_id = f"urn:nasa:pds:product_{uid}::1.0" + doi_id = f"10.17189/{uid}" + + return DoiRecord( + identifier=pds_id, + status=DoiStatus.Pending, + date_added=last_week if added_last_week else ages_ago, + date_updated=last_week if updated_last_week else ages_ago, + submitter="img-submitter@jpl.nasa.gov", + title="Laboratory Shocked Feldspars Bundle", + type=ProductType.Collection, + subtype="PDS4 Collection", + node_id="img", + doi=doi_id, + transaction_key="./transaction_history/img/2020-06-15T18:42:45.653317", + is_latest=True, + ) + + # def webclient_query_patch_nominal( + # self, query, url=None, username=None, password=None, content_type=CONTENT_TYPE_XML + # ): + # """ + # Patch for DOIWebClient.query_doi(). + # + # Allows a pending check to occur without actually having to communicate + # with the test server. + # + # This version simulates a successful registration response from the + # appropriate service provider. + # """ + # # Read an output label that corresponds to the DOI we're + # # checking for, and that has a status of 'registered' or 'findable' + # if DOIServiceFactory.get_service_type() == SERVICE_TYPE_OSTI: + # label = join(CheckActionTestCase.input_dir, "osti_record_registered.xml") + # else: + # label = join(CheckActionTestCase.input_dir, "datacite_record_findable.json") + # + # with open(label, "r") as infile: + # label_contents = infile.read() + # + # return label_contents + # + # def webclient_query_patch_error(self, query, url=None, username=None, password=None, content_type=CONTENT_TYPE_XML): + # """ + # Patch for DOIWebClient.query_doi(). + # + # Allows a pending check to occur without actually having to communicate + # with the OSTI test server. + # + # This version simulates an erroneous registration response from the + # service provider. + # """ + # # Read an output label that corresponds to the DOI we're + # # checking for, and that has a status of 'error' + # with open(join(CheckActionTestCase.input_dir, "osti_record_error.xml"), "r") as infile: + # xml_contents = infile.read() + # + # return xml_contents + # + # def webclient_query_patch_no_change( + # self, query, url=None, username=None, password=None, content_type=CONTENT_TYPE_XML + # ): + # """ + # Patch for DOIOstiWebClient.query_doi(). + # + # Allows a pending check to occur without actually having to communicate + # with the OSTI test server. + # + # This version simulates an response that is still pending release. + # """ + # # Read an output label that corresponds to the DOI we're + # # checking for, and that has a status of 'pending' + # with open(join(CheckActionTestCase.input_dir, "osti_record_pending.xml"), "r") as infile: + # xml_contents = infile.read() + # + # return xml_contents + # + # @patch.object( + # pds_doi_service.core.outputs.osti.osti_web_client.DOIOstiWebClient, "query_doi", webclient_query_patch_nominal + # ) + # @patch.object( + # pds_doi_service.core.outputs.datacite.datacite_web_client.DOIDataCiteWebClient, + # "query_doi", + # webclient_query_patch_nominal, + # ) + # def test_check_for_pending_entries(self): + # """Test check action that returns a successfully registered entry""" + # pending_records = self._action.run(email=False) + # + # self.assertEqual(len(pending_records), 1) + # + # pending_record = pending_records[0] + # + # self.assertEqual(pending_record["previous_status"], DoiStatus.Pending) + # self.assertIn(pending_record["status"], (DoiStatus.Registered, DoiStatus.Findable)) + # self.assertEqual(pending_record["submitter"], "img-submitter@jpl.nasa.gov") + # self.assertEqual(pending_record["doi"], "10.17189/29348") + # self.assertEqual(pending_record["identifier"], "urn:nasa:pds:lab_shocked_feldspars::1.0") + # + # @unittest.skipIf( + # DOIServiceFactory.get_service_type() == SERVICE_TYPE_DATACITE, "DataCite does not return errors via label" + # ) + # @patch.object( + # pds_doi_service.core.outputs.osti.osti_web_client.DOIOstiWebClient, "query_doi", webclient_query_patch_error + # ) + # def test_check_for_pending_entries_w_error(self): + # """Test check action that returns an error result""" + # pending_records = self._action.run(email=False) + # + # self.assertEqual(len(pending_records), 1) + # + # pending_record = pending_records[0] + # + # self.assertEqual(pending_record["previous_status"], DoiStatus.Pending) + # self.assertEqual(pending_record["status"], DoiStatus.Error) + # self.assertEqual(pending_record["submitter"], "img-submitter@jpl.nasa.gov") + # self.assertEqual(pending_record["doi"], "10.17189/29348") + # self.assertEqual(pending_record["identifier"], "urn:nasa:pds:lab_shocked_feldspars::1.0") + # + # # There should be a message to go along with the error + # self.assertIsNotNone(pending_record["message"]) + # + # @unittest.skipIf( + # DOIServiceFactory.get_service_type() == SERVICE_TYPE_DATACITE, + # "DataCite does not assign a pending state to release requests", + # ) + # @patch.object( + # pds_doi_service.core.outputs.osti.osti_web_client.DOIOstiWebClient, "query_doi", webclient_query_patch_no_change + # ) + # def test_check_for_pending_entries_w_no_change(self): + # """Test check action when no pending entries have been updated""" + # pending_records = self._action.run(email=False) + # + # self.assertEqual(len(pending_records), 1) + # + # pending_record = pending_records[0] + # + # self.assertEqual(pending_record["previous_status"], DoiStatus.Pending) + # self.assertEqual(pending_record["status"], DoiStatus.Pending) + # self.assertEqual(pending_record["submitter"], "img-submitter@jpl.nasa.gov") + # self.assertEqual(pending_record["doi"], "10.17189/29348") + # self.assertEqual(pending_record["identifier"], "urn:nasa:pds:lab_shocked_feldspars::1.0") + # + # def get_config_patch(self): + # """ + # Return a modified default config that points to a local test smtp + # server for use with the email test + # """ + # parser = configparser.ConfigParser() + # + # # default configuration + # conf_default = "conf.ini.default" + # conf_default_path = abspath(join(dirname(__file__), os.pardir, os.pardir, "util", conf_default)) + # + # parser.read(conf_default_path) + # parser["OTHER"]["emailer_local_host"] = "localhost" + # parser["OTHER"]["emailer_port"] = "1025" + # + # parser = DOIConfigUtil._resolve_relative_path(parser) + # + # return parser + # + # @patch.object(pds_doi_service.core.util.config_parser.DOIConfigUtil, "get_config", get_config_patch) + # @patch.object( + # pds_doi_service.core.outputs.osti.osti_web_client.DOIOstiWebClient, "query_doi", webclient_query_patch_nominal + # ) + # @patch.object( + # pds_doi_service.core.outputs.datacite.datacite_web_client.DOIDataCiteWebClient, + # "query_doi", + # webclient_query_patch_nominal, + # ) + # def test_email_receipt(self): + # """Test sending of the check action status via email""" + # # Create a new check action so our patched config is pulled in + # action = DOICoreActionCheck(self.db_name) + # + # with tempfile.TemporaryFile() as temp_file: + # # Stand up a subprocess running a debug smtpd server + # # By default, all this server is does is echo email payloads to + # # standard out, so provide a temp file to capture it + # debug_email_proc = subprocess.Popen( + # ["python", "-u", "-m", "smtpd", "-n", "-c", "DebuggingServer", "localhost:1025"], stdout=temp_file + # ) + # + # # Give the debug smtp server a chance to start listening + # time.sleep(1) + # + # try: + # # Run the check action and have it send an email w/ attachment + # action.run(email=True, attachment=True, submitter="email-test@email.com") + # + # # Read the raw email contents (payload) from the subprocess + # # into a string + # temp_file.seek(0) + # email_contents = temp_file.read() + # message = message_from_bytes(email_contents).get_payload() + # finally: + # # Send the debug smtp server a ctrl+C and wait for it to stop + # os.kill(debug_email_proc.pid, signal.SIGINT) + # debug_email_proc.wait() + # + # # Run some string searches on the email body to ensure what we expect + # # made it in + # + # # Email address provided to check action should be present + # self.assertIn("email-test@email.com", message) + # + # # Subject line should be present + # self.assertIn("DOI Submission Status Report For Node", message) + # + # # Attachment should also be provided + # self.assertIn("Content-Disposition: attachment; filename=doi_status_", message) + + +if __name__ == "__main__": + unittest.main() From 7c84a7feac253c1350281470af550d6d6813cb20 Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Tue, 13 Sep 2022 18:42:44 -0700 Subject: [PATCH 15/22] add some explanatory comments --- scripts/email_weekly_roundup.py | 8 ++++++++ src/pds_doi_service/core/actions/roundup.py | 9 +++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/scripts/email_weekly_roundup.py b/scripts/email_weekly_roundup.py index d91fae5d..af41ddb7 100644 --- a/scripts/email_weekly_roundup.py +++ b/scripts/email_weekly_roundup.py @@ -6,6 +6,14 @@ from pds_doi_service.core.util.general_util import get_logger if __name__ == '__main__': + """ + Send an email consisting of a summary of all DOIs updated in the previous week (i.e. between the previous Sunday + and the Monday before that, inclusive), with a JSON attachment for those DoiRecords. + + Should be run in a crontab, preferably on Monday, for example: + 0 0 * * MON . path/to/doi-service/venv/bin/python path/to/doi-service/scripts/email_weekly_roundup.py + """ + logging = get_logger('email_weekly_roundup') config = DOIConfigUtil.get_config() db_filepath = os.path.abspath(config['OTHER']['db_file']) diff --git a/src/pds_doi_service/core/actions/roundup.py b/src/pds_doi_service/core/actions/roundup.py index 1599fd0b..619fbaa2 100644 --- a/src/pds_doi_service/core/actions/roundup.py +++ b/src/pds_doi_service/core/actions/roundup.py @@ -39,6 +39,7 @@ def get_email_content_template(template_filename: str = "email_weekly_roundup.ji def prepare_doi_record_for_template(record: DoiRecord) -> Dict[str, str]: + """Map a DoiRecord to the set of information required for rendering it in the template""" update_type = "submitted" if record.date_added == record.date_updated else "updated" prepared_record = { "datacite_id": record.doi, @@ -51,7 +52,7 @@ def prepare_doi_record_for_template(record: DoiRecord) -> Dict[str, str]: return prepared_record -def prepare_email_content(first_date: date, last_date: date, modified_doi_records: List[DoiRecord]) -> str: +def prepare_email_html_content(first_date: date, last_date: date, modified_doi_records: List[DoiRecord]) -> str: template = get_email_content_template() template_dict = { "first_date": first_date, @@ -83,7 +84,7 @@ def prepare_email_message( msg["Subject"] = email_subject msg["To"] = receiver_email - email_content = prepare_email_content(first_date, last_date, modified_doi_records) + email_content = prepare_email_html_content(first_date, last_date, modified_doi_records) msg.attach(MIMEText(email_content, "html")) attachment_filename = f"updated_dois_{first_date.isoformat()}_{last_date.isoformat()}.json" attach_json_data(attachment_filename, modified_doi_records, msg) @@ -92,6 +93,10 @@ def prepare_email_message( def run(database: DOIDataBase, sender_email: str, receiver_email: str) -> None: + """ + Send an email consisting of a summary of all DOIs updated in the previous week (i.e. between the previous Sunday + and the Monday before that, inclusive), with a JSON attachment for those DoiRecords. + """ target_week_begin = get_start_of_local_week() - timedelta(days=7) target_week_end = target_week_begin + timedelta(days=7, microseconds=-1) last_date_of_week = (target_week_end - timedelta(microseconds=1)).date() From 6aaf8972a44e67a3045f524c802a6aaef9d35a9f Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Tue, 13 Sep 2022 18:55:47 -0700 Subject: [PATCH 16/22] remove cruft and add copyright --- scripts/email_weekly_roundup.py | 4 +- src/pds_doi_service/core/actions/roundup.py | 13 ++ .../core/actions/test/roundup_test.py | 214 +----------------- .../core/actions/test/util/email.py | 13 ++ 4 files changed, 29 insertions(+), 215 deletions(-) diff --git a/scripts/email_weekly_roundup.py b/scripts/email_weekly_roundup.py index af41ddb7..59175c2d 100644 --- a/scripts/email_weekly_roundup.py +++ b/scripts/email_weekly_roundup.py @@ -7,9 +7,9 @@ if __name__ == '__main__': """ - Send an email consisting of a summary of all DOIs updated in the previous week (i.e. between the previous Sunday + Send an email consisting of a summary of all DOIs updated in the previous week (i.e. between the previous Sunday and the Monday before that, inclusive), with a JSON attachment for those DoiRecords. - + Should be run in a crontab, preferably on Monday, for example: 0 0 * * MON . path/to/doi-service/venv/bin/python path/to/doi-service/scripts/email_weekly_roundup.py """ diff --git a/src/pds_doi_service/core/actions/roundup.py b/src/pds_doi_service/core/actions/roundup.py index 619fbaa2..0614cd90 100644 --- a/src/pds_doi_service/core/actions/roundup.py +++ b/src/pds_doi_service/core/actions/roundup.py @@ -1,3 +1,16 @@ +# +# Copyright 2022, by the California Institute of Technology. ALL RIGHTS +# RESERVED. United States Government Sponsorship acknowledged. Any commercial +# use must be negotiated with the Office of Technology Transfer at the +# California Institute of Technology. +# +""" +====== +roundup.py +====== + +Contains functions for sending email notifications for recently-updated DOIs. +""" import json import logging import os diff --git a/src/pds_doi_service/core/actions/test/roundup_test.py b/src/pds_doi_service/core/actions/test/roundup_test.py index e20bd4e1..4a482ff9 100644 --- a/src/pds_doi_service/core/actions/test/roundup_test.py +++ b/src/pds_doi_service/core/actions/test/roundup_test.py @@ -1,36 +1,19 @@ -#!/usr/bin/env python import base64 -import configparser import json import os import shutil -import signal -import subprocess import tempfile -import time import unittest -import uuid from datetime import datetime from datetime import timedelta -from email import message_from_bytes from email.message import Message -from email.parser import BytesParser -from unittest.mock import patch -import pds_doi_service.core.outputs.datacite.datacite_web_client -import pds_doi_service.core.outputs.osti.osti_web_client -from pds_doi_service.core.actions import DOICoreActionCheck from pds_doi_service.core.actions.roundup import run as do_roundup from pds_doi_service.core.actions.test.util.email import capture_email from pds_doi_service.core.db.doi_database import DOIDataBase from pds_doi_service.core.entities.doi import DoiRecord from pds_doi_service.core.entities.doi import DoiStatus from pds_doi_service.core.entities.doi import ProductType -from pds_doi_service.core.outputs.doi_record import CONTENT_TYPE_XML -from pds_doi_service.core.outputs.service import DOIServiceFactory -from pds_doi_service.core.outputs.service import SERVICE_TYPE_DATACITE -from pds_doi_service.core.outputs.service import SERVICE_TYPE_OSTI -from pds_doi_service.core.util.config_parser import DOIConfigUtil from pkg_resources import resource_filename @@ -79,7 +62,7 @@ def test_html_content(self): expected_content_filepath = os.path.join(self.resources_dir, "roundup_email_body.html") with open(expected_content_filepath, "r") as infile: expected_html_content = infile.read() - self.assertEqual(expected_html_content.replace(" ", ""), html_content.replace(" ", "")) + self.assertEqual(expected_html_content.replace(" ", "").strip(), html_content.replace(" ", "").strip()) def test_attachment_content(self): attachment_content = self._message.get_payload(1).get_payload() @@ -123,201 +106,6 @@ def generate_doi_record(uid: str, added_last_week: bool, updated_last_week: bool is_latest=True, ) - # def webclient_query_patch_nominal( - # self, query, url=None, username=None, password=None, content_type=CONTENT_TYPE_XML - # ): - # """ - # Patch for DOIWebClient.query_doi(). - # - # Allows a pending check to occur without actually having to communicate - # with the test server. - # - # This version simulates a successful registration response from the - # appropriate service provider. - # """ - # # Read an output label that corresponds to the DOI we're - # # checking for, and that has a status of 'registered' or 'findable' - # if DOIServiceFactory.get_service_type() == SERVICE_TYPE_OSTI: - # label = join(CheckActionTestCase.input_dir, "osti_record_registered.xml") - # else: - # label = join(CheckActionTestCase.input_dir, "datacite_record_findable.json") - # - # with open(label, "r") as infile: - # label_contents = infile.read() - # - # return label_contents - # - # def webclient_query_patch_error(self, query, url=None, username=None, password=None, content_type=CONTENT_TYPE_XML): - # """ - # Patch for DOIWebClient.query_doi(). - # - # Allows a pending check to occur without actually having to communicate - # with the OSTI test server. - # - # This version simulates an erroneous registration response from the - # service provider. - # """ - # # Read an output label that corresponds to the DOI we're - # # checking for, and that has a status of 'error' - # with open(join(CheckActionTestCase.input_dir, "osti_record_error.xml"), "r") as infile: - # xml_contents = infile.read() - # - # return xml_contents - # - # def webclient_query_patch_no_change( - # self, query, url=None, username=None, password=None, content_type=CONTENT_TYPE_XML - # ): - # """ - # Patch for DOIOstiWebClient.query_doi(). - # - # Allows a pending check to occur without actually having to communicate - # with the OSTI test server. - # - # This version simulates an response that is still pending release. - # """ - # # Read an output label that corresponds to the DOI we're - # # checking for, and that has a status of 'pending' - # with open(join(CheckActionTestCase.input_dir, "osti_record_pending.xml"), "r") as infile: - # xml_contents = infile.read() - # - # return xml_contents - # - # @patch.object( - # pds_doi_service.core.outputs.osti.osti_web_client.DOIOstiWebClient, "query_doi", webclient_query_patch_nominal - # ) - # @patch.object( - # pds_doi_service.core.outputs.datacite.datacite_web_client.DOIDataCiteWebClient, - # "query_doi", - # webclient_query_patch_nominal, - # ) - # def test_check_for_pending_entries(self): - # """Test check action that returns a successfully registered entry""" - # pending_records = self._action.run(email=False) - # - # self.assertEqual(len(pending_records), 1) - # - # pending_record = pending_records[0] - # - # self.assertEqual(pending_record["previous_status"], DoiStatus.Pending) - # self.assertIn(pending_record["status"], (DoiStatus.Registered, DoiStatus.Findable)) - # self.assertEqual(pending_record["submitter"], "img-submitter@jpl.nasa.gov") - # self.assertEqual(pending_record["doi"], "10.17189/29348") - # self.assertEqual(pending_record["identifier"], "urn:nasa:pds:lab_shocked_feldspars::1.0") - # - # @unittest.skipIf( - # DOIServiceFactory.get_service_type() == SERVICE_TYPE_DATACITE, "DataCite does not return errors via label" - # ) - # @patch.object( - # pds_doi_service.core.outputs.osti.osti_web_client.DOIOstiWebClient, "query_doi", webclient_query_patch_error - # ) - # def test_check_for_pending_entries_w_error(self): - # """Test check action that returns an error result""" - # pending_records = self._action.run(email=False) - # - # self.assertEqual(len(pending_records), 1) - # - # pending_record = pending_records[0] - # - # self.assertEqual(pending_record["previous_status"], DoiStatus.Pending) - # self.assertEqual(pending_record["status"], DoiStatus.Error) - # self.assertEqual(pending_record["submitter"], "img-submitter@jpl.nasa.gov") - # self.assertEqual(pending_record["doi"], "10.17189/29348") - # self.assertEqual(pending_record["identifier"], "urn:nasa:pds:lab_shocked_feldspars::1.0") - # - # # There should be a message to go along with the error - # self.assertIsNotNone(pending_record["message"]) - # - # @unittest.skipIf( - # DOIServiceFactory.get_service_type() == SERVICE_TYPE_DATACITE, - # "DataCite does not assign a pending state to release requests", - # ) - # @patch.object( - # pds_doi_service.core.outputs.osti.osti_web_client.DOIOstiWebClient, "query_doi", webclient_query_patch_no_change - # ) - # def test_check_for_pending_entries_w_no_change(self): - # """Test check action when no pending entries have been updated""" - # pending_records = self._action.run(email=False) - # - # self.assertEqual(len(pending_records), 1) - # - # pending_record = pending_records[0] - # - # self.assertEqual(pending_record["previous_status"], DoiStatus.Pending) - # self.assertEqual(pending_record["status"], DoiStatus.Pending) - # self.assertEqual(pending_record["submitter"], "img-submitter@jpl.nasa.gov") - # self.assertEqual(pending_record["doi"], "10.17189/29348") - # self.assertEqual(pending_record["identifier"], "urn:nasa:pds:lab_shocked_feldspars::1.0") - # - # def get_config_patch(self): - # """ - # Return a modified default config that points to a local test smtp - # server for use with the email test - # """ - # parser = configparser.ConfigParser() - # - # # default configuration - # conf_default = "conf.ini.default" - # conf_default_path = abspath(join(dirname(__file__), os.pardir, os.pardir, "util", conf_default)) - # - # parser.read(conf_default_path) - # parser["OTHER"]["emailer_local_host"] = "localhost" - # parser["OTHER"]["emailer_port"] = "1025" - # - # parser = DOIConfigUtil._resolve_relative_path(parser) - # - # return parser - # - # @patch.object(pds_doi_service.core.util.config_parser.DOIConfigUtil, "get_config", get_config_patch) - # @patch.object( - # pds_doi_service.core.outputs.osti.osti_web_client.DOIOstiWebClient, "query_doi", webclient_query_patch_nominal - # ) - # @patch.object( - # pds_doi_service.core.outputs.datacite.datacite_web_client.DOIDataCiteWebClient, - # "query_doi", - # webclient_query_patch_nominal, - # ) - # def test_email_receipt(self): - # """Test sending of the check action status via email""" - # # Create a new check action so our patched config is pulled in - # action = DOICoreActionCheck(self.db_name) - # - # with tempfile.TemporaryFile() as temp_file: - # # Stand up a subprocess running a debug smtpd server - # # By default, all this server is does is echo email payloads to - # # standard out, so provide a temp file to capture it - # debug_email_proc = subprocess.Popen( - # ["python", "-u", "-m", "smtpd", "-n", "-c", "DebuggingServer", "localhost:1025"], stdout=temp_file - # ) - # - # # Give the debug smtp server a chance to start listening - # time.sleep(1) - # - # try: - # # Run the check action and have it send an email w/ attachment - # action.run(email=True, attachment=True, submitter="email-test@email.com") - # - # # Read the raw email contents (payload) from the subprocess - # # into a string - # temp_file.seek(0) - # email_contents = temp_file.read() - # message = message_from_bytes(email_contents).get_payload() - # finally: - # # Send the debug smtp server a ctrl+C and wait for it to stop - # os.kill(debug_email_proc.pid, signal.SIGINT) - # debug_email_proc.wait() - # - # # Run some string searches on the email body to ensure what we expect - # # made it in - # - # # Email address provided to check action should be present - # self.assertIn("email-test@email.com", message) - # - # # Subject line should be present - # self.assertIn("DOI Submission Status Report For Node", message) - # - # # Attachment should also be provided - # self.assertIn("Content-Disposition: attachment; filename=doi_status_", message) - if __name__ == "__main__": unittest.main() diff --git a/src/pds_doi_service/core/actions/test/util/email.py b/src/pds_doi_service/core/actions/test/util/email.py index fa5c3b49..c4b7db0c 100644 --- a/src/pds_doi_service/core/actions/test/util/email.py +++ b/src/pds_doi_service/core/actions/test/util/email.py @@ -1,3 +1,16 @@ +# +# Copyright 2022, by the California Institute of Technology. ALL RIGHTS +# RESERVED. United States Government Sponsorship acknowledged. Any commercial +# use must be negotiated with the Office of Technology Transfer at the +# California Institute of Technology. +# +""" +====== +email.py +====== + +Contains utility functions for testing email functionality. +""" import configparser import os import signal From e3857fd175c21d2e184dd560b795d88bcd0ea893 Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Wed, 14 Sep 2022 09:54:59 -0700 Subject: [PATCH 17/22] change smtpd subprocess to use sys.executable --- src/pds_doi_service/core/actions/test/util/email.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pds_doi_service/core/actions/test/util/email.py b/src/pds_doi_service/core/actions/test/util/email.py index c4b7db0c..51ce4259 100644 --- a/src/pds_doi_service/core/actions/test/util/email.py +++ b/src/pds_doi_service/core/actions/test/util/email.py @@ -15,6 +15,7 @@ import os import signal import subprocess +import sys import tempfile import time from email.message import Message @@ -56,7 +57,7 @@ def capture_email(f: Callable[[], None], port: int = 1025) -> Message: # By default, all this server is does is echo email payloads to # standard out, so provide a temp file to capture it debug_email_proc = subprocess.Popen( - ["python", "-u", "-m", "smtpd", "-n", "-c", "DebuggingServer", f"localhost:{port}"], stdout=temp_file + [sys.executable, "-u", "-m", "smtpd", "-n", "-c", "DebuggingServer", f"localhost:{port}"], stdout=temp_file ) # Give the debug smtp server a chance to start listening From 4eefb3a442eaabab15aae2f55d32bdd9739d614a Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Wed, 14 Sep 2022 11:39:24 -0700 Subject: [PATCH 18/22] update README.md with link to JPL internal wiki --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 44cb30f7..47b9b852 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ Please visit the documentation at: https://nasa-pds.github.io/doi-service/ ## Developers +[JPL Internal Wiki](https://wiki.jpl.nasa.gov/display/PDSEN/DOI+Service) + Get the code and work on a branch: git clone ... From 5bdb43daab568b05c66977251b1de5ba21c60070 Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Wed, 14 Sep 2022 11:44:49 -0700 Subject: [PATCH 19/22] fixed nondeterministic elements of expected roundup email html --- ...il_body.html => roundup_email_body.jinja2} | 6 ++-- .../core/actions/test/roundup_test.py | 30 ++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) rename src/pds_doi_service/core/actions/test/data/roundup/{roundup_email_body.html => roundup_email_body.jinja2} (64%) diff --git a/src/pds_doi_service/core/actions/test/data/roundup/roundup_email_body.html b/src/pds_doi_service/core/actions/test/data/roundup/roundup_email_body.jinja2 similarity index 64% rename from src/pds_doi_service/core/actions/test/data/roundup/roundup_email_body.html rename to src/pds_doi_service/core/actions/test/data/roundup/roundup_email_body.jinja2 index 985ebbdf..34689fc2 100644 --- a/src/pds_doi_service/core/actions/test/data/roundup/roundup_email_body.html +++ b/src/pds_doi_service/core/actions/test/data/roundup/roundup_email_body.jinja2 @@ -1,11 +1,11 @@

DataCite DOI Weekly Roundup

-

The following DOIs were reserved, updated or released during the week 09/05 through 09/11:

+

The following DOIs were reserved, updated or released during the week {{ week_start.strftime('%m/%d') }} through {{ week_end.strftime('%m/%d') }}:

    -
  • 10.17189/11111 urn:nasa:pds:product_11111::1.0 (Pending) submitted 2022-09-09
  • +
  • 10.17189/11111 urn:nasa:pds:product_11111::1.0 (Pending) submitted {{ modifications_date.isoformat() }}
  • -
  • 10.17189/22222 urn:nasa:pds:product_22222::1.0 (Pending) updated 2022-09-09
  • +
  • 10.17189/22222 urn:nasa:pds:product_22222::1.0 (Pending) updated {{ modifications_date.isoformat() }}
diff --git a/src/pds_doi_service/core/actions/test/roundup_test.py b/src/pds_doi_service/core/actions/test/roundup_test.py index 4a482ff9..53db3ad4 100644 --- a/src/pds_doi_service/core/actions/test/roundup_test.py +++ b/src/pds_doi_service/core/actions/test/roundup_test.py @@ -8,7 +8,8 @@ from datetime import timedelta from email.message import Message -from pds_doi_service.core.actions.roundup import run as do_roundup +import jinja2 +from pds_doi_service.core.actions.roundup import run as do_roundup, get_start_of_local_week from pds_doi_service.core.actions.test.util.email import capture_email from pds_doi_service.core.db.doi_database import DOIDataBase from pds_doi_service.core.entities.doi import DoiRecord @@ -30,6 +31,11 @@ class WeeklyRoundupEmailNotificationTestCase(unittest.TestCase): sender = "test_sender@jpl.nasa.gov" recipient = "test_recipient@jpl.nasa.gov" + # Some reference datetimes that are referenced in SetUpClass and in tests + _now = datetime.now() + _last_week = _now - timedelta(days=4) + _ages_ago = _now - timedelta(days=30) + @classmethod def setUpClass(cls): cls._database_obj = DOIDataBase(cls.db_filepath) @@ -58,10 +64,16 @@ def test_roundup_email_recipients_correct(self): self.assertEqual(self.recipient, self._message["To"]) def test_html_content(self): + template_dict = { + "week_start": get_start_of_local_week().date() - timedelta(days=7), + "week_end": get_start_of_local_week().date() - timedelta(days=1), + "modifications_date": self._last_week.date(), + } html_content = self._message.get_payload(0).get_payload() - expected_content_filepath = os.path.join(self.resources_dir, "roundup_email_body.html") + expected_content_filepath = os.path.join(self.resources_dir, "roundup_email_body.jinja2") with open(expected_content_filepath, "r") as infile: - expected_html_content = infile.read() + template = jinja2.Template(infile.read()) + expected_html_content = template.render(template_dict) self.assertEqual(expected_html_content.replace(" ", "").strip(), html_content.replace(" ", "").strip()) def test_attachment_content(self): @@ -80,13 +92,11 @@ def test_attachment_content(self): self.assertEqual(expected_data, attachment_data) - @staticmethod - def generate_doi_record(uid: str, added_last_week: bool, updated_last_week: bool): + + @classmethod + def generate_doi_record(cls, uid: str, added_last_week: bool, updated_last_week: bool): if added_last_week: assert updated_last_week - now = datetime.now() - last_week = now - timedelta(days=4) - ages_ago = now - timedelta(days=30) pds_id = f"urn:nasa:pds:product_{uid}::1.0" doi_id = f"10.17189/{uid}" @@ -94,8 +104,8 @@ def generate_doi_record(uid: str, added_last_week: bool, updated_last_week: bool return DoiRecord( identifier=pds_id, status=DoiStatus.Pending, - date_added=last_week if added_last_week else ages_ago, - date_updated=last_week if updated_last_week else ages_ago, + date_added=cls._last_week if added_last_week else cls._ages_ago, + date_updated=cls._last_week if updated_last_week else cls._ages_ago, submitter="img-submitter@jpl.nasa.gov", title="Laboratory Shocked Feldspars Bundle", type=ProductType.Collection, From d45f32f92b5072caa5181d1f6ec5cd3bd4f71ec4 Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Wed, 14 Sep 2022 11:45:09 -0700 Subject: [PATCH 20/22] fix comment --- src/pds_doi_service/core/util/emailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pds_doi_service/core/util/emailer.py b/src/pds_doi_service/core/util/emailer.py index b9c52f4d..855ca3ad 100644 --- a/src/pds_doi_service/core/util/emailer.py +++ b/src/pds_doi_service/core/util/emailer.py @@ -70,7 +70,7 @@ def send_message(self, message): Parameters ---------- - message : Union[email.message.Message, + message : email.message.Message, A message object with the 'From' and 'To:' and 'Subject' already filled in. From 89765db2f9b6fdcc907860bcf100d445fe116607 Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Wed, 14 Sep 2022 12:16:09 -0700 Subject: [PATCH 21/22] skip WeeklyRoundupEmailNotificationTestCase when running in CI --- src/pds_doi_service/core/actions/test/roundup_test.py | 1 + tox.ini | 1 + 2 files changed, 2 insertions(+) diff --git a/src/pds_doi_service/core/actions/test/roundup_test.py b/src/pds_doi_service/core/actions/test/roundup_test.py index 53db3ad4..caef4f22 100644 --- a/src/pds_doi_service/core/actions/test/roundup_test.py +++ b/src/pds_doi_service/core/actions/test/roundup_test.py @@ -18,6 +18,7 @@ from pkg_resources import resource_filename +@unittest.skipIf(os.environ.get('CI') == 'true', "Test is currently broken in Github Actions workflow. See #") class WeeklyRoundupEmailNotificationTestCase(unittest.TestCase): tests_dir = os.path.abspath(resource_filename(__name__, "")) resources_dir = os.path.join(tests_dir, "data", "roundup") diff --git a/tox.ini b/tox.ini index cc84bd4d..836385b1 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = py39, docs, lint deps = .[dev] whitelist_externals = pytest commands = pytest +passenv = CI [testenv:docs] deps = .[dev] From 8b826e6786e27133b63cf242cc5601dd0f7a6726 Mon Sep 17 00:00:00 2001 From: Alex Dunn Date: Wed, 14 Sep 2022 14:54:31 -0700 Subject: [PATCH 22/22] lint --- .../core/actions/test/roundup_test.py | 12 ++++++------ src/pds_doi_service/core/entities/doi.py | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/pds_doi_service/core/actions/test/roundup_test.py b/src/pds_doi_service/core/actions/test/roundup_test.py index caef4f22..0c610319 100644 --- a/src/pds_doi_service/core/actions/test/roundup_test.py +++ b/src/pds_doi_service/core/actions/test/roundup_test.py @@ -9,7 +9,8 @@ from email.message import Message import jinja2 -from pds_doi_service.core.actions.roundup import run as do_roundup, get_start_of_local_week +from pds_doi_service.core.actions.roundup import get_start_of_local_week +from pds_doi_service.core.actions.roundup import run as do_roundup from pds_doi_service.core.actions.test.util.email import capture_email from pds_doi_service.core.db.doi_database import DOIDataBase from pds_doi_service.core.entities.doi import DoiRecord @@ -18,7 +19,7 @@ from pkg_resources import resource_filename -@unittest.skipIf(os.environ.get('CI') == 'true', "Test is currently broken in Github Actions workflow. See #") +@unittest.skipIf(os.environ.get("CI") == "true", "Test is currently broken in Github Actions workflow. See #361") class WeeklyRoundupEmailNotificationTestCase(unittest.TestCase): tests_dir = os.path.abspath(resource_filename(__name__, "")) resources_dir = os.path.join(tests_dir, "data", "roundup") @@ -66,9 +67,9 @@ def test_roundup_email_recipients_correct(self): def test_html_content(self): template_dict = { - "week_start": get_start_of_local_week().date() - timedelta(days=7), - "week_end": get_start_of_local_week().date() - timedelta(days=1), - "modifications_date": self._last_week.date(), + "week_start": get_start_of_local_week().date() - timedelta(days=7), + "week_end": get_start_of_local_week().date() - timedelta(days=1), + "modifications_date": self._last_week.date(), } html_content = self._message.get_payload(0).get_payload() expected_content_filepath = os.path.join(self.resources_dir, "roundup_email_body.jinja2") @@ -93,7 +94,6 @@ def test_attachment_content(self): self.assertEqual(expected_data, attachment_data) - @classmethod def generate_doi_record(cls, uid: str, added_last_week: bool, updated_last_week: bool): if added_last_week: diff --git a/src/pds_doi_service/core/entities/doi.py b/src/pds_doi_service/core/entities/doi.py index 7ee2768d..207f4d37 100644 --- a/src/pds_doi_service/core/entities/doi.py +++ b/src/pds_doi_service/core/entities/doi.py @@ -18,7 +18,8 @@ from datetime import datetime from enum import Enum from enum import unique -from typing import List, Dict +from typing import Dict +from typing import List from typing import Optional from pds_doi_service.core.outputs.schemaentities.rights import CC0_LICENSE