diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6115fc1..814cde2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: - name: Install the project run: uv sync --all-extras --dev - - name: Install emailer-lib + - name: Install nbmail run: uv pip install -e . # quarto docs build ---- @@ -57,7 +57,7 @@ jobs: with: source-dir: docs/_site - test-emailer-lib: + test-nbmail: runs-on: ubuntu-latest strategy: matrix: @@ -80,19 +80,19 @@ jobs: - name: Install the project run: uv sync --all-extras --dev - - name: Install emailer-lib + - name: Install nbmail run: uv pip install -e . - name: Install the project deps run: uv pip install -e .[dev] - - name: Test emailer-lib + - name: Test nbmail run: | - uv run pytest emailer_lib/tests/ --cov=emailer_lib --cov-report=xml --cov-report=term-missing + uv run pytest nbmail/tests/ --cov=nbmail --cov-report=xml --cov-report=term-missing - name: Upload coverage reports uses: codecov/codecov-action@v4 if: ${{ matrix.python-version == '3.12' }} with: - file: emailer-lib/coverage.xml - flags: emailer-lib + file: nbmail/coverage.xml + flags: nbmail diff --git a/Makefile b/Makefile index c0c4950..b743416 100644 --- a/Makefile +++ b/Makefile @@ -5,10 +5,10 @@ preview: cd docs && quarto preview test: - pytest emailer_lib/tests emailer_lib/mjml/tests --cov-report=xml + pytest nbmail/tests nbmail/mjml/tests --cov-report=xml test-update: - pytest emailer_lib/tests emailer_lib/mjml/tests --snapshot-update + pytest nbmail/tests nbmail/mjml/tests --snapshot-update generate-mjml-tags: - python3 emailer_lib/mjml/scripts/generate_tags.py + python3 nbmail/mjml/scripts/generate_tags.py diff --git a/README.md b/README.md index b026ae7..2c21d00 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# emailer-lib +# nbmail @@ -11,12 +11,12 @@ -> ⚠️ **emailer-lib is currently in development, expect breaking changes.** +> ⚠️ **nbmail is currently in development, expect breaking changes.** -### What is [emailer-lib](https://posit-dev.github.io/email-for-data-science/reference/)? +### What is [nbmail](https://posit-dev.github.io/email-for-data-science/reference/)? -**emailer-lib** is a Python package for serializing, previewing, and sending email messages in a consistent, simple structure. It provides utilities to convert emails from different sources (Redmail, Yagmail, MJML, Quarto JSON) into a unified intermediate format, and send them via multiple backends (Gmail, SMTP, Mailgun, etc.). +**nbmail** is a Python package for serializing, previewing, and sending email messages in a consistent, simple structure. It provides utilities to convert emails from different sources (Redmail, Yagmail, MJML, Quarto JSON) into a unified intermediate format, and send them via multiple backends (Gmail, SMTP, Mailgun, etc.). The package is designed for data science workflows and Quarto projects, making it easy to generate, preview, and deliver rich email content programmatically. @@ -24,27 +24,27 @@ The package is designed for data science workflows and Quarto projects, making i Install the latest release from your local repo or PyPI: ```bash -pip install -e ./emailer-lib +pip install -e ./nbmail ``` --> ## Example Usage ```python -from emailer_lib import ( - quarto_json_to_intermediate_email, - IntermediateEmail, - send_intermediate_email_with_gmail, +from nbmail import ( + quarto_json_to_email, + Email, + send_email_with_gmail, ) # Read a Quarto email JSON file -email_struct = quarto_json_to_intermediate_email("email.json") +email_struct = quarto_json_to_email("email.json") # Preview the email as HTML email_struct.write_preview_email("preview.html") # Send the email via Gmail -send_intermediate_email_with_gmail("your_email@gmail.com", "your_password", email_struct) +send_email_with_gmail("your_email@gmail.com", "your_password", email_struct) ``` ## Features @@ -81,4 +81,4 @@ url = {https://github.com/posit-dev/gt-extras}, version = {0.0.1} } ``` --> -For more information, see the [docs](https://posit-dev.github.io/email-for-data-science/reference) or [open an issue](https://github.com/posit-dev/email-for-data-science/issues) with questions or suggestions! \ No newline at end of file +For more information, see the [docs](https://posit-dev.github.io/email-for-data-science/reference) or [open an issue](https://github.com/posit-dev/email-for-data-science/issues) with questions or suggestions! diff --git a/docs/_quarto.yml b/docs/_quarto.yml index cfa6598..6a56812 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -29,7 +29,7 @@ website: - orchestrating-tests.qmd quartodoc: - package: emailer_lib + package: nbmail dir: reference title: API Reference css: reference/_styles-quartodoc.css @@ -43,31 +43,31 @@ quartodoc: An email object that in a serializable, previewable format, optimized for emails with content generated by data scientists. contents: - - name: IntermediateEmail + - name: Email children: separate - - name: IntermediateEmail.write_preview_email - - name: IntermediateEmail.write_email_message - - name: IntermediateEmail.preview_send_email + - name: Email.write_preview_email + - name: Email.write_email_message + - name: Email.preview_send_email - title: Uploading emails desc: > - Converting emails to IntermediateEmails, + Converting emails to Emails, at which point they can be previewed, tested, and sent. contents: - - quarto_json_to_intermediate_email - - mjml_to_intermediate_email - - redmail_to_intermediate_email - - yagmail_to_intermediate_email + - quarto_json_to_email + - mjml_to_email + - redmail_to_email + - yagmail_to_email - title: Sending desc: > Functions to sending emails with different providers. And a special handy one to bypass the intermediate object if you are sending a quarto email. contents: - - send_intermediate_email_with_gmail - - send_intermediate_email_with_smtp - - send_intermediate_email_with_redmail - - send_intermediate_email_with_yagmail - - send_intermediate_email_with_mailgun + - send_email_with_gmail + - send_email_with_smtp + - send_email_with_redmail + - send_email_with_yagmail + - send_email_with_mailgun - send_quarto_email_with_gmail - title: Utilities @@ -79,7 +79,7 @@ quartodoc: - title: MJML Authoring desc: > Write responsive emails with MJML - package: emailer_lib + package: nbmail contents: - mjml.mjml - mjml.head diff --git a/docs/objects.json b/docs/objects.json index 6165209..073ffef 100644 --- a/docs/objects.json +++ b/docs/objects.json @@ -1 +1 @@ -{"project": "emailer_lib", "version": "0.0.9999", "count": 106, "items": [{"name": "emailer_lib.IntermediateEmail.preview_send_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/emailer_lib.IntermediateEmail.preview_send_email.html#emailer_lib.IntermediateEmail.preview_send_email", "dispname": "-"}, {"name": "emailer_lib.structs.IntermediateEmail.preview_send_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/emailer_lib.IntermediateEmail.preview_send_email.html#emailer_lib.IntermediateEmail.preview_send_email", "dispname": "emailer_lib.IntermediateEmail.preview_send_email"}, {"name": "emailer_lib.IntermediateEmail.write_email_message", "domain": "py", "role": "function", "priority": "1", "uri": "reference/emailer_lib.IntermediateEmail.write_email_message.html#emailer_lib.IntermediateEmail.write_email_message", "dispname": "-"}, {"name": "emailer_lib.structs.IntermediateEmail.write_email_message", "domain": "py", "role": "function", "priority": "1", "uri": "reference/emailer_lib.IntermediateEmail.write_email_message.html#emailer_lib.IntermediateEmail.write_email_message", "dispname": "emailer_lib.IntermediateEmail.write_email_message"}, {"name": "emailer_lib.IntermediateEmail.write_preview_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/emailer_lib.IntermediateEmail.write_preview_email.html#emailer_lib.IntermediateEmail.write_preview_email", "dispname": "-"}, {"name": "emailer_lib.structs.IntermediateEmail.write_preview_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/emailer_lib.IntermediateEmail.write_preview_email.html#emailer_lib.IntermediateEmail.write_preview_email", "dispname": "emailer_lib.IntermediateEmail.write_preview_email"}, {"name": "emailer_lib.IntermediateEmail", "domain": "py", "role": "class", "priority": "1", "uri": "reference/IntermediateEmail.html#emailer_lib.IntermediateEmail", "dispname": "-"}, {"name": "emailer_lib.structs.IntermediateEmail", "domain": "py", "role": "class", "priority": "1", "uri": "reference/IntermediateEmail.html#emailer_lib.IntermediateEmail", "dispname": "emailer_lib.IntermediateEmail"}, {"name": "emailer_lib.IntermediateEmail.write_preview_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/IntermediateEmail.write_preview_email.html#emailer_lib.IntermediateEmail.write_preview_email", "dispname": "-"}, {"name": "emailer_lib.structs.IntermediateEmail.write_preview_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/IntermediateEmail.write_preview_email.html#emailer_lib.IntermediateEmail.write_preview_email", "dispname": "emailer_lib.IntermediateEmail.write_preview_email"}, {"name": "emailer_lib.IntermediateEmail.write_email_message", "domain": "py", "role": "function", "priority": "1", "uri": "reference/IntermediateEmail.write_email_message.html#emailer_lib.IntermediateEmail.write_email_message", "dispname": "-"}, {"name": "emailer_lib.structs.IntermediateEmail.write_email_message", "domain": "py", "role": "function", "priority": "1", "uri": "reference/IntermediateEmail.write_email_message.html#emailer_lib.IntermediateEmail.write_email_message", "dispname": "emailer_lib.IntermediateEmail.write_email_message"}, {"name": "emailer_lib.IntermediateEmail.preview_send_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/IntermediateEmail.preview_send_email.html#emailer_lib.IntermediateEmail.preview_send_email", "dispname": "-"}, {"name": "emailer_lib.structs.IntermediateEmail.preview_send_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/IntermediateEmail.preview_send_email.html#emailer_lib.IntermediateEmail.preview_send_email", "dispname": "emailer_lib.IntermediateEmail.preview_send_email"}, {"name": "emailer_lib.quarto_json_to_intermediate_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/quarto_json_to_intermediate_email.html#emailer_lib.quarto_json_to_intermediate_email", "dispname": "-"}, {"name": "emailer_lib.ingress.quarto_json_to_intermediate_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/quarto_json_to_intermediate_email.html#emailer_lib.quarto_json_to_intermediate_email", "dispname": "emailer_lib.quarto_json_to_intermediate_email"}, {"name": "emailer_lib.mjml_to_intermediate_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml_to_intermediate_email.html#emailer_lib.mjml_to_intermediate_email", "dispname": "-"}, {"name": "emailer_lib.ingress.mjml_to_intermediate_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml_to_intermediate_email.html#emailer_lib.mjml_to_intermediate_email", "dispname": "emailer_lib.mjml_to_intermediate_email"}, {"name": "emailer_lib.redmail_to_intermediate_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/redmail_to_intermediate_email.html#emailer_lib.redmail_to_intermediate_email", "dispname": "-"}, {"name": "emailer_lib.ingress.redmail_to_intermediate_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/redmail_to_intermediate_email.html#emailer_lib.redmail_to_intermediate_email", "dispname": "emailer_lib.redmail_to_intermediate_email"}, {"name": "emailer_lib.yagmail_to_intermediate_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/yagmail_to_intermediate_email.html#emailer_lib.yagmail_to_intermediate_email", "dispname": "-"}, {"name": "emailer_lib.ingress.yagmail_to_intermediate_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/yagmail_to_intermediate_email.html#emailer_lib.yagmail_to_intermediate_email", "dispname": "emailer_lib.yagmail_to_intermediate_email"}, {"name": "emailer_lib.send_intermediate_email_with_gmail", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_intermediate_email_with_gmail.html#emailer_lib.send_intermediate_email_with_gmail", "dispname": "-"}, {"name": "emailer_lib.egress.send_intermediate_email_with_gmail", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_intermediate_email_with_gmail.html#emailer_lib.send_intermediate_email_with_gmail", "dispname": "emailer_lib.send_intermediate_email_with_gmail"}, {"name": "emailer_lib.send_intermediate_email_with_smtp", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_intermediate_email_with_smtp.html#emailer_lib.send_intermediate_email_with_smtp", "dispname": "-"}, {"name": "emailer_lib.egress.send_intermediate_email_with_smtp", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_intermediate_email_with_smtp.html#emailer_lib.send_intermediate_email_with_smtp", "dispname": "emailer_lib.send_intermediate_email_with_smtp"}, {"name": "emailer_lib.send_intermediate_email_with_redmail", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_intermediate_email_with_redmail.html#emailer_lib.send_intermediate_email_with_redmail", "dispname": "-"}, {"name": "emailer_lib.egress.send_intermediate_email_with_redmail", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_intermediate_email_with_redmail.html#emailer_lib.send_intermediate_email_with_redmail", "dispname": "emailer_lib.send_intermediate_email_with_redmail"}, {"name": "emailer_lib.send_intermediate_email_with_yagmail", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_intermediate_email_with_yagmail.html#emailer_lib.send_intermediate_email_with_yagmail", "dispname": "-"}, {"name": "emailer_lib.egress.send_intermediate_email_with_yagmail", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_intermediate_email_with_yagmail.html#emailer_lib.send_intermediate_email_with_yagmail", "dispname": "emailer_lib.send_intermediate_email_with_yagmail"}, {"name": "emailer_lib.send_intermediate_email_with_mailgun", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_intermediate_email_with_mailgun.html#emailer_lib.send_intermediate_email_with_mailgun", "dispname": "-"}, {"name": "emailer_lib.egress.send_intermediate_email_with_mailgun", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_intermediate_email_with_mailgun.html#emailer_lib.send_intermediate_email_with_mailgun", "dispname": "emailer_lib.send_intermediate_email_with_mailgun"}, {"name": "emailer_lib.send_quarto_email_with_gmail", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_quarto_email_with_gmail.html#emailer_lib.send_quarto_email_with_gmail", "dispname": "-"}, {"name": "emailer_lib.egress.send_quarto_email_with_gmail", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_quarto_email_with_gmail.html#emailer_lib.send_quarto_email_with_gmail", "dispname": "emailer_lib.send_quarto_email_with_gmail"}, {"name": "emailer_lib.write_email_message_to_file", "domain": "py", "role": "function", "priority": "1", "uri": "reference/write_email_message_to_file.html#emailer_lib.write_email_message_to_file", "dispname": "-"}, {"name": "emailer_lib.utils.write_email_message_to_file", "domain": "py", "role": "function", "priority": "1", "uri": "reference/write_email_message_to_file.html#emailer_lib.write_email_message_to_file", "dispname": "emailer_lib.write_email_message_to_file"}, {"name": "emailer_lib.mjml.mjml", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.mjml.html#emailer_lib.mjml.mjml", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.mjml", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.mjml.html#emailer_lib.mjml.mjml", "dispname": "emailer_lib.mjml.mjml"}, {"name": "emailer_lib.mjml.head", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.head.html#emailer_lib.mjml.head", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.head", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.head.html#emailer_lib.mjml.head", "dispname": "emailer_lib.mjml.head"}, {"name": "emailer_lib.mjml.body", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.body.html#emailer_lib.mjml.body", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.body", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.body.html#emailer_lib.mjml.body", "dispname": "emailer_lib.mjml.body"}, {"name": "emailer_lib.mjml.mj_attributes", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.mj_attributes.html#emailer_lib.mjml.mj_attributes", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.mj_attributes", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.mj_attributes.html#emailer_lib.mjml.mj_attributes", "dispname": "emailer_lib.mjml.mj_attributes"}, {"name": "emailer_lib.mjml.mj_all", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.mj_all.html#emailer_lib.mjml.mj_all", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.mj_all", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.mj_all.html#emailer_lib.mjml.mj_all", "dispname": "emailer_lib.mjml.mj_all"}, {"name": "emailer_lib.mjml.mj_class", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.mj_class.html#emailer_lib.mjml.mj_class", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.mj_class", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.mj_class.html#emailer_lib.mjml.mj_class", "dispname": "emailer_lib.mjml.mj_class"}, {"name": "emailer_lib.mjml.breakpoint", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.breakpoint.html#emailer_lib.mjml.breakpoint", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.breakpoint", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.breakpoint.html#emailer_lib.mjml.breakpoint", "dispname": "emailer_lib.mjml.breakpoint"}, {"name": "emailer_lib.mjml.font", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.font.html#emailer_lib.mjml.font", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.font", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.font.html#emailer_lib.mjml.font", "dispname": "emailer_lib.mjml.font"}, {"name": "emailer_lib.mjml.html_attributes", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.html_attributes.html#emailer_lib.mjml.html_attributes", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.html_attributes", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.html_attributes.html#emailer_lib.mjml.html_attributes", "dispname": "emailer_lib.mjml.html_attributes"}, {"name": "emailer_lib.mjml.html_attribute", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.html_attribute.html#emailer_lib.mjml.html_attribute", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.html_attribute", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.html_attribute.html#emailer_lib.mjml.html_attribute", "dispname": "emailer_lib.mjml.html_attribute"}, {"name": "emailer_lib.mjml.preview", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.preview.html#emailer_lib.mjml.preview", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.preview", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.preview.html#emailer_lib.mjml.preview", "dispname": "emailer_lib.mjml.preview"}, {"name": "emailer_lib.mjml.style", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.style.html#emailer_lib.mjml.style", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.style", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.style.html#emailer_lib.mjml.style", "dispname": "emailer_lib.mjml.style"}, {"name": "emailer_lib.mjml.title", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.title.html#emailer_lib.mjml.title", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.title", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.title.html#emailer_lib.mjml.title", "dispname": "emailer_lib.mjml.title"}, {"name": "emailer_lib.mjml.accordion", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion.html#emailer_lib.mjml.accordion", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.accordion", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion.html#emailer_lib.mjml.accordion", "dispname": "emailer_lib.mjml.accordion"}, {"name": "emailer_lib.mjml.accordion_element", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_element.html#emailer_lib.mjml.accordion_element", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.accordion_element", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_element.html#emailer_lib.mjml.accordion_element", "dispname": "emailer_lib.mjml.accordion_element"}, {"name": "emailer_lib.mjml.accordion_text", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_text.html#emailer_lib.mjml.accordion_text", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.accordion_text", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_text.html#emailer_lib.mjml.accordion_text", "dispname": "emailer_lib.mjml.accordion_text"}, {"name": "emailer_lib.mjml.accordion_title", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_title.html#emailer_lib.mjml.accordion_title", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.accordion_title", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_title.html#emailer_lib.mjml.accordion_title", "dispname": "emailer_lib.mjml.accordion_title"}, {"name": "emailer_lib.mjml.button", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.button.html#emailer_lib.mjml.button", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.button", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.button.html#emailer_lib.mjml.button", "dispname": "emailer_lib.mjml.button"}, {"name": "emailer_lib.mjml.carousel", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.carousel.html#emailer_lib.mjml.carousel", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.carousel", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.carousel.html#emailer_lib.mjml.carousel", "dispname": "emailer_lib.mjml.carousel"}, {"name": "emailer_lib.mjml.carousel_image", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.carousel_image.html#emailer_lib.mjml.carousel_image", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.carousel_image", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.carousel_image.html#emailer_lib.mjml.carousel_image", "dispname": "emailer_lib.mjml.carousel_image"}, {"name": "emailer_lib.mjml.column", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.column.html#emailer_lib.mjml.column", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.column", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.column.html#emailer_lib.mjml.column", "dispname": "emailer_lib.mjml.column"}, {"name": "emailer_lib.mjml.divider", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.divider.html#emailer_lib.mjml.divider", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.divider", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.divider.html#emailer_lib.mjml.divider", "dispname": "emailer_lib.mjml.divider"}, {"name": "emailer_lib.mjml.group", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.group.html#emailer_lib.mjml.group", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.group", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.group.html#emailer_lib.mjml.group", "dispname": "emailer_lib.mjml.group"}, {"name": "emailer_lib.mjml.hero", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.hero.html#emailer_lib.mjml.hero", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.hero", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.hero.html#emailer_lib.mjml.hero", "dispname": "emailer_lib.mjml.hero"}, {"name": "emailer_lib.mjml.image", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.image.html#emailer_lib.mjml.image", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.image", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.image.html#emailer_lib.mjml.image", "dispname": "emailer_lib.mjml.image"}, {"name": "emailer_lib.mjml.navbar", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.navbar.html#emailer_lib.mjml.navbar", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.navbar", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.navbar.html#emailer_lib.mjml.navbar", "dispname": "emailer_lib.mjml.navbar"}, {"name": "emailer_lib.mjml.navbar_link", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.navbar_link.html#emailer_lib.mjml.navbar_link", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.navbar_link", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.navbar_link.html#emailer_lib.mjml.navbar_link", "dispname": "emailer_lib.mjml.navbar_link"}, {"name": "emailer_lib.mjml.raw", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.raw.html#emailer_lib.mjml.raw", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.raw", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.raw.html#emailer_lib.mjml.raw", "dispname": "emailer_lib.mjml.raw"}, {"name": "emailer_lib.mjml.section", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.section.html#emailer_lib.mjml.section", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.section", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.section.html#emailer_lib.mjml.section", "dispname": "emailer_lib.mjml.section"}, {"name": "emailer_lib.mjml.social", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.social.html#emailer_lib.mjml.social", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.social", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.social.html#emailer_lib.mjml.social", "dispname": "emailer_lib.mjml.social"}, {"name": "emailer_lib.mjml.social_element", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.social_element.html#emailer_lib.mjml.social_element", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.social_element", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.social_element.html#emailer_lib.mjml.social_element", "dispname": "emailer_lib.mjml.social_element"}, {"name": "emailer_lib.mjml.spacer", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.spacer.html#emailer_lib.mjml.spacer", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.spacer", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.spacer.html#emailer_lib.mjml.spacer", "dispname": "emailer_lib.mjml.spacer"}, {"name": "emailer_lib.mjml.table", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.table.html#emailer_lib.mjml.table", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.table", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.table.html#emailer_lib.mjml.table", "dispname": "emailer_lib.mjml.table"}, {"name": "emailer_lib.mjml.text", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.text.html#emailer_lib.mjml.text", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.text", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.text.html#emailer_lib.mjml.text", "dispname": "emailer_lib.mjml.text"}, {"name": "emailer_lib.mjml.wrapper", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.wrapper.html#emailer_lib.mjml.wrapper", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.wrapper", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.wrapper.html#emailer_lib.mjml.wrapper", "dispname": "emailer_lib.mjml.wrapper"}]} \ No newline at end of file +{"project": "nbmail", "version": "0.0.9999", "count": 106, "items": [{"name": "nbmail.Email.preview_send_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/nbmail.Email.preview_send_email.html#nbmail.Email.preview_send_email", "dispname": "-"}, {"name": "nbmail.structs.Email.preview_send_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/nbmail.Email.preview_send_email.html#nbmail.Email.preview_send_email", "dispname": "nbmail.Email.preview_send_email"}, {"name": "nbmail.Email.write_email_message", "domain": "py", "role": "function", "priority": "1", "uri": "reference/nbmail.Email.write_email_message.html#nbmail.Email.write_email_message", "dispname": "-"}, {"name": "nbmail.structs.Email.write_email_message", "domain": "py", "role": "function", "priority": "1", "uri": "reference/nbmail.Email.write_email_message.html#nbmail.Email.write_email_message", "dispname": "nbmail.Email.write_email_message"}, {"name": "nbmail.Email.write_preview_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/nbmail.Email.write_preview_email.html#nbmail.Email.write_preview_email", "dispname": "-"}, {"name": "nbmail.structs.Email.write_preview_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/nbmail.Email.write_preview_email.html#nbmail.Email.write_preview_email", "dispname": "nbmail.Email.write_preview_email"}, {"name": "nbmail.Email", "domain": "py", "role": "class", "priority": "1", "uri": "reference/Email.html#nbmail.Email", "dispname": "-"}, {"name": "nbmail.structs.Email", "domain": "py", "role": "class", "priority": "1", "uri": "reference/Email.html#nbmail.Email", "dispname": "nbmail.Email"}, {"name": "nbmail.Email.write_preview_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/Email.write_preview_email.html#nbmail.Email.write_preview_email", "dispname": "-"}, {"name": "nbmail.structs.Email.write_preview_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/Email.write_preview_email.html#nbmail.Email.write_preview_email", "dispname": "nbmail.Email.write_preview_email"}, {"name": "nbmail.Email.write_email_message", "domain": "py", "role": "function", "priority": "1", "uri": "reference/Email.write_email_message.html#nbmail.Email.write_email_message", "dispname": "-"}, {"name": "nbmail.structs.Email.write_email_message", "domain": "py", "role": "function", "priority": "1", "uri": "reference/Email.write_email_message.html#nbmail.Email.write_email_message", "dispname": "nbmail.Email.write_email_message"}, {"name": "nbmail.Email.preview_send_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/Email.preview_send_email.html#nbmail.Email.preview_send_email", "dispname": "-"}, {"name": "nbmail.structs.Email.preview_send_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/Email.preview_send_email.html#nbmail.Email.preview_send_email", "dispname": "nbmail.Email.preview_send_email"}, {"name": "nbmail.quarto_json_to_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/quarto_json_to_email.html#nbmail.quarto_json_to_email", "dispname": "-"}, {"name": "nbmail.ingress.quarto_json_to_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/quarto_json_to_email.html#nbmail.quarto_json_to_email", "dispname": "nbmail.quarto_json_to_email"}, {"name": "nbmail.mjml_to_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml_to_email.html#nbmail.mjml_to_email", "dispname": "-"}, {"name": "nbmail.ingress.mjml_to_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml_to_email.html#nbmail.mjml_to_email", "dispname": "nbmail.mjml_to_email"}, {"name": "nbmail.redmail_to_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/redmail_to_email.html#nbmail.redmail_to_email", "dispname": "-"}, {"name": "nbmail.ingress.redmail_to_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/redmail_to_email.html#nbmail.redmail_to_email", "dispname": "nbmail.redmail_to_email"}, {"name": "nbmail.yagmail_to_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/yagmail_to_email.html#nbmail.yagmail_to_email", "dispname": "-"}, {"name": "nbmail.ingress.yagmail_to_email", "domain": "py", "role": "function", "priority": "1", "uri": "reference/yagmail_to_email.html#nbmail.yagmail_to_email", "dispname": "nbmail.yagmail_to_email"}, {"name": "nbmail.send_email_with_gmail", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_email_with_gmail.html#nbmail.send_email_with_gmail", "dispname": "-"}, {"name": "nbmail.egress.send_email_with_gmail", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_email_with_gmail.html#nbmail.send_email_with_gmail", "dispname": "nbmail.send_email_with_gmail"}, {"name": "nbmail.send_email_with_smtp", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_email_with_smtp.html#nbmail.send_email_with_smtp", "dispname": "-"}, {"name": "nbmail.egress.send_email_with_smtp", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_email_with_smtp.html#nbmail.send_email_with_smtp", "dispname": "nbmail.send_email_with_smtp"}, {"name": "nbmail.send_email_with_redmail", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_email_with_redmail.html#nbmail.send_email_with_redmail", "dispname": "-"}, {"name": "nbmail.egress.send_email_with_redmail", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_email_with_redmail.html#nbmail.send_email_with_redmail", "dispname": "nbmail.send_email_with_redmail"}, {"name": "nbmail.send_email_with_yagmail", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_email_with_yagmail.html#nbmail.send_email_with_yagmail", "dispname": "-"}, {"name": "nbmail.egress.send_email_with_yagmail", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_email_with_yagmail.html#nbmail.send_email_with_yagmail", "dispname": "nbmail.send_email_with_yagmail"}, {"name": "nbmail.send_email_with_mailgun", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_email_with_mailgun.html#nbmail.send_email_with_mailgun", "dispname": "-"}, {"name": "nbmail.egress.send_email_with_mailgun", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_email_with_mailgun.html#nbmail.send_email_with_mailgun", "dispname": "nbmail.send_email_with_mailgun"}, {"name": "nbmail.send_quarto_email_with_gmail", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_quarto_email_with_gmail.html#nbmail.send_quarto_email_with_gmail", "dispname": "-"}, {"name": "nbmail.egress.send_quarto_email_with_gmail", "domain": "py", "role": "function", "priority": "1", "uri": "reference/send_quarto_email_with_gmail.html#nbmail.send_quarto_email_with_gmail", "dispname": "nbmail.send_quarto_email_with_gmail"}, {"name": "nbmail.write_email_message_to_file", "domain": "py", "role": "function", "priority": "1", "uri": "reference/write_email_message_to_file.html#nbmail.write_email_message_to_file", "dispname": "-"}, {"name": "nbmail.utils.write_email_message_to_file", "domain": "py", "role": "function", "priority": "1", "uri": "reference/write_email_message_to_file.html#nbmail.write_email_message_to_file", "dispname": "nbmail.write_email_message_to_file"}, {"name": "nbmail.mjml.mjml", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.mjml.html#nbmail.mjml.mjml", "dispname": "-"}, {"name": "nbmail.mjml.tags.mjml", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.mjml.html#nbmail.mjml.mjml", "dispname": "nbmail.mjml.mjml"}, {"name": "nbmail.mjml.head", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.head.html#nbmail.mjml.head", "dispname": "-"}, {"name": "nbmail.mjml.tags.head", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.head.html#nbmail.mjml.head", "dispname": "nbmail.mjml.head"}, {"name": "nbmail.mjml.body", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.body.html#nbmail.mjml.body", "dispname": "-"}, {"name": "nbmail.mjml.tags.body", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.body.html#nbmail.mjml.body", "dispname": "nbmail.mjml.body"}, {"name": "nbmail.mjml.mj_attributes", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.mj_attributes.html#nbmail.mjml.mj_attributes", "dispname": "-"}, {"name": "nbmail.mjml.tags.mj_attributes", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.mj_attributes.html#nbmail.mjml.mj_attributes", "dispname": "nbmail.mjml.mj_attributes"}, {"name": "nbmail.mjml.mj_all", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.mj_all.html#nbmail.mjml.mj_all", "dispname": "-"}, {"name": "nbmail.mjml.tags.mj_all", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.mj_all.html#nbmail.mjml.mj_all", "dispname": "nbmail.mjml.mj_all"}, {"name": "nbmail.mjml.mj_class", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.mj_class.html#nbmail.mjml.mj_class", "dispname": "-"}, {"name": "nbmail.mjml.tags.mj_class", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.mj_class.html#nbmail.mjml.mj_class", "dispname": "nbmail.mjml.mj_class"}, {"name": "nbmail.mjml.breakpoint", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.breakpoint.html#nbmail.mjml.breakpoint", "dispname": "-"}, {"name": "nbmail.mjml.tags.breakpoint", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.breakpoint.html#nbmail.mjml.breakpoint", "dispname": "nbmail.mjml.breakpoint"}, {"name": "nbmail.mjml.font", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.font.html#nbmail.mjml.font", "dispname": "-"}, {"name": "nbmail.mjml.tags.font", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.font.html#nbmail.mjml.font", "dispname": "nbmail.mjml.font"}, {"name": "nbmail.mjml.html_attributes", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.html_attributes.html#nbmail.mjml.html_attributes", "dispname": "-"}, {"name": "nbmail.mjml.tags.html_attributes", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.html_attributes.html#nbmail.mjml.html_attributes", "dispname": "nbmail.mjml.html_attributes"}, {"name": "nbmail.mjml.html_attribute", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.html_attribute.html#nbmail.mjml.html_attribute", "dispname": "-"}, {"name": "nbmail.mjml.tags.html_attribute", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.html_attribute.html#nbmail.mjml.html_attribute", "dispname": "nbmail.mjml.html_attribute"}, {"name": "nbmail.mjml.preview", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.preview.html#nbmail.mjml.preview", "dispname": "-"}, {"name": "nbmail.mjml.tags.preview", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.preview.html#nbmail.mjml.preview", "dispname": "nbmail.mjml.preview"}, {"name": "nbmail.mjml.style", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.style.html#nbmail.mjml.style", "dispname": "-"}, {"name": "nbmail.mjml.tags.style", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.style.html#nbmail.mjml.style", "dispname": "nbmail.mjml.style"}, {"name": "nbmail.mjml.title", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.title.html#nbmail.mjml.title", "dispname": "-"}, {"name": "nbmail.mjml.tags.title", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.title.html#nbmail.mjml.title", "dispname": "nbmail.mjml.title"}, {"name": "nbmail.mjml.accordion", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion.html#nbmail.mjml.accordion", "dispname": "-"}, {"name": "nbmail.mjml.tags.accordion", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion.html#nbmail.mjml.accordion", "dispname": "nbmail.mjml.accordion"}, {"name": "nbmail.mjml.accordion_element", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_element.html#nbmail.mjml.accordion_element", "dispname": "-"}, {"name": "nbmail.mjml.tags.accordion_element", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_element.html#nbmail.mjml.accordion_element", "dispname": "nbmail.mjml.accordion_element"}, {"name": "nbmail.mjml.accordion_text", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_text.html#nbmail.mjml.accordion_text", "dispname": "-"}, {"name": "nbmail.mjml.tags.accordion_text", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_text.html#nbmail.mjml.accordion_text", "dispname": "nbmail.mjml.accordion_text"}, {"name": "nbmail.mjml.accordion_title", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_title.html#nbmail.mjml.accordion_title", "dispname": "-"}, {"name": "nbmail.mjml.tags.accordion_title", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_title.html#nbmail.mjml.accordion_title", "dispname": "nbmail.mjml.accordion_title"}, {"name": "nbmail.mjml.button", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.button.html#nbmail.mjml.button", "dispname": "-"}, {"name": "nbmail.mjml.tags.button", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.button.html#nbmail.mjml.button", "dispname": "nbmail.mjml.button"}, {"name": "nbmail.mjml.carousel", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.carousel.html#nbmail.mjml.carousel", "dispname": "-"}, {"name": "nbmail.mjml.tags.carousel", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.carousel.html#nbmail.mjml.carousel", "dispname": "nbmail.mjml.carousel"}, {"name": "nbmail.mjml.carousel_image", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.carousel_image.html#nbmail.mjml.carousel_image", "dispname": "-"}, {"name": "nbmail.mjml.tags.carousel_image", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.carousel_image.html#nbmail.mjml.carousel_image", "dispname": "nbmail.mjml.carousel_image"}, {"name": "nbmail.mjml.column", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.column.html#nbmail.mjml.column", "dispname": "-"}, {"name": "nbmail.mjml.tags.column", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.column.html#nbmail.mjml.column", "dispname": "nbmail.mjml.column"}, {"name": "nbmail.mjml.divider", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.divider.html#nbmail.mjml.divider", "dispname": "-"}, {"name": "nbmail.mjml.tags.divider", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.divider.html#nbmail.mjml.divider", "dispname": "nbmail.mjml.divider"}, {"name": "nbmail.mjml.group", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.group.html#nbmail.mjml.group", "dispname": "-"}, {"name": "nbmail.mjml.tags.group", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.group.html#nbmail.mjml.group", "dispname": "nbmail.mjml.group"}, {"name": "nbmail.mjml.hero", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.hero.html#nbmail.mjml.hero", "dispname": "-"}, {"name": "nbmail.mjml.tags.hero", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.hero.html#nbmail.mjml.hero", "dispname": "nbmail.mjml.hero"}, {"name": "nbmail.mjml.image", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.image.html#nbmail.mjml.image", "dispname": "-"}, {"name": "nbmail.mjml.tags.image", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.image.html#nbmail.mjml.image", "dispname": "nbmail.mjml.image"}, {"name": "nbmail.mjml.navbar", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.navbar.html#nbmail.mjml.navbar", "dispname": "-"}, {"name": "nbmail.mjml.tags.navbar", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.navbar.html#nbmail.mjml.navbar", "dispname": "nbmail.mjml.navbar"}, {"name": "nbmail.mjml.navbar_link", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.navbar_link.html#nbmail.mjml.navbar_link", "dispname": "-"}, {"name": "nbmail.mjml.tags.navbar_link", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.navbar_link.html#nbmail.mjml.navbar_link", "dispname": "nbmail.mjml.navbar_link"}, {"name": "nbmail.mjml.raw", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.raw.html#nbmail.mjml.raw", "dispname": "-"}, {"name": "nbmail.mjml.tags.raw", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.raw.html#nbmail.mjml.raw", "dispname": "nbmail.mjml.raw"}, {"name": "nbmail.mjml.section", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.section.html#nbmail.mjml.section", "dispname": "-"}, {"name": "nbmail.mjml.tags.section", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.section.html#nbmail.mjml.section", "dispname": "nbmail.mjml.section"}, {"name": "nbmail.mjml.social", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.social.html#nbmail.mjml.social", "dispname": "-"}, {"name": "nbmail.mjml.tags.social", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.social.html#nbmail.mjml.social", "dispname": "nbmail.mjml.social"}, {"name": "nbmail.mjml.social_element", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.social_element.html#nbmail.mjml.social_element", "dispname": "-"}, {"name": "nbmail.mjml.tags.social_element", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.social_element.html#nbmail.mjml.social_element", "dispname": "nbmail.mjml.social_element"}, {"name": "nbmail.mjml.spacer", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.spacer.html#nbmail.mjml.spacer", "dispname": "-"}, {"name": "nbmail.mjml.tags.spacer", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.spacer.html#nbmail.mjml.spacer", "dispname": "nbmail.mjml.spacer"}, {"name": "nbmail.mjml.table", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.table.html#nbmail.mjml.table", "dispname": "-"}, {"name": "nbmail.mjml.tags.table", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.table.html#nbmail.mjml.table", "dispname": "nbmail.mjml.table"}, {"name": "nbmail.mjml.text", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.text.html#nbmail.mjml.text", "dispname": "-"}, {"name": "nbmail.mjml.tags.text", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.text.html#nbmail.mjml.text", "dispname": "nbmail.mjml.text"}, {"name": "nbmail.mjml.wrapper", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.wrapper.html#nbmail.mjml.wrapper", "dispname": "-"}, {"name": "nbmail.mjml.tags.wrapper", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.wrapper.html#nbmail.mjml.wrapper", "dispname": "nbmail.mjml.wrapper"}]} diff --git a/docs/summary.quarto_ipynb b/docs/summary.quarto_ipynb new file mode 100644 index 0000000..d12aa5d --- /dev/null +++ b/docs/summary.quarto_ipynb @@ -0,0 +1,251 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# The whole game\n", + "\n", + "\n", + "\n", + "Emailing reports is a critical but challenging task for data science.\n", + "Mainly because you have to figure out generating the email content, configuring pieces like attachments, and orchestrating it (e.g. testing, or sending on a schedule). Moreover, content can range from simple layouts to more complex ones.\n", + "\n", + "In this tutorial, we'll walk through the whole game of sending email. We'll start with this simple example:\n" + ], + "id": "a59ad40b" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# | code-fold: true\n", + "# | eval: false\n", + "import os\n", + "from dotenv import load_dotenv\n", + "from data_polars import sp500\n", + "import redmail\n", + "\n", + "load_dotenv()\n", + "gmail_address = os.environ[\"GMAIL_ADDRESS\"]\n", + "gmail_app_password = os.environ[\"GMAIL_APP_PASSWORD\"]\n", + "\n", + "\n", + "email_subject = \"Report on Cars\"\n", + "email_body = sp500.head(10).style.as_raw_html(inline_css=True)\n", + "\n", + "# This is here to emphasize the sender does not have to be the same as the receiver\n", + "email_receiver = gmail_address\n", + "\n", + "redmail.gmail.username = gmail_address\n", + "redmail.gmail.password = gmail_app_password\n", + "\n", + "redmail.gmail.send(\n", + " subject=email_subject,\n", + " receivers=[email_receiver],\n", + " html=email_body,\n", + ")" + ], + "id": "1e722fc3", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](./assets/whole-game-email-annotated.png){width=50% fig-align=\"center\"}\n", + "\n", + "* **Content**: writing the text of the email, including plots and tables.\n", + "* **Composing**: setting up the subject, sender, and receivers.\n", + "* **Orchestrating**: previewing, testing, and scheduling the email.\n", + "\n", + "We'll also quickly review writing more advanced content layouts, and authoring email reports that involve running code with Quarto.\n", + "\n", + "## A simple email\n", + "\n", + "![](./assets/whole-game-email.png){width=50% fig-align=\"center\"}\n", + "\n", + "* Generate and preview\n", + "* Authenticate (may need to refer to its own authentication page in guide)\n", + "* Send\n" + ], + "id": "466f45ac" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# | code-fold: true\n", + "# | eval: false\n", + "import os\n", + "from dotenv import load_dotenv\n", + "from data_polars import sp500\n", + "import redmail\n", + "\n", + "load_dotenv()\n", + "gmail_address = os.environ[\"GMAIL_ADDRESS\"]\n", + "gmail_app_password = os.environ[\"GMAIL_APP_PASSWORD\"]\n", + "\n", + "\n", + "email_subject = \"Report on Cars\"\n", + "email_body = sp500.head(10).style.as_raw_html(inline_css=True)\n", + "\n", + "# This is here to emphasize the sender does not have to be the same as the receiver\n", + "email_receiver = gmail_address\n", + "\n", + "redmail.gmail.username = gmail_address\n", + "redmail.gmail.password = gmail_app_password\n", + "\n", + "redmail.gmail.send(\n", + " subject=email_subject,\n", + " receivers=[email_receiver],\n", + " html=email_body,\n", + ")" + ], + "id": "d7f3dc30", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configure: subject, recipients, attachments\n", + "\n", + "* you could attach the data as a CSV attachment\n", + "\n", + "\n", + "## Orchestrate: save and preview\n", + "\n", + "* previewing email\n", + "* intermediate json, easy for sending email later\n", + "* embedding images makes previewing hard\n", + "* can always email to yourself (or use a test service like Litmus)\n", + "\n", + "## Content: Quarto authoring\n", + "\n", + "Here's our same simple email generated using quarto.\n", + "\n", + "\n", + "\n", + "![](./assets/whole-game-quarto.png){width=50% fig-align=\"center\"}\n", + "\n", + "* Focused on basic configuring, and content\n", + "* Sending happens via our tool\n", + "* Generate using quarto render\n", + "* Can preview email\n", + "\n", + "## Content: advanced layouts\n", + "\n", + "We'll highlight the key pieces (discussed later in this guide) to go from that simple email, to a more advanced on like below:\n", + "\n", + "![](./assets/whole-game-fancy.png){width=50% fig-align=\"center\"}\n", + "\n", + "\n", + "## Fridge\n", + "\n", + "In this tutorial, we are going to send an email from a Gmail account. To do so, you will need to [create an App Password](https://support.google.com/accounts/answer/185833). Note this is only possible if you've [enabled 2-step verification](https://support.google.com/accounts/answer/185839).\n", + "\n", + "\n", + "\n", + "::: {.callout-tip}\n", + "This is just one of many options: it is also possible to send emails in Python from other email providers (Outlook, ProtonMail, etc.), or even from a custom domain. To skip ahead to a discussion of alternative sending methods, see [Authentication](orchestrating-auth.qmd)\n", + ":::\n", + "\n", + "Once you've created your App Password, that is used as your Gmail password for sending with Python. \n", + "\n", + "There are many ways to store the password seperate from your email-sending code, so as to not expose any sensitive information. One such approach uses a `.env` file, and the ``dotenv` and `os` packages.\n", + "\n", + "```{.sh filename=\".env\"}\n", + "GMAIL_APP_PASSWORD=abcd abcd abcd abcd\n", + "```\n", + "\n", + "```{.python filename=\"main.py\"}\n", + "import os\n", + "from dotenv import load_dotenv\n", + "\n", + "load_dotenv()\n", + "\n", + "your_gmail_address = \"YourGmail@gmail.com\"\n", + "gmail_app_password = os.environ[\"GMAIL_APP_PASSWORD\"]\n", + "```\n", + "\n", + "Check out the email content we will send.\n" + ], + "id": "a0472e78" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from data_polars import sp500\n", + "\n", + "sp500.head(10).style" + ], + "id": "b0858409", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And now we send the email!\n" + ], + "id": "e696c7a5" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# | eval: false\n", + "import redmail\n", + "\n", + "redmail.gmail.username = your_gmail_address\n", + "redmail.gmail.password = gmail_app_password\n", + "\n", + "redmail.gmail.send(\n", + " subject=\"An Example Email\",\n", + " receivers=[username],\n", + " html=email_html,\n", + " text=email_plaintext,\n", + ")" + ], + "id": "9fbe68e8", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ], + "id": "39984027" + } + ], + "metadata": { + "kernelspec": { + "name": "python3", + "language": "python", + "display_name": "Python 3 (ipykernel)", + "path": "/Users/jules-wg/Library/Python/3.9/share/jupyter/kernels/python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/emailer_lib/__init__.py b/emailer_lib/__init__.py deleted file mode 100644 index dc25a87..0000000 --- a/emailer_lib/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -from .ingress import ( - redmail_to_intermediate_email, - yagmail_to_intermediate_email, - mjml_to_intermediate_email, - quarto_json_to_intermediate_email, -) - -from .egress import ( - send_quarto_email_with_gmail, - send_intermediate_email_with_gmail, - send_intermediate_email_with_redmail, - send_intermediate_email_with_yagmail, - send_intermediate_email_with_mailgun, - send_intermediate_email_with_smtp, -) - -from .utils import write_email_message_to_file - -from .structs import IntermediateEmail - - -__all__ = [ - "quarto_json_to_intermediate_email", - "IntermediateEmail", - "redmail_to_intermediate_email", - "yagmail_to_intermediate_email", - "mjml_to_intermediate_email", - "send_quarto_email_with_gmail", - "send_intermediate_email_with_gmail", - "send_intermediate_email_with_redmail", - "send_intermediate_email_with_yagmail", - "send_intermediate_email_with_mailgun", - "send_intermediate_email_with_smtp", - "write_email_message_to_file", -] diff --git a/examples/.gitignore b/examples/.gitignore index bd2bfd4..a131850 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -1,2 +1,2 @@ playground.qmd -emailer_lib.qmd \ No newline at end of file +nbmail.qmd diff --git a/examples/emailer_lib.qmd b/examples/emailer_lib.qmd new file mode 100644 index 0000000..e69de29 diff --git a/nbmail/__init__.py b/nbmail/__init__.py new file mode 100644 index 0000000..8c933b1 --- /dev/null +++ b/nbmail/__init__.py @@ -0,0 +1,35 @@ +from .ingress import ( + redmail_to_email, + yagmail_to_email, + mjml_to_email, + quarto_json_to_email, +) + +from .egress import ( + send_quarto_email_with_gmail, + send_email_with_gmail, + send_email_with_redmail, + send_email_with_yagmail, + send_email_with_mailgun, + send_email_with_smtp, +) + +from .utils import write_email_message_to_file + +from .structs import Email + + +__all__ = [ + "quarto_json_to_email", + "Email", + "redmail_to_email", + "yagmail_to_email", + "mjml_to_email", + "send_quarto_email_with_gmail", + "send_email_with_gmail", + "send_email_with_redmail", + "send_email_with_yagmail", + "send_email_with_mailgun", + "send_email_with_smtp", + "write_email_message_to_file", +] diff --git a/emailer_lib/egress.py b/nbmail/egress.py similarity index 83% rename from emailer_lib/egress.py rename to nbmail/egress.py index 8ce18aa..ba8ec70 100644 --- a/emailer_lib/egress.py +++ b/nbmail/egress.py @@ -12,18 +12,18 @@ from email import encoders from typing import Literal -from .ingress import quarto_json_to_intermediate_email +from .ingress import quarto_json_to_email -from .structs import IntermediateEmail +from .structs import Email import warnings __all__ = [ "send_quarto_email_with_gmail", - "send_intermediate_email_with_gmail", - "send_intermediate_email_with_redmail", - "send_intermediate_email_with_yagmail", - "send_intermediate_email_with_mailgun", - "send_intermediate_email_with_smtp", + "send_email_with_gmail", + "send_email_with_redmail", + "send_email_with_yagmail", + "send_email_with_mailgun", + "send_email_with_smtp", ] @@ -68,9 +68,9 @@ def send_quarto_email_with_gmail( ) ``` """ - i_email: IntermediateEmail = quarto_json_to_intermediate_email(json_path) + i_email: Email = quarto_json_to_email(json_path) i_email.recipients = recipients - send_intermediate_email_with_gmail( + send_email_with_gmail( username=username, password=password, i_email=i_email ) @@ -79,11 +79,11 @@ def send_quarto_email_with_gmail( # Could also take creds object -def send_intermediate_email_with_gmail( - username: str, password: str, i_email: IntermediateEmail +def send_email_with_gmail( + username: str, password: str, i_email: Email ): """ - Send an Intermediate Email object via Gmail. + Send an Email object via Gmail. Parameters ---------- @@ -94,7 +94,7 @@ def send_intermediate_email_with_gmail( Gmail app password i_email - IntermediateEmail object containing the email content and attachments + Email object containing the email content and attachments Returns ------- @@ -104,17 +104,17 @@ def send_intermediate_email_with_gmail( Examples -------- ```python - email = IntermediateEmail( + email = Email( html="

Hello world

", subject="Test Email", recipients=["user@example.com"], ) - send_intermediate_email_with_gmail("user@gmail.com", "password123", email) + send_email_with_gmail("user@gmail.com", "password123", email) ``` """ # Compose the email - return send_intermediate_email_with_smtp( + return send_email_with_smtp( smtp_host="smtp.gmail.com", smtp_port=587, username=username, @@ -124,14 +124,14 @@ def send_intermediate_email_with_gmail( ) -def send_intermediate_email_with_redmail(i_email: IntermediateEmail): +def send_email_with_redmail(i_email: Email): """ - Send an Intermediate Email object via Redmail. + Send an Email object via Redmail. Parameters ---------- i_email - IntermediateEmail object containing the email content and attachments + Email object containing the email content and attachments Returns ------- @@ -144,14 +144,14 @@ def send_intermediate_email_with_redmail(i_email: IntermediateEmail): raise NotImplementedError -def send_intermediate_email_with_yagmail(i_email: IntermediateEmail): +def send_email_with_yagmail(i_email: Email): """ - Send an Intermediate Email object via Yagmail. + Send an Email object via Yagmail. Parameters ---------- i_email - IntermediateEmail object containing the email content and attachments + Email object containing the email content and attachments Returns ------- @@ -165,14 +165,14 @@ def send_intermediate_email_with_yagmail(i_email: IntermediateEmail): raise NotImplementedError -def send_intermediate_email_with_mailgun( +def send_email_with_mailgun( api_key: str, domain: str, sender: str, - i_email: IntermediateEmail, + i_email: Email, ): """ - Send an Intermediate Email object via Mailgun. + Send an Email object via Mailgun. Parameters ---------- @@ -183,7 +183,7 @@ def send_intermediate_email_with_mailgun( sender Email address to send from (must be authorized in your domain) i_email - IntermediateEmail object containing the email content and attachments + Email object containing the email content and attachments Returns ------- @@ -198,13 +198,13 @@ def send_intermediate_email_with_mailgun( Examples -------- ```python - email = IntermediateEmail( + email = Email( html="

Hello world

", subject="Test Email", recipients=["user@example.com"], ) - response = send_intermediate_email_with_mailgun( + response = send_email_with_mailgun( api_key="your-api-key", domain="mg.yourdomain.com", sender="noreply@yourdomain.com", @@ -265,16 +265,16 @@ def send_intermediate_email_with_mailgun( return response -def send_intermediate_email_with_smtp( +def send_email_with_smtp( smtp_host: str, smtp_port: int, username: str, password: str, - i_email: IntermediateEmail, + i_email: Email, security: str = Literal["tls", "ssl", "smtp"], ): """ - Send an Intermediate Email object via SMTP. + Send an Email object via SMTP. Parameters ---------- @@ -291,7 +291,7 @@ def send_intermediate_email_with_smtp( SMTP account password i_email - IntermediateEmail object containing the email content and attachments + Email object containing the email content and attachments security Security protocol to use: "tls" (STARTTLS), "ssl" (SSL/TLS), or "smtp" (plain SMTP). @@ -310,14 +310,14 @@ def send_intermediate_email_with_smtp( Examples -------- ```python - email = IntermediateEmail( + email = Email( html="

Hello world

", subject="Test Email", recipients=["user@example.com"], ) # TLS connection (port 587) - recommended - send_intermediate_email_with_smtp( + send_email_with_smtp( "smtp.example.com", 587, "user@example.com", @@ -327,7 +327,7 @@ def send_intermediate_email_with_smtp( ) # SSL connection (port 465) - send_intermediate_email_with_smtp( + send_email_with_smtp( "smtp.example.com", 465, "user@example.com", @@ -337,7 +337,7 @@ def send_intermediate_email_with_smtp( ) # Plain SMTP (port 25) - insecure, for testing only - send_intermediate_email_with_smtp( + send_email_with_smtp( "127.0.0.1", 8025, "test@example.com", diff --git a/emailer_lib/ingress.py b/nbmail/ingress.py similarity index 81% rename from emailer_lib/ingress.py rename to nbmail/ingress.py index e92e09a..005686e 100644 --- a/emailer_lib/ingress.py +++ b/nbmail/ingress.py @@ -5,36 +5,36 @@ from email.message import EmailMessage from mjml import mjml2html -from .structs import IntermediateEmail +from .structs import Email from .mjml import MJMLTag from .mjml.image_processor import _process_mjml_images import warnings __all__ = [ - "redmail_to_intermediate_email", - "yagmail_to_intermediate_email", - "mjml_to_intermediate_email", - "quarto_json_to_intermediate_email", + "redmail_to_email", + "yagmail_to_email", + "mjml_to_email", + "quarto_json_to_email", ] -def redmail_to_intermediate_email(msg: EmailMessage) -> IntermediateEmail: +def redmail_to_email(msg: EmailMessage) -> Email: """ - Convert a Redmail EmailMessage object to an IntermediateEmail + Convert a Redmail EmailMessage object to an Email Params ------ msg The Redmail-generated EmailMessage object - Converts the input EmailMessage to the intermediate email structure + Converts the input EmailMessage to the email structure """ - return _email_message_to_intermediate_email(msg) + return _email_message_to_email(msg) -def yagmail_to_intermediate_email(): +def yagmail_to_email(): """ - Convert a Yagmail email object to an IntermediateEmail + Convert a Yagmail email object to an Email Params ------ @@ -45,11 +45,11 @@ def yagmail_to_intermediate_email(): pass -def mjml_to_intermediate_email( +def mjml_to_email( mjml_content: str | MJMLTag, -) -> IntermediateEmail: +) -> Email: """ - Convert MJML markup to an IntermediateEmail + Convert MJML markup to an Email Parameters ---------- @@ -58,12 +58,14 @@ def mjml_to_intermediate_email( Returns ------ - An Intermediate Email object + An Email object """ # Handle MJMLTag objects by preprocessing images if isinstance(mjml_content, MJMLTag): - processed_mjml, inline_attachments = _process_mjml_images(mjml_content) + # Wrap in proper MJML structure if needed + wrapped_mjml = mjml_content._wrap_in_mjml_tag(emit_warning=True) + processed_mjml, inline_attachments = _process_mjml_images(wrapped_mjml) mjml_markup = processed_mjml._to_mjml() else: # String-based MJML, no preprocessing needed @@ -73,7 +75,7 @@ def mjml_to_intermediate_email( email_content = mjml2html(mjml_markup) - i_email = IntermediateEmail( + i_email = Email( html=email_content, subject="", rsc_email_supress_report_attachment=False, @@ -85,9 +87,9 @@ def mjml_to_intermediate_email( # useful because redmail bundles an email message... may help in other cases too -def _email_message_to_intermediate_email(msg: EmailMessage) -> IntermediateEmail: +def _email_message_to_email(msg: EmailMessage) -> Email: """ - Convert a Python EmailMessage object to an IntermediateEmail + Convert a Python EmailMessage object to an Email Parameters ------ @@ -147,7 +149,7 @@ def _email_message_to_intermediate_email(msg: EmailMessage) -> IntermediateEmail # Not certain that all attached files have associated filenames external_attachments.append(filename) - return IntermediateEmail( + return Email( html=html or "", subject=subject, external_attachments=external_attachments if external_attachments else None, @@ -158,9 +160,9 @@ def _email_message_to_intermediate_email(msg: EmailMessage) -> IntermediateEmail # Helper method to parse the quarto JSON -def quarto_json_to_intermediate_email(path: str) -> IntermediateEmail: +def quarto_json_to_email(path: str) -> Email: """ - Convert a Quarto output metadata JSON file to an IntermediateEmail + Convert a Quarto output metadata JSON file to an Email Parameters ------ @@ -190,7 +192,7 @@ def quarto_json_to_intermediate_email(path: str) -> IntermediateEmail: ) supress_scheduled = metadata.get("rsc_email_supress_scheduled", False) - i_email = IntermediateEmail( + i_email = Email( html=email_html, text=email_text, inline_attachments=email_images, diff --git a/emailer_lib/mjml/.gitignore b/nbmail/mjml/.gitignore similarity index 100% rename from emailer_lib/mjml/.gitignore rename to nbmail/mjml/.gitignore diff --git a/emailer_lib/mjml/README.md b/nbmail/mjml/README.md similarity index 91% rename from emailer_lib/mjml/README.md rename to nbmail/mjml/README.md index 16efad3..2b3c624 100644 --- a/emailer_lib/mjml/README.md +++ b/nbmail/mjml/README.md @@ -14,16 +14,16 @@ This module provides Python functions for creating MJML markup, the responsive e ## Installation -This module is part of the `emailer_lib` package: +This module is part of the `nbmail` package: ```python -from emailer_lib import mjml as mj +from nbmail import mjml as mj ``` ## Quick Start ```python -from emailer_lib.mjml import mjml, body, section, column, text +from nbmail.mjml import mjml, body, section, column, text # Build an MJML email structure email = mjml( @@ -99,7 +99,7 @@ button( The base class for all MJML elements. Can be instantiated directly or via helper functions. ```python -from emailer_lib.mjml import MJMLTag +from nbmail.mjml import MJMLTag tag = MJMLTag( "mj-text", @@ -117,7 +117,7 @@ A dictionary type for tag attributes. ### Simple Email ```python -from emailer_lib.mjml import mjml, head, body, section, column, text, title +from nbmail.mjml import mjml, head, body, section, column, text, title email = mjml( head( @@ -136,7 +136,7 @@ email = mjml( ### Multi-column Layout ```python -from emailer_lib.mjml import body, section, column, text, image +from nbmail.mjml import body, section, column, text, image layout = body( section( @@ -157,7 +157,7 @@ layout = body( ### Using Attributes ```python -from emailer_lib.mjml import section, column, text +from nbmail.mjml import section, column, text # Attributes as kwargs section( diff --git a/emailer_lib/mjml/__init__.py b/nbmail/mjml/__init__.py similarity index 94% rename from emailer_lib/mjml/__init__.py rename to nbmail/mjml/__init__.py index ea3b2f1..3720161 100644 --- a/emailer_lib/mjml/__init__.py +++ b/nbmail/mjml/__init__.py @@ -39,7 +39,7 @@ navbar_link, social_element, ) -# _process_mjml_images is called internally by mjml_to_intermediate_email +# _process_mjml_images is called internally by mjml_to_email __all__ = ( "MJMLTag", diff --git a/emailer_lib/mjml/_core.py b/nbmail/mjml/_core.py similarity index 81% rename from emailer_lib/mjml/_core.py rename to nbmail/mjml/_core.py index d55913e..9331639 100644 --- a/emailer_lib/mjml/_core.py +++ b/nbmail/mjml/_core.py @@ -132,7 +132,7 @@ def _to_mjml(self, indent: int = 0, eol: str = "\n") -> str: Ported from htmltools Tag rendering logic. Note: BytesIO/bytes in image src attributes are not supported by _to_mjml(). - Pass the MJMLTag directly to mjml_to_intermediate_email() instead. + Pass the MJMLTag directly to mjml_to_email() instead. """ def _flatten(children): @@ -150,8 +150,8 @@ def _flatten(children): if isinstance(src_value, (bytes, BytesIO)): raise ValueError( "Cannot render MJML with BytesIO/bytes in image src attribute. " - "Pass the MJMLTag object directly to mjml_to_intermediate_email() instead of calling _to_mjml() first. " - "Example: i_email = mjml_to_intermediate_email(doc)" + "Pass the MJMLTag object directly to mjml_to_email() instead of calling _to_mjml() first. " + "Example: i_email = mjml_to_email(doc)" ) # Build attribute string @@ -181,8 +181,8 @@ def _flatten(children): return f"{pad}<{self.tagName}{attr_str}>" def _repr_html_(self): - from ..ingress import mjml_to_intermediate_email - return mjml_to_intermediate_email(self)._repr_html_() + from ..ingress import mjml_to_email + return mjml_to_email(self)._repr_html_() # TODO: make something deliberate def __repr__(self) -> str: @@ -193,7 +193,47 @@ def __repr__(self) -> str: ) return f"" - # warning explain that they are not to pass this to intermediate email + def _wrap_in_mjml_tag(self, emit_warning: bool = True) -> "MJMLTag": + """ + Wrap this tag in proper MJML structure if needed. + + If this is already a complete MJML document ( tag), return it as-is. + If this is an tag, wrap it in an tag. + Otherwise, wrap it in .... + + Parameters + ---------- + emit_warning + Whether to emit a warning when wrapping is performed + + Returns + ------- + MJMLTag + A complete MJML document (with root tag) + """ + if self.tagName == "mjml": + # Already a complete MJML document + return self + elif self.tagName == "mj-body": + # Wrap only in mjml tag + if emit_warning: + warnings.warn( + "Automatically wrapping in .... " + "For full control, create a complete MJML document with the mjml() tag.", + UserWarning, + ) + return MJMLTag("mjml", self) + else: + # Wrap in mjml and mj-body + if emit_warning: + warnings.warn( + "Automatically wrapping in .... " + "For full control, create a complete MJML document with the mjml() tag.", + UserWarning, + ) + return MJMLTag("mjml", MJMLTag("mj-body", self)) + + # warning explain that they are not to pass this to email def to_html(self, **mjml2html_kwargs) -> str: """ Render MJMLTag to HTML using mjml2html. @@ -202,7 +242,7 @@ def to_html(self, **mjml2html_kwargs) -> str: in ... with a warning. Note: This method embeds all images as inline data URIs in the HTML. - For email composition with inline attachments, use mjml_to_intermediate_email() instead. + For email composition with inline attachments, use mjml_to_email() instead. Parameters ---------- @@ -214,31 +254,6 @@ def to_html(self, **mjml2html_kwargs) -> str: str Result from `mjml-python.mjml2html()` containing html content """ - if self.tagName == "mjml": - # Already a complete MJML document - mjml_markup = self._to_mjml() - elif self.tagName == "mj-body": - # Wrap only in mjml tag - warnings.warn( - "to_html() called on tag. " - "Automatically wrapping in .... " - "For full control, create a complete MJML document with the mjml() tag.", - UserWarning, - stacklevel=2, - ) - wrapped = MJMLTag("mjml", self) - mjml_markup = wrapped._to_mjml() - else: - # Warn and wrap in mjml/mj-body - warnings.warn( - f"to_html() called on <{self.tagName}> tag. " - "Automatically wrapping in .... " - "For full control, create a complete MJML document with the mjml() tag.", - UserWarning, - stacklevel=2, - ) - # Wrap in mjml and mj-body - wrapped = MJMLTag("mjml", MJMLTag("mj-body", self)) - mjml_markup = wrapped._to_mjml() - + wrapped = self._wrap_in_mjml_tag(emit_warning=True) + mjml_markup = wrapped._to_mjml() return mjml2html(mjml_markup, **mjml2html_kwargs) diff --git a/emailer_lib/mjml/image_processor.py b/nbmail/mjml/image_processor.py similarity index 93% rename from emailer_lib/mjml/image_processor.py rename to nbmail/mjml/image_processor.py index 02c61ed..483705f 100644 --- a/emailer_lib/mjml/image_processor.py +++ b/nbmail/mjml/image_processor.py @@ -52,7 +52,7 @@ def _process_mjml_images(mjml_tag: MJMLTag) -> Tuple[MJMLTag, Dict[str, str]]: Extract inline attachments from MJML tree and convert bytes/BytesIO to CID references. This is a private function. Users should not call it directly. - It is called automatically by mjml_to_intermediate_email(). + It is called automatically by mjml_to_email(). This function recursively walks through the MJML tag tree and finds mj-image tags with BytesIO or bytes in their src attribute. It converts these to CID references @@ -73,8 +73,8 @@ def _process_mjml_images(mjml_tag: MJMLTag) -> Tuple[MJMLTag, Dict[str, str]]: Examples -------- ```python - from emailer_lib.mjml import mjml, body, section, column, image - from emailer_lib import mjml_to_intermediate_email + from nbmail.mjml import mjml, body, section, column, image + from nbmail import mjml_to_email from io import BytesIO # Create BytesIO with image data @@ -95,8 +95,8 @@ def _process_mjml_images(mjml_tag: MJMLTag) -> Tuple[MJMLTag, Dict[str, str]]: ) ) - # Pass directly to mjml_to_intermediate_email (calls _process_mjml_images internally) - i_email = mjml_to_intermediate_email(email) + # Pass directly to mjml_to_email (calls _process_mjml_images internally) + i_email = mjml_to_email(email) # Result: i_email.inline_attachments = {"plot_1.png": "iVBORw0KGgo..."} ``` diff --git a/emailer_lib/mjml/scripts/generate_tags.py b/nbmail/mjml/scripts/generate_tags.py similarity index 96% rename from emailer_lib/mjml/scripts/generate_tags.py rename to nbmail/mjml/scripts/generate_tags.py index d84b7f0..86fae9a 100644 --- a/emailer_lib/mjml/scripts/generate_tags.py +++ b/nbmail/mjml/scripts/generate_tags.py @@ -115,7 +115,7 @@ def {py_name}( -------- With children: ```{{python}} - from emailer_lib.mjml import {py_name}, text + from nbmail.mjml import {py_name}, text child = text("Hello World") result = {py_name}(child) @@ -165,7 +165,7 @@ def {py_name}( -------- With content: ```{{python}} - from emailer_lib.mjml import {py_name} + from nbmail.mjml import {py_name} result = {py_name}("Hello") ``` @@ -182,7 +182,7 @@ def {py_name}( # Combine all parts output = header + "\n".join(functions) - # Write to file - navigate from scripts/ to emailer_lib/mjml/tags.py + # Write to file - navigate from scripts/ to nbmail/mjml/tags.py script_dir = Path(__file__).parent tags_file = script_dir.parent / "tags.py" diff --git a/emailer_lib/mjml/tags.py b/nbmail/mjml/tags.py similarity index 94% rename from emailer_lib/mjml/tags.py rename to nbmail/mjml/tags.py index 1cdbf1c..3589fc7 100644 --- a/emailer_lib/mjml/tags.py +++ b/nbmail/mjml/tags.py @@ -35,7 +35,7 @@ def mjml( -------- With children: ```{python} - from emailer_lib.mjml import mjml, text + from nbmail.mjml import mjml, text child = text("Hello World") result = mjml(child) @@ -80,7 +80,7 @@ def head( -------- With children: ```{python} - from emailer_lib.mjml import head, text + from nbmail.mjml import head, text child = text("Hello World") result = head(child) @@ -125,7 +125,7 @@ def body( -------- With children: ```{python} - from emailer_lib.mjml import body, text + from nbmail.mjml import body, text child = text("Hello World") result = body(child) @@ -170,7 +170,7 @@ def mj_attributes( -------- With children: ```{python} - from emailer_lib.mjml import mj_attributes, text + from nbmail.mjml import mj_attributes, text child = text("Hello World") result = mj_attributes(child) @@ -215,7 +215,7 @@ def mj_all( -------- With children: ```{python} - from emailer_lib.mjml import mj_all, text + from nbmail.mjml import mj_all, text child = text("Hello World") result = mj_all(child) @@ -260,7 +260,7 @@ def mj_class( -------- With children: ```{python} - from emailer_lib.mjml import mj_class, text + from nbmail.mjml import mj_class, text child = text("Hello World") result = mj_class(child) @@ -305,7 +305,7 @@ def breakpoint( -------- With children: ```{python} - from emailer_lib.mjml import breakpoint, text + from nbmail.mjml import breakpoint, text child = text("Hello World") result = breakpoint(child) @@ -350,7 +350,7 @@ def font( -------- With children: ```{python} - from emailer_lib.mjml import font, text + from nbmail.mjml import font, text child = text("Hello World") result = font(child) @@ -395,7 +395,7 @@ def html_attributes( -------- With children: ```{python} - from emailer_lib.mjml import html_attributes, text + from nbmail.mjml import html_attributes, text child = text("Hello World") result = html_attributes(child) @@ -440,7 +440,7 @@ def html_attribute( -------- With children: ```{python} - from emailer_lib.mjml import html_attribute, text + from nbmail.mjml import html_attribute, text child = text("Hello World") result = html_attribute(child) @@ -485,7 +485,7 @@ def preview( -------- With children: ```{python} - from emailer_lib.mjml import preview, text + from nbmail.mjml import preview, text child = text("Hello World") result = preview(child) @@ -530,7 +530,7 @@ def style( -------- With children: ```{python} - from emailer_lib.mjml import style, text + from nbmail.mjml import style, text child = text("Hello World") result = style(child) @@ -575,7 +575,7 @@ def accordion( -------- With children: ```{python} - from emailer_lib.mjml import accordion, text + from nbmail.mjml import accordion, text child = text("Hello World") result = accordion(child) @@ -620,7 +620,7 @@ def accordion_element( -------- With children: ```{python} - from emailer_lib.mjml import accordion_element, text + from nbmail.mjml import accordion_element, text child = text("Hello World") result = accordion_element(child) @@ -665,7 +665,7 @@ def carousel( -------- With children: ```{python} - from emailer_lib.mjml import carousel, text + from nbmail.mjml import carousel, text child = text("Hello World") result = carousel(child) @@ -710,7 +710,7 @@ def column( -------- With children: ```{python} - from emailer_lib.mjml import column, text + from nbmail.mjml import column, text child = text("Hello World") result = column(child) @@ -755,7 +755,7 @@ def divider( -------- With children: ```{python} - from emailer_lib.mjml import divider, text + from nbmail.mjml import divider, text child = text("Hello World") result = divider(child) @@ -800,7 +800,7 @@ def group( -------- With children: ```{python} - from emailer_lib.mjml import group, text + from nbmail.mjml import group, text child = text("Hello World") result = group(child) @@ -845,7 +845,7 @@ def hero( -------- With children: ```{python} - from emailer_lib.mjml import hero, text + from nbmail.mjml import hero, text child = text("Hello World") result = hero(child) @@ -890,7 +890,7 @@ def image( -------- With children: ```{python} - from emailer_lib.mjml import image, text + from nbmail.mjml import image, text child = text("Hello World") result = image(child) @@ -935,7 +935,7 @@ def navbar( -------- With children: ```{python} - from emailer_lib.mjml import navbar, text + from nbmail.mjml import navbar, text child = text("Hello World") result = navbar(child) @@ -980,7 +980,7 @@ def section( -------- With children: ```{python} - from emailer_lib.mjml import section, text + from nbmail.mjml import section, text child = text("Hello World") result = section(child) @@ -1025,7 +1025,7 @@ def social( -------- With children: ```{python} - from emailer_lib.mjml import social, text + from nbmail.mjml import social, text child = text("Hello World") result = social(child) @@ -1070,7 +1070,7 @@ def spacer( -------- With children: ```{python} - from emailer_lib.mjml import spacer, text + from nbmail.mjml import spacer, text child = text("Hello World") result = spacer(child) @@ -1115,7 +1115,7 @@ def wrapper( -------- With children: ```{python} - from emailer_lib.mjml import wrapper, text + from nbmail.mjml import wrapper, text child = text("Hello World") result = wrapper(child) @@ -1159,7 +1159,7 @@ def accordion_text( -------- With content: ```{python} - from emailer_lib.mjml import accordion_text + from nbmail.mjml import accordion_text result = accordion_text("Hello") ``` @@ -1197,7 +1197,7 @@ def accordion_title( -------- With content: ```{python} - from emailer_lib.mjml import accordion_title + from nbmail.mjml import accordion_title result = accordion_title("Hello") ``` @@ -1235,7 +1235,7 @@ def button( -------- With content: ```{python} - from emailer_lib.mjml import button + from nbmail.mjml import button result = button("Hello") ``` @@ -1273,7 +1273,7 @@ def carousel_image( -------- With content: ```{python} - from emailer_lib.mjml import carousel_image + from nbmail.mjml import carousel_image result = carousel_image("Hello") ``` @@ -1311,7 +1311,7 @@ def navbar_link( -------- With content: ```{python} - from emailer_lib.mjml import navbar_link + from nbmail.mjml import navbar_link result = navbar_link("Hello") ``` @@ -1349,7 +1349,7 @@ def raw( -------- With content: ```{python} - from emailer_lib.mjml import raw + from nbmail.mjml import raw result = raw("Hello") ``` @@ -1387,7 +1387,7 @@ def social_element( -------- With content: ```{python} - from emailer_lib.mjml import social_element + from nbmail.mjml import social_element result = social_element("Hello") ``` @@ -1425,7 +1425,7 @@ def table( -------- With content: ```{python} - from emailer_lib.mjml import table + from nbmail.mjml import table result = table("Hello") ``` @@ -1463,7 +1463,7 @@ def text( -------- With content: ```{python} - from emailer_lib.mjml import text + from nbmail.mjml import text result = text("Hello") ``` @@ -1501,7 +1501,7 @@ def title( -------- With content: ```{python} - from emailer_lib.mjml import title + from nbmail.mjml import title result = title("Hello") ``` diff --git a/emailer_lib/mjml/tests/test_core.py b/nbmail/mjml/tests/test_core.py similarity index 95% rename from emailer_lib/mjml/tests/test_core.py rename to nbmail/mjml/tests/test_core.py index 6069c15..21dbf38 100644 --- a/emailer_lib/mjml/tests/test_core.py +++ b/nbmail/mjml/tests/test_core.py @@ -1,7 +1,7 @@ import pytest from io import BytesIO -from emailer_lib.ingress import mjml_to_intermediate_email -from emailer_lib.mjml._core import MJMLTag, TagAttrDict +from nbmail.ingress import mjml_to_email +from nbmail.mjml._core import MJMLTag, TagAttrDict def test_accepts_dict_arguments(): @@ -122,14 +122,14 @@ def test_to_html_warns_and_wraps_other_tags(): assert "html" in html_result -def test_repr_html_returns_intermediate_email_repr_html(): +def test_repr_html_returns_email_repr_html(): tag = MJMLTag("mjml", MJMLTag("mj-body")) html_from_repr = tag._repr_html_() - # _repr_html_() should return the HTML representation from mjml_to_intermediate_email + # _repr_html_() should return the HTML representation from mjml_to_email assert "Hello world

", subject="Test Email", recipients=["user@example.com"], @@ -123,7 +123,7 @@ def _repr_html_(self) -> str: """ Return HTML representation with inline attachments for rich display. - This method enables rich display of the IntermediateEmail in Jupyter notebooks + This method enables rich display of the Email in Jupyter notebooks and other IPython-compatible environments. It converts cid: references to base64 data URIs so the email can be previewed directly in the notebook. @@ -136,7 +136,7 @@ def _repr_html_(self) -> str: -------- ```python # In a Jupyter notebook, simply display the email object: - email = IntermediateEmail( + email = Email( html='

Hello

', subject="Test Email", inline_attachments={"img1.png": "iVBORw0KGgo..."} @@ -188,10 +188,10 @@ def write_preview_email(self, out_file: str = "preview_email.html") -> None: def write_email_message(self) -> EmailMessage: """ - Convert the IntermediateEmail to a Python EmailMessage. + Convert the Email to a Python EmailMessage. This method creates a standard library EmailMessage object from the - IntermediateEmail, including HTML, plain text, recipients, and attachments. + Email, including HTML, plain text, recipients, and attachments. Returns ------- diff --git a/emailer_lib/tests/__snapshots__/test_structs.ambr b/nbmail/tests/__snapshots__/test_structs.ambr similarity index 100% rename from emailer_lib/tests/__snapshots__/test_structs.ambr rename to nbmail/tests/__snapshots__/test_structs.ambr diff --git a/emailer_lib/tests/test_egress.py b/nbmail/tests/test_egress.py similarity index 79% rename from emailer_lib/tests/test_egress.py rename to nbmail/tests/test_egress.py index e8abc65..fb98b13 100644 --- a/emailer_lib/tests/test_egress.py +++ b/nbmail/tests/test_egress.py @@ -2,19 +2,19 @@ import pytest -from emailer_lib.egress import ( - send_intermediate_email_with_redmail, - send_intermediate_email_with_yagmail, - send_intermediate_email_with_mailgun, - send_intermediate_email_with_smtp, - send_intermediate_email_with_gmail, +from nbmail.egress import ( + send_email_with_redmail, + send_email_with_yagmail, + send_email_with_mailgun, + send_email_with_smtp, + send_email_with_gmail, send_quarto_email_with_gmail, ) -from emailer_lib.structs import IntermediateEmail +from nbmail.structs import Email def make_basic_email(): - return IntermediateEmail( + return Email( html="

Hi

", subject="Test", recipients=["a@example.com"], @@ -31,22 +31,22 @@ def setup_smtp_mocks(monkeypatch): context = mock_smtp.return_value.__enter__.return_value - # Patch in the emailer_lib.egress module where they're used - monkeypatch.setattr("emailer_lib.egress.smtplib.SMTP", mock_smtp) - monkeypatch.setattr("emailer_lib.egress.smtplib.SMTP_SSL", mock_smtp_ssl) + # Patch in the nbmail.egress module where they're used + monkeypatch.setattr("nbmail.egress.smtplib.SMTP", mock_smtp) + monkeypatch.setattr("nbmail.egress.smtplib.SMTP_SSL", mock_smtp_ssl) return mock_smtp, mock_smtp_ssl, context -def test_send_intermediate_email_with_gmail_calls_smtp(monkeypatch): +def test_send_email_with_gmail_calls_smtp(monkeypatch): email = make_basic_email() mock_smtp_send = MagicMock() monkeypatch.setattr( - "emailer_lib.egress.send_intermediate_email_with_smtp", mock_smtp_send + "nbmail.egress.send_email_with_smtp", mock_smtp_send ) - send_intermediate_email_with_gmail("user@gmail.com", "pass", email) + send_email_with_gmail("user@gmail.com", "pass", email) mock_smtp_send.assert_called_once_with( smtp_host="smtp.gmail.com", @@ -58,11 +58,11 @@ def test_send_intermediate_email_with_gmail_calls_smtp(monkeypatch): ) -def test_send_intermediate_email_with_smtp_tls(monkeypatch): +def test_send_email_with_smtp_tls(monkeypatch): email = make_basic_email() mock_smtp, mock_smtp_ssl, context = setup_smtp_mocks(monkeypatch) - send_intermediate_email_with_smtp( + send_email_with_smtp( smtp_host="smtp.example.com", smtp_port=587, username="user", @@ -77,11 +77,11 @@ def test_send_intermediate_email_with_smtp_tls(monkeypatch): context.sendmail.assert_called_once() -def test_send_intermediate_email_with_smtp_ssl(monkeypatch): +def test_send_email_with_smtp_ssl(monkeypatch): email = make_basic_email() mock_smtp, mock_smtp_ssl, context = setup_smtp_mocks(monkeypatch) - send_intermediate_email_with_smtp( + send_email_with_smtp( smtp_host="smtp.example.com", smtp_port=465, username="user", @@ -95,14 +95,14 @@ def test_send_intermediate_email_with_smtp_ssl(monkeypatch): context.sendmail.assert_called_once() -def test_send_intermediate_email_with_smtp_with_attachment(monkeypatch): +def test_send_email_with_smtp_with_attachment(monkeypatch): email = make_basic_email() email.external_attachments = ["file.txt"] mock_smtp, mock_smtp_ssl, context = setup_smtp_mocks(monkeypatch) with patch("builtins.open", mock_open(read_data=b"data")): - send_intermediate_email_with_smtp( + send_email_with_smtp( smtp_host="smtp.example.com", smtp_port=587, username="user", @@ -117,14 +117,14 @@ def test_send_intermediate_email_with_smtp_with_attachment(monkeypatch): assert 'Content-Disposition: attachment; filename="file.txt"' in email_message -def test_send_intermediate_email_with_smtp_unknown_mime_type(monkeypatch): +def test_send_email_with_smtp_unknown_mime_type(monkeypatch): email = make_basic_email() email.external_attachments = ["file_without_extension"] mock_smtp, mock_smtp_ssl, context = setup_smtp_mocks(monkeypatch) with patch("builtins.open", mock_open(read_data=b"data")): - send_intermediate_email_with_smtp( + send_email_with_smtp( smtp_host="smtp.example.com", smtp_port=587, username="user", @@ -144,11 +144,11 @@ def test_send_intermediate_email_with_smtp_unknown_mime_type(monkeypatch): ) -def test_send_intermediate_email_with_smtp_sendmail_args(monkeypatch): +def test_send_email_with_smtp_sendmail_args(monkeypatch): email = make_basic_email() mock_smtp, mock_smtp_ssl, context = setup_smtp_mocks(monkeypatch) - send_intermediate_email_with_smtp( + send_email_with_smtp( smtp_host="mock_host", smtp_port=465, username="user@gmail.com", @@ -175,16 +175,16 @@ def test_send_intermediate_email_with_smtp_sendmail_args(monkeypatch): # this is probably not the best way to test this, # for what it's worth I will test each part separately def test_send_quarto_email_with_gmail(monkeypatch): - # Mock the quarto_json_to_intermediate_email function + # Mock the quarto_json_to_email function mock_quarto_to_email = MagicMock(return_value=make_basic_email()) monkeypatch.setattr( - "emailer_lib.egress.quarto_json_to_intermediate_email", mock_quarto_to_email + "nbmail.egress.quarto_json_to_email", mock_quarto_to_email ) # Mock the Gmail sending function mock_send_gmail = MagicMock() monkeypatch.setattr( - "emailer_lib.egress.send_intermediate_email_with_gmail", mock_send_gmail + "nbmail.egress.send_email_with_gmail", mock_send_gmail ) # Call the function @@ -203,7 +203,7 @@ def test_send_quarto_email_with_gmail(monkeypatch): assert i_email.recipients == ["recipient@example.com"] -def test_send_intermediate_email_with_mailgun(monkeypatch): +def test_send_email_with_mailgun(monkeypatch): email = make_basic_email() email.external_attachments = ["file.txt"] @@ -225,7 +225,7 @@ def test_send_intermediate_email_with_mailgun(monkeypatch): with patch("mailgun.client.Client", mock_client_class): with patch("builtins.open", mock_open(read_data=b"file content")): - response = send_intermediate_email_with_mailgun( + response = send_email_with_mailgun( api_key="test-api-key", domain="mg.example.com", sender="sender@example.com", @@ -259,15 +259,15 @@ def test_send_intermediate_email_with_mailgun(monkeypatch): } -def test_send_intermediate_email_with_mailgun_no_recipients(): - email = IntermediateEmail( +def test_send_email_with_mailgun_no_recipients(): + email = Email( html="

Hi

", subject="Test", recipients=None, ) with pytest.raises(TypeError, match="i_email must have a populated recipients attribute"): - send_intermediate_email_with_mailgun( + send_email_with_mailgun( api_key="test-api-key", domain="mg.example.com", sender="sender@example.com", @@ -278,8 +278,8 @@ def test_send_intermediate_email_with_mailgun_no_recipients(): @pytest.mark.parametrize( "send_func", [ - send_intermediate_email_with_redmail, - send_intermediate_email_with_yagmail, + send_email_with_redmail, + send_email_with_yagmail, ], ) def test_not_implemented_functions(send_func): diff --git a/emailer_lib/tests/test_end_to_end.py b/nbmail/tests/test_end_to_end.py similarity index 90% rename from emailer_lib/tests/test_end_to_end.py rename to nbmail/tests/test_end_to_end.py index 7c01906..29a6175 100644 --- a/emailer_lib/tests/test_end_to_end.py +++ b/nbmail/tests/test_end_to_end.py @@ -2,8 +2,8 @@ import time from aiosmtpd.controller import Controller from email import message_from_bytes -from emailer_lib.egress import send_intermediate_email_with_smtp -from emailer_lib.structs import IntermediateEmail +from nbmail.egress import send_email_with_smtp +from nbmail.structs import Email class EmailHandler: @@ -37,7 +37,7 @@ def smtp_server(): def test_send_email_integration(smtp_server): controller, handler = smtp_server - email = IntermediateEmail( + email = Email( html="

Test Email

Hello World

", subject="Integration Test", recipients=["test@example.com"], @@ -47,7 +47,7 @@ def test_send_email_integration(smtp_server): ) # Send email to the test SMTP server - send_intermediate_email_with_smtp( + send_email_with_smtp( smtp_host="127.0.0.1", smtp_port=8025, username="test@example.com", @@ -87,4 +87,4 @@ def test_send_email_integration(smtp_server): inline_images = [p for p in msg.walk() if p.get('Content-ID')] assert len(inline_images) == 1 - assert '' in inline_images[0].get('Content-ID') \ No newline at end of file + assert '' in inline_images[0].get('Content-ID') diff --git a/emailer_lib/tests/test_ingress.py b/nbmail/tests/test_ingress.py similarity index 73% rename from emailer_lib/tests/test_ingress.py rename to nbmail/tests/test_ingress.py index b66abfd..148c4cd 100644 --- a/emailer_lib/tests/test_ingress.py +++ b/nbmail/tests/test_ingress.py @@ -3,24 +3,24 @@ from email.message import EmailMessage from base64 import b64encode -from emailer_lib.ingress import ( - redmail_to_intermediate_email, - yagmail_to_intermediate_email, - mjml_to_intermediate_email, - quarto_json_to_intermediate_email, - _email_message_to_intermediate_email, +from nbmail.ingress import ( + redmail_to_email, + yagmail_to_email, + mjml_to_email, + quarto_json_to_email, + _email_message_to_email, ) -from emailer_lib.structs import IntermediateEmail +from nbmail.structs import Email -def test_email_message_to_intermediate_email_simple(): +def test_email_message_to_email_simple(): msg = EmailMessage() msg["Subject"] = "Test Subject" msg["To"] = "recipient@example.com" msg.set_content("Plain text") msg.add_alternative("

HTML content

", subtype="html") - result = _email_message_to_intermediate_email(msg) + result = _email_message_to_email(msg) assert result.subject == "Test Subject" assert result.recipients == ["recipient@example.com"] @@ -30,7 +30,7 @@ def test_email_message_to_intermediate_email_simple(): assert result.external_attachments is None -def test_email_message_to_intermediate_email_multiple_recipients(): +def test_email_message_to_email_multiple_recipients(): msg = EmailMessage() msg["Subject"] = "Multi-recipient" msg["To"] = "to1@example.com, to2@example.com" @@ -38,7 +38,7 @@ def test_email_message_to_intermediate_email_multiple_recipients(): msg["Bcc"] = "bcc@example.com" msg.add_alternative("Test", subtype="html") - result = _email_message_to_intermediate_email(msg) + result = _email_message_to_email(msg) assert len(result.recipients) == 4 assert "to1@example.com" in result.recipients @@ -47,7 +47,7 @@ def test_email_message_to_intermediate_email_multiple_recipients(): assert "bcc@example.com" in result.recipients -def test_email_message_to_intermediate_email_with_inline_image(): +def test_email_message_to_email_with_inline_image(): msg = EmailMessage() msg["Subject"] = "With Image" msg.add_alternative("", subtype="html") @@ -56,65 +56,65 @@ def test_email_message_to_intermediate_email_with_inline_image(): img_data = b"\x89PNG\r\n\x1a\n" msg.add_attachment(img_data, maintype="image", subtype="png", cid="img1") - result = _email_message_to_intermediate_email(msg) + result = _email_message_to_email(msg) assert result.inline_attachments is not None assert "img1" in result.inline_attachments assert result.inline_attachments["img1"] == b64encode(img_data).decode("utf-8") -def test_email_message_to_intermediate_email_with_external_attachment(): +def test_email_message_to_email_with_external_attachment(): msg = EmailMessage() msg["Subject"] = "With Attachment" msg.add_alternative("Content", subtype="html") msg.add_attachment(b"file content", maintype="application", subtype="pdf", filename="document.pdf") - result = _email_message_to_intermediate_email(msg) + result = _email_message_to_email(msg) assert result.external_attachments is not None assert "document.pdf" in result.external_attachments -def test_email_message_to_intermediate_email_plain_text_only(): +def test_email_message_to_email_plain_text_only(): msg = EmailMessage() msg["Subject"] = "Plain Only" msg.set_content("Just plain text") - result = _email_message_to_intermediate_email(msg) + result = _email_message_to_email(msg) assert result.text == "Just plain text\n" assert result.html == "" # Empty string when no HTML -def test_email_message_to_intermediate_email_html_only_not_multipart(): +def test_email_message_to_email_html_only_not_multipart(): msg = EmailMessage() msg["Subject"] = "HTML Only" msg.set_content("HTML", subtype="html") - result = _email_message_to_intermediate_email(msg) + result = _email_message_to_email(msg) assert result.html == "HTML\n" assert result.text is None -def test_redmail_to_intermediate_email(): +def test_redmail_to_email(): msg = EmailMessage() msg["Subject"] = "Redmail Test" msg.add_alternative("Redmail content", subtype="html") - result = redmail_to_intermediate_email(msg) + result = redmail_to_email(msg) - assert isinstance(result, IntermediateEmail) + assert isinstance(result, Email) assert result.subject == "Redmail Test" assert "Redmail content" in result.html -def test_yagmail_to_intermediate_email_not_implemented(): - result = yagmail_to_intermediate_email() +def test_yagmail_to_email_not_implemented(): + result = yagmail_to_email() assert result is None -def test_mjml_to_intermediate_email_no_images(): +def test_mjml_to_email_no_images(): mjml_content = """ @@ -127,15 +127,15 @@ def test_mjml_to_intermediate_email_no_images(): """ - result = mjml_to_intermediate_email(mjml_content) + result = mjml_to_email(mjml_content) - assert isinstance(result, IntermediateEmail) + assert isinstance(result, Email) assert "Hello World" in result.html assert result.subject == "" assert result.inline_attachments == {} -def test_mjml_to_intermediate_email_with_string_url(): +def test_mjml_to_email_with_string_url(): mjml_content = """ @@ -148,16 +148,16 @@ def test_mjml_to_intermediate_email_with_string_url(): """ - result = mjml_to_intermediate_email(mjml_content) + result = mjml_to_email(mjml_content) - assert isinstance(result, IntermediateEmail) + assert isinstance(result, Email) assert result.inline_attachments == {} assert "https://example.com/image.jpg" in result.html -def test_mjml_to_intermediate_email_with_bytesio(): +def test_mjml_to_email_with_bytesio(): from io import BytesIO - from emailer_lib.mjml import mjml, body, section, column, image + from nbmail.mjml import mjml, body, section, column, image buf = BytesIO(b'\x89PNG\r\n\x1a\n') @@ -175,9 +175,9 @@ def test_mjml_to_intermediate_email_with_bytesio(): ) ) - result = mjml_to_intermediate_email(mjml_tag) + result = mjml_to_email(mjml_tag) - assert isinstance(result, IntermediateEmail) + assert isinstance(result, Email) assert len(result.inline_attachments) == 1 cid_filename = list(result.inline_attachments.keys())[0] @@ -188,7 +188,7 @@ def test_mjml_to_intermediate_email_with_bytesio(): def test_mjml_to_mjml_with_bytesio_raises_error(): from io import BytesIO - from emailer_lib.mjml import mjml, body, section, column, image + from nbmail.mjml import mjml, body, section, column, image # Create a simple BytesIO object with fake image data buf = BytesIO(b'\x89PNG\r\n\x1a\n') @@ -211,13 +211,13 @@ def test_mjml_to_mjml_with_bytesio_raises_error(): with pytest.raises(ValueError, match="Cannot render MJML with BytesIO/bytes"): mjml_tag._to_mjml() - # But passing the tag directly to mjml_to_intermediate_email should work - result = mjml_to_intermediate_email(mjml_tag) - assert isinstance(result, IntermediateEmail) + # But passing the tag directly to mjml_to_email should work + result = mjml_to_email(mjml_tag) + assert isinstance(result, Email) assert len(result.inline_attachments) == 1 -def test_quarto_json_to_intermediate_email_basic(tmp_path): +def test_quarto_json_to_email_basic(tmp_path): json_data = { "rsc_email_body_html": "

Quarto email

", "rsc_email_subject": "Quarto Test", @@ -233,7 +233,7 @@ def test_quarto_json_to_intermediate_email_basic(tmp_path): with open(json_file, "w") as f: json.dump(json_data, f) - result = quarto_json_to_intermediate_email(str(json_file)) + result = quarto_json_to_email(str(json_file)) assert result.subject == "Quarto Test" assert "

Quarto email

" in result.html @@ -244,7 +244,7 @@ def test_quarto_json_to_intermediate_email_basic(tmp_path): assert result.rsc_email_supress_scheduled is False -def test_quarto_json_to_intermediate_email_minimal(tmp_path): +def test_quarto_json_to_email_minimal(tmp_path): json_data = { "rsc_email_body_html": "Minimal", "rsc_email_subject": "Minimal Subject", @@ -254,7 +254,7 @@ def test_quarto_json_to_intermediate_email_minimal(tmp_path): with open(json_file, "w") as f: json.dump(json_data, f) - result = quarto_json_to_intermediate_email(str(json_file)) + result = quarto_json_to_email(str(json_file)) assert result.subject == "Minimal Subject" assert result.html == "Minimal" @@ -265,7 +265,7 @@ def test_quarto_json_to_intermediate_email_minimal(tmp_path): assert result.rsc_email_supress_scheduled is False -def test_quarto_json_to_intermediate_email_empty_lists(tmp_path): +def test_quarto_json_to_email_empty_lists(tmp_path): """Test handling empty lists for attachments and images.""" json_data = { "rsc_email_body_html": "Test", @@ -279,7 +279,7 @@ def test_quarto_json_to_intermediate_email_empty_lists(tmp_path): with open(json_file, "w") as f: json.dump(json_data, f) - result = quarto_json_to_intermediate_email(str(json_file)) + result = quarto_json_to_email(str(json_file)) assert result.external_attachments == [] assert result.inline_attachments == {} diff --git a/emailer_lib/tests/test_structs.py b/nbmail/tests/test_structs.py similarity index 94% rename from emailer_lib/tests/test_structs.py rename to nbmail/tests/test_structs.py index a5876f7..60d9847 100644 --- a/emailer_lib/tests/test_structs.py +++ b/nbmail/tests/test_structs.py @@ -1,11 +1,11 @@ import re import pytest -from emailer_lib.structs import IntermediateEmail +from nbmail.structs import Email def test_creation_with_text_and_attachments(): - email = IntermediateEmail( + email = Email( html="

Hi

", subject="With Text and Attachments", text="Plain text version", @@ -21,7 +21,7 @@ def test_creation_with_text_and_attachments(): def test_creation_without_text_and_attachments(): - email = IntermediateEmail( + email = Email( html="

Hi

", subject="No Text or Attachments", ) @@ -34,7 +34,7 @@ def test_creation_without_text_and_attachments(): def test_subject_inserts_after_body(tmp_path): html = "

Hello!

" - email = IntermediateEmail( + email = Email( html=html, subject="Test Subject", rsc_email_supress_report_attachment=False, @@ -55,7 +55,7 @@ def test_subject_inserts_after_body(tmp_path): def test_subject_prepends_if_no_body(tmp_path): html = "

Hello!

" - email = IntermediateEmail( + email = Email( html=html, subject="NoBody", ) @@ -68,7 +68,7 @@ def test_subject_prepends_if_no_body(tmp_path): def test_raises_on_external_attachments(tmp_path): html = "

Test

" - email = IntermediateEmail( + email = Email( html=html, subject="Test", external_attachments=["file.txt"], @@ -88,7 +88,7 @@ def test_raises_on_external_attachments(tmp_path): ) def test_not_implemented_methods(method_name): """Test that unimplemented methods raise NotImplementedError.""" - email = IntermediateEmail( + email = Email( html="

Hi

", subject="Test", ) @@ -99,7 +99,7 @@ def test_not_implemented_methods(method_name): def test_preview_email_simple_html(tmp_path, snapshot): html = "

Hello World!

" - email = IntermediateEmail( + email = Email( html=html, subject="Simple Test Email", ) @@ -120,7 +120,7 @@ def test_preview_email_with_inline_attachments(tmp_path, snapshot): Banner """ - email = IntermediateEmail( + email = Email( html=html, subject="Email with Inline Images", inline_attachments={ @@ -163,7 +163,7 @@ def test_preview_email_complex_html(tmp_path, snapshot): """ - email = IntermediateEmail( + email = Email( html=html, subject="Complex Email Structure", inline_attachments={ diff --git a/emailer_lib/tests/test_utils.py b/nbmail/tests/test_utils.py similarity index 99% rename from emailer_lib/tests/test_utils.py rename to nbmail/tests/test_utils.py index bf81779..66edb92 100644 --- a/emailer_lib/tests/test_utils.py +++ b/nbmail/tests/test_utils.py @@ -2,7 +2,7 @@ from email.message import EmailMessage import re -from emailer_lib.utils import ( +from nbmail.utils import ( write_email_message_to_file, _add_base_64_to_inline_attachments, ) diff --git a/emailer_lib/utils.py b/nbmail/utils.py similarity index 100% rename from emailer_lib/utils.py rename to nbmail/utils.py diff --git a/pyproject.toml b/pyproject.toml index 916e82a..096c7c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,11 +6,11 @@ build-backend = "setuptools.build_meta" [tool.pytest.ini_options] minversion = "6.0" -addopts = "-ra --cov=emailer_lib --cov-report=term-missing" -testpaths = ["emailer_lib/**/tests"] +addopts = "-ra --cov=nbmail --cov-report=term-missing" +testpaths = ["nbmail/**/tests"] [project] -name = "emailer-lib" +name = "nbmail" version = "0.0.1" description = "Email serialization and sending utilities" authors = [{name = "Jules Walzer-Goldfeld"}] @@ -55,7 +55,7 @@ docs = [ exclude_also = [ "if TYPE_CHECKING:" ] -include = ["emailer_lib/*"] +include = ["nbmail/*"] omit = [ - "emailer_lib/tests/*" + "nbmail/tests/*" ] diff --git a/uv.lock b/uv.lock index ea5a350..14764be 100644 --- a/uv.lock +++ b/uv.lock @@ -1042,7 +1042,7 @@ wheels = [ ] [[package]] -name = "emailer-lib" +name = "nbmail" version = "0.0.1" source = { editable = "." } dependencies = [