diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bf9eb43..6115fc1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,23 +31,23 @@ jobs:
run: uv sync --all-extras --dev
- name: Install emailer-lib
- run: uv pip install -e "./emailer-lib"
+ run: uv pip install -e .
# quarto docs build ----
- uses: quarto-dev/quarto-actions/setup@v2
- with:
- tinytex: true
+ # with:
+ # tinytex: true
- name: Build docs
run: |
- uv run quartodoc build --verbose && uv run quarto render
+ cd docs && uv run quartodoc build --verbose && uv run quarto render
- name: Deploy
uses: JamesIves/github-pages-deploy-action@v4
if: ${{ github.ref == 'refs/heads/main' }}
with:
force: false
- folder: _site
+ folder: docs/_site
clean-exclude: pr-preview
- name: Deploy (preview)
@@ -55,7 +55,7 @@ jobs:
# if in a PR
if: ${{ github.event_name == 'pull_request' }}
with:
- source-dir: _site
+ source-dir: docs/_site
test-emailer-lib:
runs-on: ubuntu-latest
@@ -81,18 +81,18 @@ jobs:
run: uv sync --all-extras --dev
- name: Install emailer-lib
- run: uv pip install -e "./emailer-lib"
+ run: uv pip install -e .
- name: Install the project deps
- run: uv pip install -e "./emailer-lib[dev]"
+ run: uv pip install -e .[dev]
- name: Test emailer-lib
run: |
- uv run pytest emailer-lib/emailer_lib/tests/ --cov=emailer_lib --cov-report=xml --cov-report=term-missing
+ uv run pytest emailer_lib/tests/ --cov=emailer_lib --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
\ No newline at end of file
+ flags: emailer-lib
diff --git a/.gitignore b/.gitignore
index 5753ab2..709c5de 100644
--- a/.gitignore
+++ b/.gitignore
@@ -212,4 +212,4 @@ _site
.DS_Store
-.vscode
\ No newline at end of file
+.vscode
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c0c4950
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,14 @@
+build-docs: generate-mjml-tags
+ cd docs && uv run quartodoc build --verbose && quarto render
+
+preview:
+ cd docs && quarto preview
+
+test:
+ pytest emailer_lib/tests emailer_lib/mjml/tests --cov-report=xml
+
+test-update:
+ pytest emailer_lib/tests emailer_lib/mjml/tests --snapshot-update
+
+generate-mjml-tags:
+ python3 emailer_lib/mjml/scripts/generate_tags.py
diff --git a/README.md b/README.md
index 5c8c8e9..b026ae7 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,84 @@
-# email-for-data-science
\ No newline at end of file
+# emailer-lib
+
+
+
+
+
+
+
+[](https://posit-dev.github.io/email-for-data-science/reference/)
+
+
+
+
+> ⚠️ **emailer-lib is currently in development, expect breaking changes.**
+
+
+### What is [emailer-lib](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.).
+
+The package is designed for data science workflows and Quarto projects, making it easy to generate, preview, and deliver rich email content programmatically.
+
+
+
+## Example Usage
+
+```python
+from emailer_lib import (
+ quarto_json_to_intermediate_email,
+ IntermediateEmail,
+ send_intermediate_email_with_gmail,
+)
+
+# Read a Quarto email JSON file
+email_struct = quarto_json_to_intermediate_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)
+```
+
+## Features
+
+- **Unified email structure** for serialization and conversion
+- **Convert** emails from Redmail, Yagmail, MJML, and Quarto JSON
+- **Send** emails via Gmail, SMTP, Mailgun, and more
+- **Preview** emails as HTML files
+- **Support for attachments** (inline and external)
+- **Simple API** for integration in data science and reporting workflows
+
+## Contributing
+If you encounter a bug, have usage questions, or want to share ideas to make this package better, please feel free to file an [issue](https://github.com/posit-dev/email-for-data-science/issues).
+
+
+
+
+
+
+
+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
diff --git a/cover.png b/cover.png
deleted file mode 100644
index e1f5bc6..0000000
Binary files a/cover.png and /dev/null differ
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000..0709e39
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,2 @@
+/.quarto/
+/reference/
diff --git a/.output_metadata.json b/docs/.output_metadata.json
similarity index 100%
rename from .output_metadata.json
rename to docs/.output_metadata.json
diff --git a/_quarto.yml b/docs/_quarto.yml
similarity index 71%
rename from _quarto.yml
rename to docs/_quarto.yml
index 28841ce..cfa6598 100644
--- a/_quarto.yml
+++ b/docs/_quarto.yml
@@ -70,13 +70,52 @@ quartodoc:
- send_intermediate_email_with_mailgun
- send_quarto_email_with_gmail
-
- title: Utilities
desc: >
Previews and more
contents:
- write_email_message_to_file
+ - title: MJML Authoring
+ desc: >
+ Write responsive emails with MJML
+ package: emailer_lib
+ contents:
+ - mjml.mjml
+ - mjml.head
+ - mjml.body
+ - mjml.mj_attributes
+ - mjml.mj_all
+ - mjml.mj_class
+ - mjml.breakpoint
+ - mjml.font
+ - mjml.html_attributes
+ - mjml.html_attribute
+ - mjml.preview
+ - mjml.style
+ - mjml.title
+ - mjml.accordion
+ - mjml.accordion_element
+ - mjml.accordion_text
+ - mjml.accordion_title
+ - mjml.button
+ - mjml.carousel
+ - mjml.carousel_image
+ - mjml.column
+ - mjml.divider
+ - mjml.group
+ - mjml.hero
+ - mjml.image
+ - mjml.navbar
+ - mjml.navbar_link
+ - mjml.raw
+ - mjml.section
+ - mjml.social
+ - mjml.social_element
+ - mjml.spacer
+ - mjml.table
+ - mjml.text
+ - mjml.wrapper
format:
html:
diff --git a/assets/mjml-email-full.png b/docs/assets/mjml-email-full.png
similarity index 100%
rename from assets/mjml-email-full.png
rename to docs/assets/mjml-email-full.png
diff --git a/assets/whole-game-email-annotated.png b/docs/assets/whole-game-email-annotated.png
similarity index 100%
rename from assets/whole-game-email-annotated.png
rename to docs/assets/whole-game-email-annotated.png
diff --git a/assets/whole-game-email.png b/docs/assets/whole-game-email.png
similarity index 100%
rename from assets/whole-game-email.png
rename to docs/assets/whole-game-email.png
diff --git a/assets/whole-game-fancy.png b/docs/assets/whole-game-fancy.png
similarity index 100%
rename from assets/whole-game-fancy.png
rename to docs/assets/whole-game-fancy.png
diff --git a/assets/whole-game-quarto.png b/docs/assets/whole-game-quarto.png
similarity index 100%
rename from assets/whole-game-quarto.png
rename to docs/assets/whole-game-quarto.png
diff --git a/configuring-attachments.qmd b/docs/configuring-attachments.qmd
similarity index 100%
rename from configuring-attachments.qmd
rename to docs/configuring-attachments.qmd
diff --git a/configuring-subject.qmd b/docs/configuring-subject.qmd
similarity index 100%
rename from configuring-subject.qmd
rename to docs/configuring-subject.qmd
diff --git a/content-embedding.qmd b/docs/content-embedding.qmd
similarity index 100%
rename from content-embedding.qmd
rename to docs/content-embedding.qmd
diff --git a/content-layout.qmd b/docs/content-layout.qmd
similarity index 100%
rename from content-layout.qmd
rename to docs/content-layout.qmd
diff --git a/data_polars.py b/docs/data_polars.py
similarity index 100%
rename from data_polars.py
rename to docs/data_polars.py
diff --git a/index.qmd b/docs/index.qmd
similarity index 100%
rename from index.qmd
rename to docs/index.qmd
diff --git a/docs/objects.json b/docs/objects.json
new file mode 100644
index 0000000..6165209
--- /dev/null
+++ b/docs/objects.json
@@ -0,0 +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
diff --git a/orchestrating-auth.qmd b/docs/orchestrating-auth.qmd
similarity index 100%
rename from orchestrating-auth.qmd
rename to docs/orchestrating-auth.qmd
diff --git a/orchestrating-tests.qmd b/docs/orchestrating-tests.qmd
similarity index 100%
rename from orchestrating-tests.qmd
rename to docs/orchestrating-tests.qmd
diff --git a/summary.qmd b/docs/summary.qmd
similarity index 100%
rename from summary.qmd
rename to docs/summary.qmd
diff --git a/emailer-lib/.gitignore b/emailer-lib/.gitignore
deleted file mode 100644
index 68297d9..0000000
--- a/emailer-lib/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# generated emails
-*.html
\ No newline at end of file
diff --git a/emailer-lib/Makefile b/emailer-lib/Makefile
deleted file mode 100644
index 649ce8a..0000000
--- a/emailer-lib/Makefile
+++ /dev/null
@@ -1,5 +0,0 @@
-test:
- pytest --cov-report=xml
-
-test-update:
- pytest --snapshot-update
\ No newline at end of file
diff --git a/emailer-lib/README.md b/emailer-lib/README.md
deleted file mode 100644
index b026ae7..0000000
--- a/emailer-lib/README.md
+++ /dev/null
@@ -1,84 +0,0 @@
-# emailer-lib
-
-
-
-
-
-
-
-[](https://posit-dev.github.io/email-for-data-science/reference/)
-
-
-
-
-> ⚠️ **emailer-lib is currently in development, expect breaking changes.**
-
-
-### What is [emailer-lib](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.).
-
-The package is designed for data science workflows and Quarto projects, making it easy to generate, preview, and deliver rich email content programmatically.
-
-
-
-## Example Usage
-
-```python
-from emailer_lib import (
- quarto_json_to_intermediate_email,
- IntermediateEmail,
- send_intermediate_email_with_gmail,
-)
-
-# Read a Quarto email JSON file
-email_struct = quarto_json_to_intermediate_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)
-```
-
-## Features
-
-- **Unified email structure** for serialization and conversion
-- **Convert** emails from Redmail, Yagmail, MJML, and Quarto JSON
-- **Send** emails via Gmail, SMTP, Mailgun, and more
-- **Preview** emails as HTML files
-- **Support for attachments** (inline and external)
-- **Simple API** for integration in data science and reporting workflows
-
-## Contributing
-If you encounter a bug, have usage questions, or want to share ideas to make this package better, please feel free to file an [issue](https://github.com/posit-dev/email-for-data-science/issues).
-
-
-
-
-
-
-
-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
diff --git a/emailer-lib/pyproject.toml b/emailer-lib/pyproject.toml
deleted file mode 100644
index d7e4092..0000000
--- a/emailer-lib/pyproject.toml
+++ /dev/null
@@ -1,40 +0,0 @@
-[build-system]
-requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2"]
-build-backend = "setuptools.build_meta"
-
-[tool.setuptools_scm]
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-addopts = "-ra --cov=emailer_lib --cov-report=term-missing"
-testpaths = ["emailer_lib/tests"]
-
-[project]
-name = "emailer-lib"
-version = "0.0.1"
-description = "Email serialization and sending utilities"
-authors = [{name = "Jules Walzer-Goldfeld"}]
-readme = "README.md"
-
-requires-python = ">=3.9"
-
-dependencies = [
- "dotenv",
- "mjml-python>=1.3.6",
-]
-
-[project.optional-dependencies]
-dev = [
- "aiosmtpd",
- "pytest",
- "pytest-cov",
-]
-
-[tool.coverage.report]
-exclude_also = [
- "if TYPE_CHECKING:"
-]
-include = ["emailer_lib/*"]
-omit = [
- "emailer_lib/tests/*"
-]
\ No newline at end of file
diff --git a/emailer-lib/emailer_lib/__init__.py b/emailer_lib/__init__.py
similarity index 100%
rename from emailer-lib/emailer_lib/__init__.py
rename to emailer_lib/__init__.py
diff --git a/emailer-lib/emailer_lib/egress.py b/emailer_lib/egress.py
similarity index 100%
rename from emailer-lib/emailer_lib/egress.py
rename to emailer_lib/egress.py
diff --git a/emailer-lib/emailer_lib/ingress.py b/emailer_lib/ingress.py
similarity index 100%
rename from emailer-lib/emailer_lib/ingress.py
rename to emailer_lib/ingress.py
diff --git a/emailer_lib/mjml/.gitignore b/emailer_lib/mjml/.gitignore
new file mode 100644
index 0000000..9ce3ea5
--- /dev/null
+++ b/emailer_lib/mjml/.gitignore
@@ -0,0 +1,3 @@
+playground.qmd
+*.html
+sample_mjml.mjml
diff --git a/emailer_lib/mjml/README.md b/emailer_lib/mjml/README.md
new file mode 100644
index 0000000..16efad3
--- /dev/null
+++ b/emailer_lib/mjml/README.md
@@ -0,0 +1,198 @@
+# MJML Module
+
+A Python implementation of MJML tags for building responsive email templates programmatically.
+
+## Overview
+
+This module provides Python functions for creating MJML markup, the responsive email framework. Instead of writing raw MJML XML, you can use Python functions to build your email templates.
+
+## Features
+
+- **Automatic tag generation**: Tags are auto-generated from the official MJML specification
+- **Leaf tag safety**: Ending tags (like `mj-text`, `mj-button`) only accept content, not MJML children
+- **Flexible rendering**: Convert your Python MJML structures to valid MJML markup via [mjml2html](https://github.com/mgd020/mjml-python)
+
+## Installation
+
+This module is part of the `emailer_lib` package:
+
+```python
+from emailer_lib import mjml as mj
+```
+
+## Quick Start
+
+```python
+from emailer_lib.mjml import mjml, body, section, column, text
+
+# Build an MJML email structure
+email = mjml(
+ body(
+ section(
+ column(
+ text(content="Hello, World!", color="#ff6600")
+ )
+ )
+ )
+)
+
+# Render to MJML markup
+mjml_string = email.render()
+```
+
+## Tag Types
+
+### Container Tags
+
+These tags accept children (other MJML components) and optional content:
+
+- Structure: `mjml`, `head`, `body`, `include`
+- Layout: `section`, `column`, `group`, `wrapper`
+- Components: `accordion`, `carousel`, `hero`, `navbar`, `social`
+- Configuration: `attributes`, `breakpoint`, `font`, `html_attributes`, `style`, `title`
+
+Example:
+```python
+section(
+ column(
+ text(content="First column")
+ ),
+ column(
+ text(content="Second column")
+ ),
+ background_color="#f0f0f0"
+)
+```
+
+### Leaf/Ending Tags
+
+These tags accept text or HTML content but **not** MJML children:
+
+- `button` - Call-to-action buttons
+- `text` - Text content with HTML support
+- `table` - HTML tables
+- `raw` - Raw HTML content
+- `accordion_text`, `accordion_title` - Accordion content
+- `navbar_link` - Navigation links
+- `social_element` - Social media icons
+- `carousel_image` - Carousel images
+
+Example:
+```python
+text(
+ content="Bold text and a link",
+ font_size="16px",
+ color="#333333"
+)
+
+button(
+ content="Click Here",
+ href="https://example.com",
+ background_color="#007bff"
+)
+```
+
+## Core Classes
+
+### `MJMLTag`
+
+The base class for all MJML elements. Can be instantiated directly or via helper functions.
+
+```python
+from emailer_lib.mjml import MJMLTag
+
+tag = MJMLTag(
+ "mj-text",
+ content="Hello",
+ color="#ff6600"
+)
+```
+
+### `TagAttrDict`
+
+A dictionary type for tag attributes.
+
+## Examples
+
+### Simple Email
+
+```python
+from emailer_lib.mjml import mjml, head, body, section, column, text, title
+
+email = mjml(
+ head(
+ title(content="Welcome Email")
+ ),
+ body(
+ section(
+ column(
+ text(content="Welcome to our service!")
+ )
+ )
+ )
+)
+```
+
+### Multi-column Layout
+
+```python
+from emailer_lib.mjml import body, section, column, text, image
+
+layout = body(
+ section(
+ column(
+ image(src="https://example.com/logo.png"),
+ text(content="Column 1")
+ ),
+ column(
+ text(content="Column 2")
+ ),
+ column(
+ text(content="Column 3")
+ )
+ )
+)
+```
+
+### Using Attributes
+
+```python
+from emailer_lib.mjml import section, column, text
+
+# Attributes as kwargs
+section(
+ column(
+ text(content="Styled text", color="#ff0000", font_size="20px")
+ ),
+ background_color="#f5f5f5",
+ padding="20px"
+)
+```
+
+## Rendering
+
+Use the `.render()` method to convert MJML structures to markup:
+
+```python
+mjml_markup = email.render()
+# Pass to MJML API or tool for HTML conversion
+```
+
+## API Reference
+
+For detailed documentation of all tags and their attributes, see the [API Reference](https://posit-dev.github.io/email-for-data-science/reference/).
+
+## Resources
+
+- [Official MJML Documentation](https://documentation.mjml.io/)
+- [MJML GitHub Repository](https://github.com/mjmlio/mjml)
+
+## Development
+
+The tag functions in `tags.py` are auto-generated by `scripts/generate-tags.py`. To regenerate:
+
+```bash
+make generate-mjml-tags
+```
+
+**Do not edit `tags.py` directly** - your changes will be overwritten.
diff --git a/emailer_lib/mjml/__init__.py b/emailer_lib/mjml/__init__.py
new file mode 100644
index 0000000..959139c
--- /dev/null
+++ b/emailer_lib/mjml/__init__.py
@@ -0,0 +1,81 @@
+# MJMLTools package init
+# Exposes MJMLTag, TagAttrDict, and all tag functions
+
+from ._core import MJMLTag, TagAttrDict
+from .tags import (
+ mjml,
+ head,
+ body,
+ mj_attributes,
+ mj_all,
+ mj_class,
+ breakpoint,
+ font,
+ html_attributes,
+ html_attribute,
+ preview,
+ style,
+ title,
+ accordion,
+ accordion_element,
+ button,
+ carousel,
+ carousel_image,
+ column,
+ divider,
+ group,
+ hero,
+ image,
+ navbar,
+ raw,
+ section,
+ social,
+ spacer,
+ table,
+ text,
+ wrapper,
+ accordion_text,
+ accordion_title,
+ navbar_link,
+ social_element,
+)
+
+__all__ = (
+ "MJMLTag",
+ "TagAttrDict",
+ "mjml",
+ "head",
+ "body",
+ "mj_attributes",
+ "mj_all",
+ "mj_class",
+ "breakpoint",
+ "font",
+ "html_attributes",
+ "html_attribute",
+ "preview",
+ "style",
+ "title",
+ "accordion",
+ "accordion_element",
+ "button",
+ "carousel",
+ "carousel_image",
+ "column",
+ "divider",
+ "group",
+ "hero",
+ "image",
+ "navbar",
+ "raw",
+ "section",
+ "social",
+ "spacer",
+ "table",
+ "text",
+ "wrapper",
+ "accordion_text",
+ "accordion_title",
+ "navbar_link",
+ "social_element",
+)
diff --git a/emailer_lib/mjml/_core.py b/emailer_lib/mjml/_core.py
new file mode 100644
index 0000000..d2068f2
--- /dev/null
+++ b/emailer_lib/mjml/_core.py
@@ -0,0 +1,204 @@
+# MJML core classes adapted from py-htmltools
+
+## TODO: make sure Ending tags are rendered as needed
+# https://documentation.mjml.io/#ending-tags
+
+from typing import Dict, Mapping, Optional, Sequence, Union
+import warnings
+from mjml import mjml2html
+
+
+# Types for MJML
+TagAttrValue = Union[str, float, bool, None]
+TagAttrs = Union[Dict[str, TagAttrValue], "TagAttrDict"]
+TagChild = Union["MJMLTag", str, float, None, Sequence["TagChild"]]
+
+
+class TagAttrDict(Dict[str, str]):
+ """
+ MJML attribute dictionary. All values are stored as strings.
+ """
+
+ def __init__(
+ self, *args: Mapping[str, TagAttrValue]
+ ) -> None:
+ super().__init__()
+ for mapping in args:
+ for k, v in mapping.items():
+ if v is not None:
+ self[k] = str(v)
+
+ def update(self, *args: Mapping[str, TagAttrValue]) -> None:
+ for mapping in args:
+ for k, v in mapping.items():
+ if v is not None:
+ self[k] = str(v)
+
+
+class MJMLTag:
+ """
+ MJML tag class.
+ """
+
+ def __init__(
+ self,
+ tagName: str,
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+ _is_leaf: bool = False,
+ ) -> None:
+ self.tagName = tagName
+ self.attrs = TagAttrDict()
+ self.children = []
+ self._is_leaf = _is_leaf
+
+ # Runtime validation for leaf tags
+ if self._is_leaf:
+ # For leaf tags, treat the first positional argument as content if provided
+ if args:
+ if len(args) > 1:
+ raise TypeError(
+ f"<{tagName}> is a leaf tag and accepts only one positional argument for content."
+ )
+ self.content = args[0]
+ else:
+ self.content = content
+
+ # Validate content type
+ if self.content is not None and not isinstance(self.content, (str, int, float)):
+ raise TypeError(
+ f"<{tagName}> content must be a string, int, or float, "
+ f"got {type(self.content).__name__}"
+ )
+
+ # Validate attributes parameter type
+ if attributes is not None and not isinstance(attributes, (dict, TagAttrDict)):
+ raise TypeError(
+ f"attributes must be a dict or TagAttrDict, got {type(attributes).__name__}."
+ )
+
+ # Process attributes
+ if attributes is not None:
+ self.attrs.update(attributes)
+ else:
+ # For container tags
+ self.content = content
+
+ # Validate attributes parameter type
+ if attributes is not None and not isinstance(attributes, (dict, TagAttrDict)):
+ raise TypeError(
+ f"attributes must be a dict or TagAttrDict, got {type(attributes).__name__}. "
+ f"If you meant to pass children, use positional arguments for container tags."
+ )
+
+ # Collect children (for non-leaf tags only)
+ for arg in args:
+ if (
+ isinstance(arg, (str, float)) or arg is None or isinstance(arg, MJMLTag)
+ ):
+ self.children.append(arg)
+ elif isinstance(arg, Sequence) and not isinstance(arg, str):
+ self.children.extend(arg)
+
+ # Process attributes
+ if attributes is not None:
+ self.attrs.update(attributes)
+
+ # TODO: confirm if this is the case... I don't think it is
+ # # If content is provided, children should be empty
+ # if self.content is not None:
+ # self.children = []
+
+ def render_mjml(self, indent: int = 0, eol: str = "\n") -> str:
+ """
+ Render MJMLTag and its children to MJML markup.
+ Ported from htmltools Tag rendering logic.
+ """
+
+ def _flatten(children):
+ for c in children:
+ if c is None:
+ continue
+ elif isinstance(c, MJMLTag):
+ yield c
+ elif isinstance(c, (str, float)):
+ yield c
+
+ # Build attribute string
+ attr_str = ""
+ if self.attrs:
+ attr_str = " " + " ".join(f'{k}="{v}"' for k, v in self.attrs.items())
+
+ # Render children/content
+ inner = ""
+ if self.content is not None:
+ inner = str(self.content)
+ else:
+ child_strs = []
+ for child in _flatten(self.children):
+ if isinstance(child, MJMLTag):
+ child_strs.append(child.render_mjml(indent + 2, eol))
+ else:
+ child_strs.append(str(child))
+ if child_strs:
+ inner = eol.join(child_strs)
+
+ # Indentation
+ pad = " " * indent
+ if inner:
+ return f"{pad}<{self.tagName}{attr_str}>{eol}{inner}{eol}{pad}{self.tagName}>"
+ else:
+ return f"{pad}<{self.tagName}{attr_str}>{self.tagName}>"
+
+ def _repr_html_(self):
+ return self.to_html()
+
+ def __repr__(self) -> str:
+ return self.render_mjml()
+
+ def to_html(self, **mjml2html_kwargs):
+ """
+ Render MJMLTag to HTML using mjml2html.
+
+ If this is not a top-level tag, it will be automatically wrapped
+ in ... with a warning.
+
+ Parameters
+ ----------
+ **mjml2html_kwargs
+ Additional keyword arguments to pass to mjml2html
+
+ Returns
+ -------
+ str
+ Result from mjml2html containing html content
+ """
+ if self.tagName == "mjml":
+ # Already a complete MJML document
+ mjml_markup = self.render_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.render_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.render_mjml()
+
+ return mjml2html(mjml_markup, **mjml2html_kwargs)
diff --git a/emailer_lib/mjml/scripts/generate_tags.py b/emailer_lib/mjml/scripts/generate_tags.py
new file mode 100644
index 0000000..d84b7f0
--- /dev/null
+++ b/emailer_lib/mjml/scripts/generate_tags.py
@@ -0,0 +1,196 @@
+#!/usr/bin/env python3
+
+"""
+Script to auto-generate tags.py with MJML tag functions.
+Run this script to regenerate tags.py from the list of MJML tags.
+"""
+
+from pathlib import Path
+
+# List of all MJML tags (from official docs)
+MJML_TAGS = [
+ # Root
+ "mjml",
+ "mj-head",
+ "mj-body",
+ # Head components
+ "mj-attributes",
+ "mj-all", # sub-attribute for mj-attributes
+ "mj-class", # sub-attribute for mj-attributes
+ "mj-breakpoint",
+ "mj-font",
+ "mj-html-attributes",
+ "mj-html-attribute", # sub-attribute for mj-html-attributes
+ "mj-preview",
+ "mj-style",
+ # Body components
+ "mj-accordion",
+ "mj-accordion-element",
+ "mj-carousel",
+ "mj-column",
+ "mj-divider",
+ "mj-group",
+ "mj-hero",
+ "mj-image",
+ "mj-navbar",
+ "mj-section",
+ "mj-social",
+ "mj-spacer",
+ "mj-wrapper",
+]
+
+# Leaf/ending tags - accept content (text/HTML) but not MJML children
+LEAF_TAGS = [
+ "mj-accordion-text",
+ "mj-accordion-title",
+ "mj-button",
+ "mj-carousel-image",
+ "mj-navbar-link",
+ "mj-raw",
+ "mj-social-element",
+ "mj-table",
+ "mj-text",
+ "mj-title"
+]
+
+# Tags that should keep the mj- prefix in the function name
+KEEP_MJ_PREFIX = ["mj-attributes", "mj-all", "mj-class"]
+
+
+def get_python_name(tag_name: str) -> str:
+ """Convert MJML tag name to Python function name."""
+ if tag_name in KEEP_MJ_PREFIX:
+ # Keep mj- prefix, just replace hyphens
+ return tag_name.replace("-", "_")
+ else:
+ # Remove mj- prefix and replace hyphens
+ return tag_name.replace("-", "_").replace("mj_", "", 1)
+
+
+def generate_tags_file():
+ """Generate tags.py with all MJML tag functions."""
+
+ header = '''# AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
+# This file is auto-generated by scripts/generate-tags.py
+# To regenerate, run: python scripts/generate-tags.py
+
+"""MJML tag functions for all official MJML tags."""
+
+from typing import Optional, Union
+
+from ._core import MJMLTag, TagAttrs, TagAttrValue, TagChild
+
+'''
+
+ functions = []
+
+ # Generate regular MJML tags (accept children and optional content)
+ for tag_name in MJML_TAGS:
+ py_name = get_python_name(tag_name)
+
+ function_code = f'''
+def {py_name}(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `<{tag_name}>` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing `<{tag_name}>`
+
+ Examples
+ --------
+ With children:
+ ```{{python}}
+ from emailer_lib.mjml import {py_name}, text
+ child = text("Hello World")
+
+ result = {py_name}(child)
+ ```
+
+ With attributes:
+ ```{{python}}
+ result = {py_name}(attributes={{"attr": "value"}})
+ ```
+
+ With both:
+ ```{{python}}
+ result = {py_name}(child, attributes={{"background-color": "yellow"}})
+ ```
+ """
+ return MJMLTag("{tag_name}", *args, attributes=attributes, content=content)
+'''
+ functions.append(function_code)
+
+ # Generate leaf tags (accept content but not MJML children)
+ for tag_name in LEAF_TAGS:
+ py_name = get_python_name(tag_name)
+
+ function_code = f'''
+def {py_name}(
+ content: Optional[str] = None,
+ attributes: Optional[TagAttrs] = None,
+):
+ """
+ Create an MJML `<{tag_name}>` tag.
+
+ This is an ending tag that accepts text/HTML content but not MJML children.
+
+ Parameters
+ ----------
+ content
+ Text or HTML content for the tag
+ attributes
+ Optional dict of tag attributes
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing `<{tag_name}>`
+
+ Examples
+ --------
+ With content:
+ ```{{python}}
+ from emailer_lib.mjml import {py_name}
+
+ result = {py_name}("Hello")
+ ```
+
+ With attributes and content:
+ ```{{python}}
+ result = {py_name}("Hello", attributes={{"background-color": "red"}})
+ ```
+ """
+ return MJMLTag("{tag_name}", content, attributes=attributes, _is_leaf=True)
+'''
+ functions.append(function_code)
+
+ # Combine all parts
+ output = header + "\n".join(functions)
+
+ # Write to file - navigate from scripts/ to emailer_lib/mjml/tags.py
+ script_dir = Path(__file__).parent
+ tags_file = script_dir.parent / "tags.py"
+
+ with open(tags_file, "w") as f:
+ f.write(output)
+
+ print(f"Generated {tags_file} with {len(MJML_TAGS)} container tags and {len(LEAF_TAGS)} leaf tags")
+
+
+if __name__ == "__main__":
+ generate_tags_file()
diff --git a/emailer_lib/mjml/tags.py b/emailer_lib/mjml/tags.py
new file mode 100644
index 0000000..1cdbf1c
--- /dev/null
+++ b/emailer_lib/mjml/tags.py
@@ -0,0 +1,1514 @@
+# AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
+# This file is auto-generated by scripts/generate-tags.py
+# To regenerate, run: python scripts/generate-tags.py
+
+"""MJML tag functions for all official MJML tags."""
+
+from typing import Optional, Union
+
+from ._core import MJMLTag, TagAttrs, TagAttrValue, TagChild
+
+
+def mjml(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import mjml, text
+ child = text("Hello World")
+
+ result = mjml(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = mjml(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = mjml(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mjml", *args, attributes=attributes, content=content)
+
+
+def head(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import head, text
+ child = text("Hello World")
+
+ result = head(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = head(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = head(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-head", *args, attributes=attributes, content=content)
+
+
+def body(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import body, text
+ child = text("Hello World")
+
+ result = body(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = body(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = body(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-body", *args, attributes=attributes, content=content)
+
+
+def mj_attributes(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import mj_attributes, text
+ child = text("Hello World")
+
+ result = mj_attributes(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = mj_attributes(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = mj_attributes(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-attributes", *args, attributes=attributes, content=content)
+
+
+def mj_all(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import mj_all, text
+ child = text("Hello World")
+
+ result = mj_all(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = mj_all(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = mj_all(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-all", *args, attributes=attributes, content=content)
+
+
+def mj_class(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import mj_class, text
+ child = text("Hello World")
+
+ result = mj_class(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = mj_class(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = mj_class(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-class", *args, attributes=attributes, content=content)
+
+
+def breakpoint(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import breakpoint, text
+ child = text("Hello World")
+
+ result = breakpoint(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = breakpoint(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = breakpoint(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-breakpoint", *args, attributes=attributes, content=content)
+
+
+def font(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import font, text
+ child = text("Hello World")
+
+ result = font(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = font(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = font(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-font", *args, attributes=attributes, content=content)
+
+
+def html_attributes(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import html_attributes, text
+ child = text("Hello World")
+
+ result = html_attributes(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = html_attributes(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = html_attributes(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-html-attributes", *args, attributes=attributes, content=content)
+
+
+def html_attribute(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import html_attribute, text
+ child = text("Hello World")
+
+ result = html_attribute(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = html_attribute(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = html_attribute(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-html-attribute", *args, attributes=attributes, content=content)
+
+
+def preview(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import preview, text
+ child = text("Hello World")
+
+ result = preview(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = preview(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = preview(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-preview", *args, attributes=attributes, content=content)
+
+
+def style(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import style, text
+ child = text("Hello World")
+
+ result = style(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = style(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = style(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-style", *args, attributes=attributes, content=content)
+
+
+def accordion(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import accordion, text
+ child = text("Hello World")
+
+ result = accordion(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = accordion(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = accordion(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-accordion", *args, attributes=attributes, content=content)
+
+
+def accordion_element(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import accordion_element, text
+ child = text("Hello World")
+
+ result = accordion_element(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = accordion_element(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = accordion_element(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-accordion-element", *args, attributes=attributes, content=content)
+
+
+def carousel(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import carousel, text
+ child = text("Hello World")
+
+ result = carousel(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = carousel(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = carousel(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-carousel", *args, attributes=attributes, content=content)
+
+
+def column(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import column, text
+ child = text("Hello World")
+
+ result = column(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = column(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = column(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-column", *args, attributes=attributes, content=content)
+
+
+def divider(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import divider, text
+ child = text("Hello World")
+
+ result = divider(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = divider(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = divider(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-divider", *args, attributes=attributes, content=content)
+
+
+def group(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import group, text
+ child = text("Hello World")
+
+ result = group(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = group(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = group(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-group", *args, attributes=attributes, content=content)
+
+
+def hero(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import hero, text
+ child = text("Hello World")
+
+ result = hero(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = hero(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = hero(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-hero", *args, attributes=attributes, content=content)
+
+
+def image(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import image, text
+ child = text("Hello World")
+
+ result = image(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = image(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = image(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-image", *args, attributes=attributes, content=content)
+
+
+def navbar(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import navbar, text
+ child = text("Hello World")
+
+ result = navbar(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = navbar(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = navbar(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-navbar", *args, attributes=attributes, content=content)
+
+
+def section(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import section, text
+ child = text("Hello World")
+
+ result = section(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = section(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = section(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-section", *args, attributes=attributes, content=content)
+
+
+def social(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import social, text
+ child = text("Hello World")
+
+ result = social(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = social(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = social(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-social", *args, attributes=attributes, content=content)
+
+
+def spacer(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import spacer, text
+ child = text("Hello World")
+
+ result = spacer(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = spacer(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = spacer(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-spacer", *args, attributes=attributes, content=content)
+
+
+def wrapper(
+ *args: TagChild,
+ attributes: Optional[TagAttrs] = None,
+ content: Optional[str] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ Parameters
+ ----------
+ *args
+ Children (MJMLTag objects)
+ attributes
+ Optional dict of tag attributes
+ content
+ Optional text content for the tag
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With children:
+ ```{python}
+ from emailer_lib.mjml import wrapper, text
+ child = text("Hello World")
+
+ result = wrapper(child)
+ ```
+
+ With attributes:
+ ```{python}
+ result = wrapper(attributes={"attr": "value"})
+ ```
+
+ With both:
+ ```{python}
+ result = wrapper(child, attributes={"background-color": "yellow"})
+ ```
+ """
+ return MJMLTag("mj-wrapper", *args, attributes=attributes, content=content)
+
+
+def accordion_text(
+ content: Optional[str] = None,
+ attributes: Optional[TagAttrs] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ This is an ending tag that accepts text/HTML content but not MJML children.
+
+ Parameters
+ ----------
+ content
+ Text or HTML content for the tag
+ attributes
+ Optional dict of tag attributes
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With content:
+ ```{python}
+ from emailer_lib.mjml import accordion_text
+
+ result = accordion_text("Hello")
+ ```
+
+ With attributes and content:
+ ```{python}
+ result = accordion_text("Hello", attributes={"background-color": "red"})
+ ```
+ """
+ return MJMLTag("mj-accordion-text", content, attributes=attributes, _is_leaf=True)
+
+
+def accordion_title(
+ content: Optional[str] = None,
+ attributes: Optional[TagAttrs] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ This is an ending tag that accepts text/HTML content but not MJML children.
+
+ Parameters
+ ----------
+ content
+ Text or HTML content for the tag
+ attributes
+ Optional dict of tag attributes
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With content:
+ ```{python}
+ from emailer_lib.mjml import accordion_title
+
+ result = accordion_title("Hello")
+ ```
+
+ With attributes and content:
+ ```{python}
+ result = accordion_title("Hello", attributes={"background-color": "red"})
+ ```
+ """
+ return MJMLTag("mj-accordion-title", content, attributes=attributes, _is_leaf=True)
+
+
+def button(
+ content: Optional[str] = None,
+ attributes: Optional[TagAttrs] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ This is an ending tag that accepts text/HTML content but not MJML children.
+
+ Parameters
+ ----------
+ content
+ Text or HTML content for the tag
+ attributes
+ Optional dict of tag attributes
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With content:
+ ```{python}
+ from emailer_lib.mjml import button
+
+ result = button("Hello")
+ ```
+
+ With attributes and content:
+ ```{python}
+ result = button("Hello", attributes={"background-color": "red"})
+ ```
+ """
+ return MJMLTag("mj-button", content, attributes=attributes, _is_leaf=True)
+
+
+def carousel_image(
+ content: Optional[str] = None,
+ attributes: Optional[TagAttrs] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ This is an ending tag that accepts text/HTML content but not MJML children.
+
+ Parameters
+ ----------
+ content
+ Text or HTML content for the tag
+ attributes
+ Optional dict of tag attributes
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With content:
+ ```{python}
+ from emailer_lib.mjml import carousel_image
+
+ result = carousel_image("Hello")
+ ```
+
+ With attributes and content:
+ ```{python}
+ result = carousel_image("Hello", attributes={"background-color": "red"})
+ ```
+ """
+ return MJMLTag("mj-carousel-image", content, attributes=attributes, _is_leaf=True)
+
+
+def navbar_link(
+ content: Optional[str] = None,
+ attributes: Optional[TagAttrs] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ This is an ending tag that accepts text/HTML content but not MJML children.
+
+ Parameters
+ ----------
+ content
+ Text or HTML content for the tag
+ attributes
+ Optional dict of tag attributes
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With content:
+ ```{python}
+ from emailer_lib.mjml import navbar_link
+
+ result = navbar_link("Hello")
+ ```
+
+ With attributes and content:
+ ```{python}
+ result = navbar_link("Hello", attributes={"background-color": "red"})
+ ```
+ """
+ return MJMLTag("mj-navbar-link", content, attributes=attributes, _is_leaf=True)
+
+
+def raw(
+ content: Optional[str] = None,
+ attributes: Optional[TagAttrs] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ This is an ending tag that accepts text/HTML content but not MJML children.
+
+ Parameters
+ ----------
+ content
+ Text or HTML content for the tag
+ attributes
+ Optional dict of tag attributes
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With content:
+ ```{python}
+ from emailer_lib.mjml import raw
+
+ result = raw("Hello")
+ ```
+
+ With attributes and content:
+ ```{python}
+ result = raw("Hello", attributes={"background-color": "red"})
+ ```
+ """
+ return MJMLTag("mj-raw", content, attributes=attributes, _is_leaf=True)
+
+
+def social_element(
+ content: Optional[str] = None,
+ attributes: Optional[TagAttrs] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ This is an ending tag that accepts text/HTML content but not MJML children.
+
+ Parameters
+ ----------
+ content
+ Text or HTML content for the tag
+ attributes
+ Optional dict of tag attributes
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With content:
+ ```{python}
+ from emailer_lib.mjml import social_element
+
+ result = social_element("Hello")
+ ```
+
+ With attributes and content:
+ ```{python}
+ result = social_element("Hello", attributes={"background-color": "red"})
+ ```
+ """
+ return MJMLTag("mj-social-element", content, attributes=attributes, _is_leaf=True)
+
+
+def table(
+ content: Optional[str] = None,
+ attributes: Optional[TagAttrs] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ This is an ending tag that accepts text/HTML content but not MJML children.
+
+ Parameters
+ ----------
+ content
+ Text or HTML content for the tag
+ attributes
+ Optional dict of tag attributes
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With content:
+ ```{python}
+ from emailer_lib.mjml import table
+
+ result = table("Hello")
+ ```
+
+ With attributes and content:
+ ```{python}
+ result = table("Hello", attributes={"background-color": "red"})
+ ```
+ """
+ return MJMLTag("mj-table", content, attributes=attributes, _is_leaf=True)
+
+
+def text(
+ content: Optional[str] = None,
+ attributes: Optional[TagAttrs] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ This is an ending tag that accepts text/HTML content but not MJML children.
+
+ Parameters
+ ----------
+ content
+ Text or HTML content for the tag
+ attributes
+ Optional dict of tag attributes
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With content:
+ ```{python}
+ from emailer_lib.mjml import text
+
+ result = text("Hello")
+ ```
+
+ With attributes and content:
+ ```{python}
+ result = text("Hello", attributes={"background-color": "red"})
+ ```
+ """
+ return MJMLTag("mj-text", content, attributes=attributes, _is_leaf=True)
+
+
+def title(
+ content: Optional[str] = None,
+ attributes: Optional[TagAttrs] = None,
+):
+ """
+ Create an MJML `` tag.
+
+ This is an ending tag that accepts text/HTML content but not MJML children.
+
+ Parameters
+ ----------
+ content
+ Text or HTML content for the tag
+ attributes
+ Optional dict of tag attributes
+
+ Returns
+ -------
+ MJMLTag
+ MJMLTag object representing ``
+
+ Examples
+ --------
+ With content:
+ ```{python}
+ from emailer_lib.mjml import title
+
+ result = title("Hello")
+ ```
+
+ With attributes and content:
+ ```{python}
+ result = title("Hello", attributes={"background-color": "red"})
+ ```
+ """
+ return MJMLTag("mj-title", content, attributes=attributes, _is_leaf=True)
diff --git a/emailer_lib/mjml/tests/test_core.py b/emailer_lib/mjml/tests/test_core.py
new file mode 100644
index 0000000..935d955
--- /dev/null
+++ b/emailer_lib/mjml/tests/test_core.py
@@ -0,0 +1,173 @@
+import pytest
+from emailer_lib.mjml._core import MJMLTag, TagAttrDict
+
+
+def test_accepts_dict_arguments():
+ attrs = TagAttrDict({"color": "red", "padding": "10px"})
+
+ assert attrs["color"] == "red"
+ assert attrs["padding"] == "10px"
+
+
+def test_values_converted_to_strings():
+ attrs = TagAttrDict({"width": 100, "visible": True, "opacity": 0.5})
+
+ assert attrs["width"] == "100"
+ assert attrs["visible"] == "True"
+ assert attrs["opacity"] == "0.5"
+
+
+def test_update_method():
+ attrs = TagAttrDict({"color": "red"})
+ attrs.update({"font-size": "14px", "padding": "10px"})
+
+ assert attrs["color"] == "red"
+ assert attrs["font-size"] == "14px"
+ assert attrs["padding"] == "10px"
+
+
+def test_tag_with_dict_attributes():
+ attrs_dict = {"background-color": "#fff", "padding": "20px"}
+ tag = MJMLTag("mj-section", attributes=attrs_dict)
+
+ assert tag.attrs["background-color"] == "#fff"
+ assert tag.attrs["padding"] == "20px"
+
+
+def test_tag_filters_none_children():
+ tag = MJMLTag("mj-column", MJMLTag("mj-text", content="Text"), None)
+ mjml_content = tag.render_mjml()
+
+ # None should not appear in output
+ assert mjml_content.count("") == 1
+
+
+def test_render_empty_tag():
+ tag = MJMLTag("mj-spacer")
+ mjml_content = tag.render_mjml()
+ assert mjml_content == ""
+
+
+def test_render_with_attributes():
+ tag = MJMLTag("mj-spacer", attributes={"height": "20px"})
+ mjml_content = tag.render_mjml()
+ assert mjml_content == ''
+
+
+def test_render_with_custom_indent():
+ tag = MJMLTag("mj-text", content="Hello")
+ mjml_content = tag.render_mjml(indent=4)
+ assert mjml_content.startswith(" ")
+
+
+def test_render_with_custom_eol():
+ tag = MJMLTag("mj-text", content="Hello")
+ mjml_content = tag.render_mjml(eol="\r\n")
+ assert "\r\n" in mjml_content
+
+
+def test_render_nested_tags():
+ tag = MJMLTag(
+ "mj-section", MJMLTag("mj-column", MJMLTag("mj-text", content="Nested"))
+ )
+ mjml_content = tag.render_mjml()
+
+ assert "" in mjml_content
+ assert "" in mjml_content
+ assert "" in mjml_content
+ assert "Nested" in mjml_content
+
+
+def test_render_with_string_and_tag_children():
+ child_tag = MJMLTag("mj-text", content="Tagged")
+ tag = MJMLTag("mj-column", "Plain text", child_tag, "More text")
+ mjml_content = tag.render_mjml()
+
+ assert "Plain text" in mjml_content
+ assert "" in mjml_content
+ assert "More text" in mjml_content
+
+
+def test_repr_returns_mjml():
+ tag = MJMLTag("mj-text", content="Hello")
+
+ assert repr(tag) == tag.render_mjml()
+
+
+def test_to_html_with_complete_mjml_document():
+ tag = MJMLTag("mjml", MJMLTag("mj-body", MJMLTag("mj-section")))
+ html_result = tag.to_html()
+
+ assert "") == 3
+ assert "Text 1" in mjml_content
+ assert "Text 2" in mjml_content
+ assert "Text 3" in mjml_content
diff --git a/emailer_lib/mjml/tests/test_tags.py b/emailer_lib/mjml/tests/test_tags.py
new file mode 100644
index 0000000..ea10c32
--- /dev/null
+++ b/emailer_lib/mjml/tests/test_tags.py
@@ -0,0 +1,267 @@
+import pytest
+from emailer_lib.mjml import (
+ MJMLTag,
+ mjml,
+ head,
+ body,
+ section,
+ column,
+ text,
+ button,
+ image,
+ raw,
+ accordion,
+ accordion_element,
+ accordion_text,
+ accordion_title,
+ navbar,
+ navbar_link,
+ social,
+ social_element,
+ carousel,
+ carousel_image,
+ table,
+ mj_attributes,
+ mj_all,
+ mj_class,
+)
+
+
+def test_container_tag_accepts_children():
+ sec = section(
+ column(
+ text(content="Hello")
+ )
+ )
+
+ assert isinstance(sec, MJMLTag)
+ assert sec.tagName == "mj-section"
+ assert len(sec.children) == 1
+ assert sec.children[0].tagName == "mj-column"
+
+ mjml_content = sec.render_mjml()
+ assert "" in mjml_content
+ assert "" in mjml_content
+ assert "" in mjml_content
+ assert "Hello" in mjml_content
+ assert "" in mjml_content
+
+
+def test_container_tag_accepts_attributes():
+ sec = section(attributes={"background-color": "#fff", "padding": "20px"})
+
+ assert sec.attrs["background-color"] == "#fff"
+ assert sec.attrs["padding"] == "20px"
+
+ mjml_content = sec.render_mjml()
+ assert '' in mjml_content
+
+
+def test_container_tag_accepts_children_and_attrs():
+ sec = section(
+ column(text(content="Col 1")),
+ column(text(content="Col 2")),
+ attributes={"background-color": "#f0f0f0"}
+ )
+ assert len(sec.children) == 2
+ assert sec.attrs["background-color"] == "#f0f0f0"
+
+ mjml_content = sec.render_mjml()
+ assert 'background-color="#f0f0f0"' in mjml_content
+ assert "Col 1" in mjml_content
+ assert "Col 2" in mjml_content
+
+
+def test_leaf_tag_accepts_content():
+ txt = text(content="Hello World")
+
+ assert isinstance(txt, MJMLTag)
+ assert txt.tagName == "mj-text"
+ assert txt.content == "Hello World"
+
+ mjml_content = txt.render_mjml()
+ assert mjml_content == "\nHello World\n"
+
+
+def test_leaf_tag_accepts_attributes():
+ txt = text(attributes={"color": "red", "font-size": "16px"}, content="Hello")
+ assert txt.content == "Hello"
+ assert txt.attrs["color"] == "red"
+ assert txt.attrs["font-size"] == "16px"
+
+ mjml_content = txt.render_mjml()
+ assert 'color="red"' in mjml_content
+ assert 'font-size="16px"' in mjml_content
+ assert "Hello" in mjml_content
+
+
+def test_leaf_tag_no_positional_children():
+ # Leaf tags only have attributes and content parameters
+ # Passing a positional arg should fail
+ with pytest.raises(TypeError):
+ text(section(content="child_not_allowed"))
+
+
+def test_button_is_leaf_tag():
+ btn = button(attributes={"href": "https://example.com"}, content="Click Me")
+ assert btn.tagName == "mj-button"
+ assert btn.content == "Click Me"
+ assert btn.attrs["href"] == "https://example.com"
+
+ mjml_content = btn.render_mjml()
+ assert 'href="https://example.com"' in mjml_content
+ assert "Click Me" in mjml_content
+ assert "Custom HTML")
+ assert r.tagName == "mj-raw"
+ assert r.content == "Custom HTML
"
+
+ mjml_content = r.render_mjml()
+ assert mjml_content == "\nCustom HTML
\n"
+
+
+def test_table_tag():
+ tbl = table(content="")
+ assert tbl.tagName == "mj-table"
+ assert "" in tbl.content
+
+ mjml_content = tbl.render_mjml()
+ assert "" in mjml_content
+ assert "" in mjml_content
+
+
+def test_mjml_full_document():
+ doc = mjml(
+ head(),
+ body(
+ section(
+ column(
+ text(content="Hello World")
+ )
+ )
+ )
+ )
+ assert doc.tagName == "mjml"
+ assert len(doc.children) == 2
+ assert doc.children[0].tagName == "mj-head"
+ assert doc.children[1].tagName == "mj-body"
+
+
+def test_image_tag():
+ img = image(attributes={"src": "https://example.com/image.jpg", "alt": "Test Image"})
+ assert img.tagName == "mj-image"
+ assert img.attrs["src"] == "https://example.com/image.jpg"
+ assert img.attrs["alt"] == "Test Image"
+
+ mjml_content = img.render_mjml()
+ assert 'src="https://example.com/image.jpg"' in mjml_content
+ assert 'alt="Test Image"' in mjml_content
+ assert "" in mjml_content
+ assert "" in mjml_content
+ assert '=45", "wheel", "setuptools_scm>=6.2"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools_scm]
+
+[tool.pytest.ini_options]
+minversion = "6.0"
+addopts = "-ra --cov=emailer_lib --cov-report=term-missing"
+testpaths = ["emailer_lib/**/tests"]
+
[project]
-name = "email-for-data-science"
-version = "0.1.0"
-description = "Add your description here"
+name = "emailer-lib"
+version = "0.0.1"
+description = "Email serialization and sending utilities"
+authors = [{name = "Jules Walzer-Goldfeld"}]
readme = "README.md"
+
requires-python = ">=3.9"
+
dependencies = [
+ "dotenv",
+ "mjml-python>=1.3.6",
+]
+
+[project.optional-dependencies]
+dev = [
+ "aiosmtpd",
+ "pytest",
+ "quartodoc",
+ "pytest-cov",
+ "griffe",
+]
+
+docs = [
"redmail>=0.6.0",
"jupyter",
"ipykernel>=6.29.5",
- "dotenv",
"nbformat",
"nbclient",
"great-tables>=0.18.0",
@@ -17,14 +44,13 @@ dependencies = [
"css-inline>=0.17.0",
"plotnine>=0.13.6",
"pyarrow>=21.0.0",
- "mjml-python>=1.3.6",
- "quartodoc>=0.11.1",
]
-[dependency-groups]
-dev = [
- "quartodoc",
- "pytest>=3",
- "pytest-cov",
- "griffe",
+[tool.coverage.report]
+exclude_also = [
+ "if TYPE_CHECKING:"
+]
+include = ["emailer_lib/*"]
+omit = [
+ "emailer_lib/tests/*"
]
diff --git a/reference/IntermediateEmail.preview_send_email.qmd b/reference/IntermediateEmail.preview_send_email.qmd
deleted file mode 100644
index 6a4976d..0000000
--- a/reference/IntermediateEmail.preview_send_email.qmd
+++ /dev/null
@@ -1,22 +0,0 @@
-# IntermediateEmail.preview_send_email { #emailer_lib.IntermediateEmail.preview_send_email }
-
-```python
-IntermediateEmail.preview_send_email()
-```
-
-Send a preview of the email to a test recipient.
-
-This method is intended for sending the email to a designated preview recipient
-for testing purposes before sending to the full recipient list.
-
-## Returns {.doc-section .doc-section-returns}
-
-[]{.parameter-name} [:]{.parameter-annotation-sep} [None]{.parameter-annotation}
-
-:
-
-## Examples {.doc-section .doc-section-examples}
-
-```python
-email.preview_send_email()
-```
\ No newline at end of file
diff --git a/reference/IntermediateEmail.qmd b/reference/IntermediateEmail.qmd
deleted file mode 100644
index 5db5e37..0000000
--- a/reference/IntermediateEmail.qmd
+++ /dev/null
@@ -1,73 +0,0 @@
-# IntermediateEmail { #emailer_lib.IntermediateEmail }
-
-```python
-IntermediateEmail(
- html,
- subject,
- rsc_email_supress_report_attachment=None,
- rsc_email_supress_scheduled=None,
- external_attachments=None,
- inline_attachments=None,
- text=None,
- recipients=None,
-)
-```
-
-A serializable, previewable, sendable email object for data science workflows.
-
-The `IntermediateEmail` class provides a unified structure for representing email messages,
-including HTML and plain text content, subject, inline or external attachments, and recipients.
-It is designed to be generated from a variety of authoring tools and sent via multiple providers.
-
-## Parameters {.doc-section .doc-section-parameters}
-
-[**html**]{.parameter-name} [:]{.parameter-annotation-sep} [str]{.parameter-annotation}
-
-: The HTML content of the email.
-
-[**subject**]{.parameter-name} [:]{.parameter-annotation-sep} [str]{.parameter-annotation}
-
-: The subject line of the email.
-
-[**external_attachments**]{.parameter-name} [:]{.parameter-annotation-sep} [list\[str\] \| None]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default}
-
-: List of file paths for external attachments to include.
-
-[**inline_attachments**]{.parameter-name} [:]{.parameter-annotation-sep} [dict\[str, str\] \| None]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default}
-
-: Dictionary mapping filenames to base64-encoded strings for inline attachments.
-
-[**text**]{.parameter-name} [:]{.parameter-annotation-sep} [str \| None]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default}
-
-: Optional plain text version of the email.
-
-[**recipients**]{.parameter-name} [:]{.parameter-annotation-sep} [list\[str\] \| None]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default}
-
-: Optional list of recipient email addresses.
-
-[**rsc_email_supress_report_attachment**]{.parameter-name} [:]{.parameter-annotation-sep} [bool \| None]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default}
-
-: Whether to suppress report attachments (used in some workflows).
-
-[**rsc_email_supress_scheduled**]{.parameter-name} [:]{.parameter-annotation-sep} [bool \| None]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default}
-
-: Whether to suppress scheduled sending (used in some workflows).
-
-## Examples {.doc-section .doc-section-examples}
-
-```python
-email = IntermediateEmail(
- html="Hello world
",
- subject="Test Email",
- recipients=["user@example.com"],
-)
-email.write_preview_email("preview.html")
-```
-
-## Methods
-
-| Name | Description |
-| --- | --- |
-| [preview_send_email](emailer_lib.IntermediateEmail.preview_send_email.qmd#emailer_lib.IntermediateEmail.preview_send_email) | Send a preview of the email to a test recipient. |
-| [write_email_message](emailer_lib.IntermediateEmail.write_email_message.qmd#emailer_lib.IntermediateEmail.write_email_message) | Convert the IntermediateEmail to a Python EmailMessage. |
-| [write_preview_email](emailer_lib.IntermediateEmail.write_preview_email.qmd#emailer_lib.IntermediateEmail.write_preview_email) | Write a preview HTML file with inline attachments embedded. |
\ No newline at end of file
diff --git a/reference/IntermediateEmail.write_email_message.qmd b/reference/IntermediateEmail.write_email_message.qmd
deleted file mode 100644
index 63d4921..0000000
--- a/reference/IntermediateEmail.write_email_message.qmd
+++ /dev/null
@@ -1,22 +0,0 @@
-# IntermediateEmail.write_email_message { #emailer_lib.IntermediateEmail.write_email_message }
-
-```python
-IntermediateEmail.write_email_message()
-```
-
-Convert the IntermediateEmail to a Python EmailMessage.
-
-This method creates a standard library EmailMessage object from the
-IntermediateEmail, including HTML, plain text, recipients, and attachments.
-
-## Returns {.doc-section .doc-section-returns}
-
-[]{.parameter-name} [:]{.parameter-annotation-sep} [EmailMessage]{.parameter-annotation}
-
-: The constructed EmailMessage object.
-
-## Examples {.doc-section .doc-section-examples}
-
-```python
-msg = email.write_email_message()
-```
\ No newline at end of file
diff --git a/reference/IntermediateEmail.write_preview_email.qmd b/reference/IntermediateEmail.write_preview_email.qmd
deleted file mode 100644
index 21894f5..0000000
--- a/reference/IntermediateEmail.write_preview_email.qmd
+++ /dev/null
@@ -1,32 +0,0 @@
-# IntermediateEmail.write_preview_email { #emailer_lib.IntermediateEmail.write_preview_email }
-
-```python
-IntermediateEmail.write_preview_email(out_file='preview_email.html')
-```
-
-Write a preview HTML file with inline attachments embedded.
-
-This method replaces image sources in the HTML with base64-encoded data from
-inline attachments, allowing you to preview the email as it would appear to recipients.
-
-## Parameters {.doc-section .doc-section-parameters}
-
-[**out_file**]{.parameter-name} [:]{.parameter-annotation-sep} [str]{.parameter-annotation} [ = ]{.parameter-default-sep} [\'preview_email.html\']{.parameter-default}
-
-: The file path to write the preview HTML. Defaults to "preview_email.html".
-
-## Returns {.doc-section .doc-section-returns}
-
-[]{.parameter-name} [:]{.parameter-annotation-sep} [None]{.parameter-annotation}
-
-:
-
-## Examples {.doc-section .doc-section-examples}
-
-```python
-email.write_preview_email("preview.html")
-```
-
-## Notes {.doc-section .doc-section-notes}
-
-Raises ValueError if external attachments are present, as preview does not support them.
\ No newline at end of file
diff --git a/reference/_styles-quartodoc.css b/reference/_styles-quartodoc.css
deleted file mode 100644
index 51714ba..0000000
--- a/reference/_styles-quartodoc.css
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
-This file generated automatically by quartodoc version 0.11.1.
-Modifications may be overwritten by quartodoc build. If you want to
-customize styles, create a new .css file to avoid losing changes.
-*/
-
-
-/* styles for parameter tables, etc.. ----
-*/
-
-.doc-section dt code {
- background: none;
-}
-
-.doc-section dt {
- /* background-color: lightyellow; */
- display: block;
-}
-
-.doc-section dl dd {
- margin-left: 3rem;
-}
diff --git a/reference/emailer_lib.IntermediateEmail.preview_send_email.qmd b/reference/emailer_lib.IntermediateEmail.preview_send_email.qmd
deleted file mode 100644
index 7398b00..0000000
--- a/reference/emailer_lib.IntermediateEmail.preview_send_email.qmd
+++ /dev/null
@@ -1,22 +0,0 @@
-# preview_send_email { #emailer_lib.IntermediateEmail.preview_send_email }
-
-```python
-IntermediateEmail.preview_send_email()
-```
-
-Send a preview of the email to a test recipient.
-
-This method is intended for sending the email to a designated preview recipient
-for testing purposes before sending to the full recipient list.
-
-## Returns {.doc-section .doc-section-returns}
-
-[]{.parameter-name} [:]{.parameter-annotation-sep} [None]{.parameter-annotation}
-
-:
-
-## Examples {.doc-section .doc-section-examples}
-
-```python
-email.preview_send_email()
-```
\ No newline at end of file
diff --git a/reference/emailer_lib.IntermediateEmail.write_email_message.qmd b/reference/emailer_lib.IntermediateEmail.write_email_message.qmd
deleted file mode 100644
index e23a485..0000000
--- a/reference/emailer_lib.IntermediateEmail.write_email_message.qmd
+++ /dev/null
@@ -1,22 +0,0 @@
-# write_email_message { #emailer_lib.IntermediateEmail.write_email_message }
-
-```python
-IntermediateEmail.write_email_message()
-```
-
-Convert the IntermediateEmail to a Python EmailMessage.
-
-This method creates a standard library EmailMessage object from the
-IntermediateEmail, including HTML, plain text, recipients, and attachments.
-
-## Returns {.doc-section .doc-section-returns}
-
-[]{.parameter-name} [:]{.parameter-annotation-sep} [EmailMessage]{.parameter-annotation}
-
-: The constructed EmailMessage object.
-
-## Examples {.doc-section .doc-section-examples}
-
-```python
-msg = email.write_email_message()
-```
\ No newline at end of file
diff --git a/reference/emailer_lib.IntermediateEmail.write_preview_email.qmd b/reference/emailer_lib.IntermediateEmail.write_preview_email.qmd
deleted file mode 100644
index 2946061..0000000
--- a/reference/emailer_lib.IntermediateEmail.write_preview_email.qmd
+++ /dev/null
@@ -1,32 +0,0 @@
-# write_preview_email { #emailer_lib.IntermediateEmail.write_preview_email }
-
-```python
-IntermediateEmail.write_preview_email(out_file='preview_email.html')
-```
-
-Write a preview HTML file with inline attachments embedded.
-
-This method replaces image sources in the HTML with base64-encoded data from
-inline attachments, allowing you to preview the email as it would appear to recipients.
-
-## Parameters {.doc-section .doc-section-parameters}
-
-[**out_file**]{.parameter-name} [:]{.parameter-annotation-sep} [str]{.parameter-annotation} [ = ]{.parameter-default-sep} [\'preview_email.html\']{.parameter-default}
-
-: The file path to write the preview HTML. Defaults to "preview_email.html".
-
-## Returns {.doc-section .doc-section-returns}
-
-[]{.parameter-name} [:]{.parameter-annotation-sep} [None]{.parameter-annotation}
-
-:
-
-## Examples {.doc-section .doc-section-examples}
-
-```python
-email.write_preview_email("preview.html")
-```
-
-## Notes {.doc-section .doc-section-notes}
-
-Raises ValueError if external attachments are present, as preview does not support them.
\ No newline at end of file
diff --git a/reference/index.qmd b/reference/index.qmd
deleted file mode 100644
index 9d86d34..0000000
--- a/reference/index.qmd
+++ /dev/null
@@ -1,48 +0,0 @@
-# API Reference {.doc .doc-index}
-
-## The Email Object
-
-An email object that in a serializable, previewable format, optimized for emails with content generated by data scientists.
-
-
-| | |
-| --- | --- |
-| [IntermediateEmail](IntermediateEmail.qmd#emailer_lib.IntermediateEmail) | A serializable, previewable, sendable email object for data science workflows. |
-| [IntermediateEmail.write_preview_email](IntermediateEmail.write_preview_email.qmd#emailer_lib.IntermediateEmail.write_preview_email) | Write a preview HTML file with inline attachments embedded. |
-| [IntermediateEmail.write_email_message](IntermediateEmail.write_email_message.qmd#emailer_lib.IntermediateEmail.write_email_message) | Convert the IntermediateEmail to a Python EmailMessage. |
-| [IntermediateEmail.preview_send_email](IntermediateEmail.preview_send_email.qmd#emailer_lib.IntermediateEmail.preview_send_email) | Send a preview of the email to a test recipient. |
-
-## Uploading emails
-
-Converting emails to IntermediateEmails, at which point they can be previewed, tested, and sent.
-
-
-| | |
-| --- | --- |
-| [quarto_json_to_intermediate_email](quarto_json_to_intermediate_email.qmd#emailer_lib.quarto_json_to_intermediate_email) | Convert a Quarto output metadata JSON file to an IntermediateEmail |
-| [mjml_to_intermediate_email](mjml_to_intermediate_email.qmd#emailer_lib.mjml_to_intermediate_email) | Convert MJML markup to an IntermediateEmail |
-| [redmail_to_intermediate_email](redmail_to_intermediate_email.qmd#emailer_lib.redmail_to_intermediate_email) | Convert a Redmail EmailMessage object to an IntermediateEmail |
-| [yagmail_to_intermediate_email](yagmail_to_intermediate_email.qmd#emailer_lib.yagmail_to_intermediate_email) | Convert a Yagmail email object to an IntermediateEmail |
-
-## Sending
-
-Functions to sending emails with different providers. And a special handy one to bypass the intermediate object if you are sending a quarto email.
-
-
-| | |
-| --- | --- |
-| [send_intermediate_email_with_gmail](send_intermediate_email_with_gmail.qmd#emailer_lib.send_intermediate_email_with_gmail) | Send an Intermediate Email object via Gmail. |
-| [send_intermediate_email_with_smtp](send_intermediate_email_with_smtp.qmd#emailer_lib.send_intermediate_email_with_smtp) | Send an Intermediate Email object via SMTP. |
-| [send_intermediate_email_with_redmail](send_intermediate_email_with_redmail.qmd#emailer_lib.send_intermediate_email_with_redmail) | Send an Intermediate Email object via Redmail. |
-| [send_intermediate_email_with_yagmail](send_intermediate_email_with_yagmail.qmd#emailer_lib.send_intermediate_email_with_yagmail) | Send an Intermediate Email object via Yagmail. |
-| [send_intermediate_email_with_mailgun](send_intermediate_email_with_mailgun.qmd#emailer_lib.send_intermediate_email_with_mailgun) | Send an Intermediate Email object via Mailgun. |
-| [send_quarto_email_with_gmail](send_quarto_email_with_gmail.qmd#emailer_lib.send_quarto_email_with_gmail) | Send an email using Gmail with content from a Quarto metadata JSON file. |
-
-## Utilities
-
-Previews and more
-
-
-| | |
-| --- | --- |
-| [write_email_message_to_file](write_email_message_to_file.qmd#emailer_lib.write_email_message_to_file) | Writes the HTML content of an email message to a file, inlining any images referenced by Content-ID (cid). |
\ No newline at end of file
diff --git a/reference/mjml_to_intermediate_email.qmd b/reference/mjml_to_intermediate_email.qmd
deleted file mode 100644
index b828f86..0000000
--- a/reference/mjml_to_intermediate_email.qmd
+++ /dev/null
@@ -1,19 +0,0 @@
-# mjml_to_intermediate_email { #emailer_lib.mjml_to_intermediate_email }
-
-```python
-mjml_to_intermediate_email(mjml_content)
-```
-
-Convert MJML markup to an IntermediateEmail
-
-## Parameters {.doc-section .doc-section-parameters}
-
-[**mjml_content**]{.parameter-name} [:]{.parameter-annotation-sep} [str]{.parameter-annotation}
-
-: MJML markup string
-
-## Returns {.doc-section .doc-section-returns}
-
-[]{.parameter-name} [:]{.parameter-annotation-sep} [An Intermediate Email object]{.parameter-annotation}
-
-:
\ No newline at end of file
diff --git a/reference/quarto_json_to_intermediate_email.qmd b/reference/quarto_json_to_intermediate_email.qmd
deleted file mode 100644
index b3c8539..0000000
--- a/reference/quarto_json_to_intermediate_email.qmd
+++ /dev/null
@@ -1,13 +0,0 @@
-# quarto_json_to_intermediate_email { #emailer_lib.quarto_json_to_intermediate_email }
-
-```python
-quarto_json_to_intermediate_email(path)
-```
-
-Convert a Quarto output metadata JSON file to an IntermediateEmail
-
-## Parameters {.doc-section .doc-section-parameters}
-
-[**path**]{.parameter-name} [:]{.parameter-annotation-sep} [str]{.parameter-annotation}
-
-: Path to the Quarto output metadata JSON file
\ No newline at end of file
diff --git a/reference/redmail_to_intermediate_email.qmd b/reference/redmail_to_intermediate_email.qmd
deleted file mode 100644
index 7e03d00..0000000
--- a/reference/redmail_to_intermediate_email.qmd
+++ /dev/null
@@ -1,14 +0,0 @@
-# redmail_to_intermediate_email { #emailer_lib.redmail_to_intermediate_email }
-
-```python
-redmail_to_intermediate_email(msg)
-```
-
-Convert a Redmail EmailMessage object to an IntermediateEmail
-
-## Params {.doc-section .doc-section-params}
-
-msg
- The Redmail-generated EmailMessage object
-
-Converts the input EmailMessage to the intermediate email structure
\ No newline at end of file
diff --git a/reference/send_intermediate_email_with_gmail.qmd b/reference/send_intermediate_email_with_gmail.qmd
deleted file mode 100644
index 509d95b..0000000
--- a/reference/send_intermediate_email_with_gmail.qmd
+++ /dev/null
@@ -1,39 +0,0 @@
-# send_intermediate_email_with_gmail { #emailer_lib.send_intermediate_email_with_gmail }
-
-```python
-send_intermediate_email_with_gmail(username, password, i_email)
-```
-
-Send an Intermediate Email object via Gmail.
-
-## Parameters {.doc-section .doc-section-parameters}
-
-[**username**]{.parameter-name} [:]{.parameter-annotation-sep} [str]{.parameter-annotation}
-
-: Gmail account username for sending the email
-
-[**password**]{.parameter-name} [:]{.parameter-annotation-sep} [str]{.parameter-annotation}
-
-: Gmail app password
-
-[**i_email**]{.parameter-name} [:]{.parameter-annotation-sep} [IntermediateEmail]{.parameter-annotation}
-
-: IntermediateEmail object containing the email content and attachments
-
-## Returns {.doc-section .doc-section-returns}
-
-[]{.parameter-name} [:]{.parameter-annotation-sep} [None]{.parameter-annotation}
-
-: The function sends an email but doesn't return a value
-
-## Examples {.doc-section .doc-section-examples}
-
-```python
-email = IntermediateEmail(
- html="Hello world
",
- subject="Test Email",
- recipients=["user@example.com"],
-)
-
-send_intermediate_email_with_gmail("user@gmail.com", "password123", email)
-```
\ No newline at end of file
diff --git a/reference/send_intermediate_email_with_mailgun.qmd b/reference/send_intermediate_email_with_mailgun.qmd
deleted file mode 100644
index fc7f9f5..0000000
--- a/reference/send_intermediate_email_with_mailgun.qmd
+++ /dev/null
@@ -1,23 +0,0 @@
-# send_intermediate_email_with_mailgun { #emailer_lib.send_intermediate_email_with_mailgun }
-
-```python
-send_intermediate_email_with_mailgun(i_email)
-```
-
-Send an Intermediate Email object via Mailgun.
-
-## Parameters {.doc-section .doc-section-parameters}
-
-[**i_email**]{.parameter-name} [:]{.parameter-annotation-sep} [IntermediateEmail]{.parameter-annotation}
-
-: IntermediateEmail object containing the email content and attachments
-
-## Returns {.doc-section .doc-section-returns}
-
-[]{.parameter-name} [:]{.parameter-annotation-sep} [None]{.parameter-annotation}
-
-:
-
-## Notes {.doc-section .doc-section-notes}
-
-This function is a placeholder and has not been implemented yet.
\ No newline at end of file
diff --git a/reference/send_intermediate_email_with_redmail.qmd b/reference/send_intermediate_email_with_redmail.qmd
deleted file mode 100644
index 32c0d50..0000000
--- a/reference/send_intermediate_email_with_redmail.qmd
+++ /dev/null
@@ -1,23 +0,0 @@
-# send_intermediate_email_with_redmail { #emailer_lib.send_intermediate_email_with_redmail }
-
-```python
-send_intermediate_email_with_redmail(i_email)
-```
-
-Send an Intermediate Email object via Redmail.
-
-## Parameters {.doc-section .doc-section-parameters}
-
-[**i_email**]{.parameter-name} [:]{.parameter-annotation-sep} [IntermediateEmail]{.parameter-annotation}
-
-: IntermediateEmail object containing the email content and attachments
-
-## Returns {.doc-section .doc-section-returns}
-
-[]{.parameter-name} [:]{.parameter-annotation-sep} [None]{.parameter-annotation}
-
-:
-
-## Notes {.doc-section .doc-section-notes}
-
-This function is a placeholder and has not been implemented yet.
\ No newline at end of file
diff --git a/reference/send_intermediate_email_with_smtp.qmd b/reference/send_intermediate_email_with_smtp.qmd
deleted file mode 100644
index b353990..0000000
--- a/reference/send_intermediate_email_with_smtp.qmd
+++ /dev/null
@@ -1,92 +0,0 @@
-# send_intermediate_email_with_smtp { #emailer_lib.send_intermediate_email_with_smtp }
-
-```python
-send_intermediate_email_with_smtp(
- smtp_host,
- smtp_port,
- username,
- password,
- i_email,
- security=Literal['tls', 'ssl', 'smtp'],
-)
-```
-
-Send an Intermediate Email object via SMTP.
-
-## Parameters {.doc-section .doc-section-parameters}
-
-[**smtp_host**]{.parameter-name} [:]{.parameter-annotation-sep} [str]{.parameter-annotation}
-
-: SMTP server hostname (e.g., "smtp.example.com")
-
-[**smtp_port**]{.parameter-name} [:]{.parameter-annotation-sep} [int]{.parameter-annotation}
-
-: SMTP server port (typically 587 for TLS, 465 for SSL, 25 for plain SMTP)
-
-[**username**]{.parameter-name} [:]{.parameter-annotation-sep} [str]{.parameter-annotation}
-
-: SMTP account username for authentication
-
-[**password**]{.parameter-name} [:]{.parameter-annotation-sep} [str]{.parameter-annotation}
-
-: SMTP account password
-
-[**i_email**]{.parameter-name} [:]{.parameter-annotation-sep} [IntermediateEmail]{.parameter-annotation}
-
-: IntermediateEmail object containing the email content and attachments
-
-[**security**]{.parameter-name} [:]{.parameter-annotation-sep} [str]{.parameter-annotation} [ = ]{.parameter-default-sep} [Literal\[\'tls\', \'ssl\', \'smtp\'\]]{.parameter-default}
-
-: Security protocol to use: "tls" (STARTTLS), "ssl" (SSL/TLS), or "smtp" (plain SMTP). Default is "tls".
-
-## Returns {.doc-section .doc-section-returns}
-
-[]{.parameter-name} [:]{.parameter-annotation-sep} [None]{.parameter-annotation}
-
-: The function sends an email but doesn't return a value
-
-## Raises {.doc-section .doc-section-raises}
-
-[:]{.parameter-annotation-sep} [ValueError]{.parameter-annotation}
-
-: If security parameter is not one of "tls", "ssl", or "smtp"
-
-## Examples {.doc-section .doc-section-examples}
-
-```python
-email = IntermediateEmail(
- html="Hello world
",
- subject="Test Email",
- recipients=["user@example.com"],
-)
-
-# TLS connection (port 587) - recommended
-send_intermediate_email_with_smtp(
- "smtp.example.com",
- 587,
- "user@example.com",
- "password123",
- email,
- security="tls"
-)
-
-# SSL connection (port 465)
-send_intermediate_email_with_smtp(
- "smtp.example.com",
- 465,
- "user@example.com",
- "password123",
- email,
- security="ssl"
-)
-
-# Plain SMTP (port 25) - insecure, for testing only
-send_intermediate_email_with_smtp(
- "127.0.0.1",
- 8025,
- "test@example.com",
- "password",
- email,
- security="smtp"
-)
-```
\ No newline at end of file
diff --git a/reference/send_intermediate_email_with_yagmail.qmd b/reference/send_intermediate_email_with_yagmail.qmd
deleted file mode 100644
index 9cda074..0000000
--- a/reference/send_intermediate_email_with_yagmail.qmd
+++ /dev/null
@@ -1,23 +0,0 @@
-# send_intermediate_email_with_yagmail { #emailer_lib.send_intermediate_email_with_yagmail }
-
-```python
-send_intermediate_email_with_yagmail(i_email)
-```
-
-Send an Intermediate Email object via Yagmail.
-
-## Parameters {.doc-section .doc-section-parameters}
-
-[**i_email**]{.parameter-name} [:]{.parameter-annotation-sep} [IntermediateEmail]{.parameter-annotation}
-
-: IntermediateEmail object containing the email content and attachments
-
-## Returns {.doc-section .doc-section-returns}
-
-[]{.parameter-name} [:]{.parameter-annotation-sep} [None]{.parameter-annotation}
-
-:
-
-## Notes {.doc-section .doc-section-notes}
-
-This function is a placeholder and has not been implemented yet.
\ No newline at end of file
diff --git a/reference/send_quarto_email_with_gmail.qmd b/reference/send_quarto_email_with_gmail.qmd
deleted file mode 100644
index b152f26..0000000
--- a/reference/send_quarto_email_with_gmail.qmd
+++ /dev/null
@@ -1,42 +0,0 @@
-# send_quarto_email_with_gmail { #emailer_lib.send_quarto_email_with_gmail }
-
-```python
-send_quarto_email_with_gmail(username, password, json_path, recipients)
-```
-
-Send an email using Gmail with content from a Quarto metadata JSON file.
-
-## Parameters {.doc-section .doc-section-parameters}
-
-[**username**]{.parameter-name} [:]{.parameter-annotation-sep} [str]{.parameter-annotation}
-
-: Gmail account username for sending the email
-
-[**password**]{.parameter-name} [:]{.parameter-annotation-sep} [str]{.parameter-annotation}
-
-: Gmail app password
-
-[**json_path**]{.parameter-name} [:]{.parameter-annotation-sep} [str]{.parameter-annotation}
-
-: Path to the Quarto-generated .output_metadata.json file
-
-[**recipients**]{.parameter-name} [:]{.parameter-annotation-sep} [list\[str\]]{.parameter-annotation}
-
-: List of email addresses to send the email to
-
-## Returns {.doc-section .doc-section-returns}
-
-[]{.parameter-name} [:]{.parameter-annotation-sep} [None]{.parameter-annotation}
-
-: The function sends an email but doesn't return a value
-
-## Examples {.doc-section .doc-section-examples}
-
-```python
-send_quarto_email_with_gmail(
- "user@gmail.com",
- "password123",
- "path/to/output_metadata.json",
- ["recipient1@example.com", "recipient2@example.com"]
-)
-```
\ No newline at end of file
diff --git a/reference/styles.css b/reference/styles.css
deleted file mode 100644
index 9be2d3a..0000000
--- a/reference/styles.css
+++ /dev/null
@@ -1,4 +0,0 @@
-table.caption-top.table td a {
- display: inline-block;
- min-width: 18em;
-}
\ No newline at end of file
diff --git a/reference/write_email_message_to_file.qmd b/reference/write_email_message_to_file.qmd
deleted file mode 100644
index 6f1fd86..0000000
--- a/reference/write_email_message_to_file.qmd
+++ /dev/null
@@ -1,20 +0,0 @@
-# write_email_message_to_file { #emailer_lib.write_email_message_to_file }
-
-```python
-write_email_message_to_file(msg, out_file='preview_email.html')
-```
-
-Writes the HTML content of an email message to a file, inlining any images referenced by Content-ID (cid).
-
-This function extracts all attachments referenced by Content-ID from the given EmailMessage,
-replaces any `src="cid:..."` references in the HTML body with base64-encoded image data,
-and writes the resulting HTML to the specified output file.
-
-Params:
- msg
- The email message object containing the HTML body and attachments.
- out_file
- The path to the output HTML file.
-
-Returns:
- None
\ No newline at end of file
diff --git a/reference/yagmail_to_intermediate_email.qmd b/reference/yagmail_to_intermediate_email.qmd
deleted file mode 100644
index a8f9f7f..0000000
--- a/reference/yagmail_to_intermediate_email.qmd
+++ /dev/null
@@ -1,13 +0,0 @@
-# yagmail_to_intermediate_email { #emailer_lib.yagmail_to_intermediate_email }
-
-```python
-yagmail_to_intermediate_email()
-```
-
-Convert a Yagmail email object to an IntermediateEmail
-
-## Params {.doc-section .doc-section-params}
-
-(none)
-
-Not yet implemented
\ No newline at end of file
diff --git a/uv.lock b/uv.lock
index ca91185..944a3b8 100644
--- a/uv.lock
+++ b/uv.lock
@@ -9,6 +9,19 @@ resolution-markers = [
"python_full_version < '3.10'",
]
+[[package]]
+name = "aiosmtpd"
+version = "1.4.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "atpublic" },
+ { name = "attrs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c4/ca/b2b7cc880403ef24be77383edaadfcf0098f5d7b9ddbf3e2c17ef0a6af0d/aiosmtpd-1.4.6.tar.gz", hash = "sha256:5a811826e1a5a06c25ebc3e6c4a704613eb9a1bcf6b78428fbe865f4f6c9a4b8", size = 152775, upload-time = "2024-05-18T11:37:50.029Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/39/d401756df60a8344848477d54fdf4ce0f50531f6149f3b8eaae9c06ae3dc/aiosmtpd-1.4.6-py3-none-any.whl", hash = "sha256:72c99179ba5aa9ae0abbda6994668239b64a5ce054471955fe75f581d2592475", size = 154263, upload-time = "2024-05-18T11:37:47.877Z" },
+]
+
[[package]]
name = "annotated-types"
version = "0.7.0"
@@ -124,6 +137,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943", size = 6069, upload-time = "2025-03-16T17:25:35.422Z" },
]
+[[package]]
+name = "atpublic"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8c/78/a7c9b6d6581353204a7a099567783dd3352405b1662988892b9e67039c6c/atpublic-6.0.2.tar.gz", hash = "sha256:f90dcd17627ac21d5ce69e070d6ab89fb21736eb3277e8b693cc8484e1c7088c", size = 17708, upload-time = "2025-09-24T18:30:13.8Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/72/da/8916af0a074d24354d685fe4178a52d3fafd07b62e6f81124fdeac15594d/atpublic-6.0.2-py3-none-any.whl", hash = "sha256:156cfd3854e580ebfa596094a018fe15e4f3fa5bade74b39c3dabb54f12d6565", size = 6423, upload-time = "2025-09-24T18:30:15.214Z" },
+]
+
[[package]]
name = "attrs"
version = "25.4.0"
@@ -1020,16 +1042,27 @@ wheels = [
]
[[package]]
-name = "email-for-data-science"
-version = "0.1.0"
-source = { virtual = "." }
+name = "emailer-lib"
+version = "0.0.1"
+source = { editable = "." }
dependencies = [
- { name = "css-inline" },
{ name = "dotenv" },
+ { name = "mjml-python" },
+]
+
+[package.optional-dependencies]
+dev = [
+ { name = "aiosmtpd" },
+ { name = "griffe" },
+ { name = "pytest" },
+ { name = "pytest-cov" },
+ { name = "quartodoc" },
+]
+docs = [
+ { name = "css-inline" },
{ name = "great-tables" },
{ name = "ipykernel" },
{ name = "jupyter" },
- { name = "mjml-python" },
{ name = "nbclient" },
{ name = "nbformat" },
{ name = "pandas" },
@@ -1037,43 +1070,31 @@ dependencies = [
{ name = "plotnine", version = "0.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "polars" },
{ name = "pyarrow" },
- { name = "quartodoc" },
{ name = "redmail" },
]
-[package.dev-dependencies]
-dev = [
- { name = "griffe" },
- { name = "pytest" },
- { name = "pytest-cov" },
- { name = "quartodoc" },
-]
-
[package.metadata]
requires-dist = [
- { name = "css-inline", specifier = ">=0.17.0" },
+ { name = "aiosmtpd", marker = "extra == 'dev'" },
+ { name = "css-inline", marker = "extra == 'docs'", specifier = ">=0.17.0" },
{ name = "dotenv" },
- { name = "great-tables", specifier = ">=0.18.0" },
- { name = "ipykernel", specifier = ">=6.29.5" },
- { name = "jupyter" },
+ { name = "great-tables", marker = "extra == 'docs'", specifier = ">=0.18.0" },
+ { name = "griffe", marker = "extra == 'dev'" },
+ { name = "ipykernel", marker = "extra == 'docs'", specifier = ">=6.29.5" },
+ { name = "jupyter", marker = "extra == 'docs'" },
{ name = "mjml-python", specifier = ">=1.3.6" },
- { name = "nbclient" },
- { name = "nbformat" },
- { name = "pandas", specifier = ">=2.3.3" },
- { name = "plotnine", specifier = ">=0.13.6" },
- { name = "polars", specifier = ">=1.34.0" },
- { name = "pyarrow", specifier = ">=21.0.0" },
- { name = "quartodoc", specifier = ">=0.11.1" },
- { name = "redmail", specifier = ">=0.6.0" },
-]
-
-[package.metadata.requires-dev]
-dev = [
- { name = "griffe" },
- { name = "pytest", specifier = ">=3" },
- { name = "pytest-cov" },
- { name = "quartodoc" },
-]
+ { name = "nbclient", marker = "extra == 'docs'" },
+ { name = "nbformat", marker = "extra == 'docs'" },
+ { name = "pandas", marker = "extra == 'docs'", specifier = ">=2.3.3" },
+ { name = "plotnine", marker = "extra == 'docs'", specifier = ">=0.13.6" },
+ { name = "polars", marker = "extra == 'docs'", specifier = ">=1.34.0" },
+ { name = "pyarrow", marker = "extra == 'docs'", specifier = ">=21.0.0" },
+ { name = "pytest", marker = "extra == 'dev'" },
+ { name = "pytest-cov", marker = "extra == 'dev'" },
+ { name = "quartodoc", marker = "extra == 'dev'" },
+ { name = "redmail", marker = "extra == 'docs'", specifier = ">=0.6.0" },
+]
+provides-extras = ["dev", "docs"]
[[package]]
name = "exceptiongroup"