From 640352b62230e18a472624c418049cb9b3eb8bce Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:34:00 -0400 Subject: [PATCH 01/29] re-arrange docs structure --- Makefile | 11 +++ README.md | 85 +++++++++++++++- cover.png | Bin 51194 -> 0 bytes docs/.gitignore | 1 + .../.output_metadata.json | 0 _quarto.yml => docs/_quarto.yml | 0 {assets => docs/assets}/mjml-email-full.png | Bin .../assets}/whole-game-email-annotated.png | Bin {assets => docs/assets}/whole-game-email.png | Bin {assets => docs/assets}/whole-game-fancy.png | Bin {assets => docs/assets}/whole-game-quarto.png | Bin .../configuring-attachments.qmd | 0 .../configuring-subject.qmd | 0 .../content-embedding.qmd | 0 content-layout.qmd => docs/content-layout.qmd | 0 data_polars.py => docs/data_polars.py | 0 index.qmd => docs/index.qmd | 0 objects.json => docs/objects.json | 0 .../orchestrating-auth.qmd | 0 .../orchestrating-tests.qmd | 0 .../IntermediateEmail.preview_send_email.qmd | 0 .../reference}/IntermediateEmail.qmd | 0 .../IntermediateEmail.write_email_message.qmd | 0 .../IntermediateEmail.write_preview_email.qmd | 0 .../reference}/_styles-quartodoc.css | 0 ...b.IntermediateEmail.preview_send_email.qmd | 0 ....IntermediateEmail.write_email_message.qmd | 0 ....IntermediateEmail.write_preview_email.qmd | 0 {reference => docs/reference}/index.qmd | 0 .../reference}/mjml_to_intermediate_email.qmd | 0 .../quarto_json_to_intermediate_email.qmd | 0 .../redmail_to_intermediate_email.qmd | 0 .../send_intermediate_email_with_gmail.qmd | 0 .../send_intermediate_email_with_mailgun.qmd | 0 .../send_intermediate_email_with_redmail.qmd | 0 .../send_intermediate_email_with_smtp.qmd | 0 .../send_intermediate_email_with_yagmail.qmd | 0 .../send_quarto_email_with_gmail.qmd | 0 {reference => docs/reference}/styles.css | 0 .../write_email_message_to_file.qmd | 0 .../yagmail_to_intermediate_email.qmd | 0 summary.qmd => docs/summary.qmd | 0 emailer-lib/.gitignore | 2 - emailer-lib/Makefile | 5 - emailer-lib/README.md | 84 ---------------- emailer-lib/pyproject.toml | 40 -------- .../emailer_lib => emailer_lib}/__init__.py | 0 .../emailer_lib => emailer_lib}/egress.py | 0 .../emailer_lib => emailer_lib}/ingress.py | 0 .../emailer_lib => emailer_lib}/structs.py | 0 .../tests/test_egress.py | 0 .../tests/test_end_to_end.py | 0 .../tests/test_ingress.py | 0 .../tests/test_structs.py | 0 .../tests/test_utils.py | 0 .../emailer_lib => emailer_lib}/utils.py | 0 pyproject.toml | 50 +++++++--- uv.lock | 91 +++++++++++------- 58 files changed, 190 insertions(+), 179 deletions(-) create mode 100644 Makefile delete mode 100644 cover.png create mode 100644 docs/.gitignore rename .output_metadata.json => docs/.output_metadata.json (100%) rename _quarto.yml => docs/_quarto.yml (100%) rename {assets => docs/assets}/mjml-email-full.png (100%) rename {assets => docs/assets}/whole-game-email-annotated.png (100%) rename {assets => docs/assets}/whole-game-email.png (100%) rename {assets => docs/assets}/whole-game-fancy.png (100%) rename {assets => docs/assets}/whole-game-quarto.png (100%) rename configuring-attachments.qmd => docs/configuring-attachments.qmd (100%) rename configuring-subject.qmd => docs/configuring-subject.qmd (100%) rename content-embedding.qmd => docs/content-embedding.qmd (100%) rename content-layout.qmd => docs/content-layout.qmd (100%) rename data_polars.py => docs/data_polars.py (100%) rename index.qmd => docs/index.qmd (100%) rename objects.json => docs/objects.json (100%) rename orchestrating-auth.qmd => docs/orchestrating-auth.qmd (100%) rename orchestrating-tests.qmd => docs/orchestrating-tests.qmd (100%) rename {reference => docs/reference}/IntermediateEmail.preview_send_email.qmd (100%) rename {reference => docs/reference}/IntermediateEmail.qmd (100%) rename {reference => docs/reference}/IntermediateEmail.write_email_message.qmd (100%) rename {reference => docs/reference}/IntermediateEmail.write_preview_email.qmd (100%) rename {reference => docs/reference}/_styles-quartodoc.css (100%) rename {reference => docs/reference}/emailer_lib.IntermediateEmail.preview_send_email.qmd (100%) rename {reference => docs/reference}/emailer_lib.IntermediateEmail.write_email_message.qmd (100%) rename {reference => docs/reference}/emailer_lib.IntermediateEmail.write_preview_email.qmd (100%) rename {reference => docs/reference}/index.qmd (100%) rename {reference => docs/reference}/mjml_to_intermediate_email.qmd (100%) rename {reference => docs/reference}/quarto_json_to_intermediate_email.qmd (100%) rename {reference => docs/reference}/redmail_to_intermediate_email.qmd (100%) rename {reference => docs/reference}/send_intermediate_email_with_gmail.qmd (100%) rename {reference => docs/reference}/send_intermediate_email_with_mailgun.qmd (100%) rename {reference => docs/reference}/send_intermediate_email_with_redmail.qmd (100%) rename {reference => docs/reference}/send_intermediate_email_with_smtp.qmd (100%) rename {reference => docs/reference}/send_intermediate_email_with_yagmail.qmd (100%) rename {reference => docs/reference}/send_quarto_email_with_gmail.qmd (100%) rename {reference => docs/reference}/styles.css (100%) rename {reference => docs/reference}/write_email_message_to_file.qmd (100%) rename {reference => docs/reference}/yagmail_to_intermediate_email.qmd (100%) rename summary.qmd => docs/summary.qmd (100%) delete mode 100644 emailer-lib/.gitignore delete mode 100644 emailer-lib/Makefile delete mode 100644 emailer-lib/README.md delete mode 100644 emailer-lib/pyproject.toml rename {emailer-lib/emailer_lib => emailer_lib}/__init__.py (100%) rename {emailer-lib/emailer_lib => emailer_lib}/egress.py (100%) rename {emailer-lib/emailer_lib => emailer_lib}/ingress.py (100%) rename {emailer-lib/emailer_lib => emailer_lib}/structs.py (100%) rename {emailer-lib/emailer_lib => emailer_lib}/tests/test_egress.py (100%) rename {emailer-lib/emailer_lib => emailer_lib}/tests/test_end_to_end.py (100%) rename {emailer-lib/emailer_lib => emailer_lib}/tests/test_ingress.py (100%) rename {emailer-lib/emailer_lib => emailer_lib}/tests/test_structs.py (100%) rename {emailer-lib/emailer_lib => emailer_lib}/tests/test_utils.py (100%) rename {emailer-lib/emailer_lib => emailer_lib}/utils.py (100%) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..106ca17 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +build-docs: + cd docs && uv run quartodoc build --verbose && quarto render + +preview: + cd docs && quarto preview + +test: + pytest --cov-report=xml + +test-update: + pytest --snapshot-update \ No newline at end of file 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 + + + + + + + +[![Documentation](https://img.shields.io/badge/docs-project_website-blue.svg)](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 e1f5bc61d11049afe5b5dc2a67a3a32f406be964..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51194 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

sLQu|F-aZhE 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�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 Vyw!|9>LU1OnX%cT*addJ{|^#&e**vj diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..075b254 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +/.quarto/ 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 100% rename from _quarto.yml rename to docs/_quarto.yml 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/objects.json b/docs/objects.json similarity index 100% rename from objects.json rename to docs/objects.json 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/reference/IntermediateEmail.preview_send_email.qmd b/docs/reference/IntermediateEmail.preview_send_email.qmd similarity index 100% rename from reference/IntermediateEmail.preview_send_email.qmd rename to docs/reference/IntermediateEmail.preview_send_email.qmd diff --git a/reference/IntermediateEmail.qmd b/docs/reference/IntermediateEmail.qmd similarity index 100% rename from reference/IntermediateEmail.qmd rename to docs/reference/IntermediateEmail.qmd diff --git a/reference/IntermediateEmail.write_email_message.qmd b/docs/reference/IntermediateEmail.write_email_message.qmd similarity index 100% rename from reference/IntermediateEmail.write_email_message.qmd rename to docs/reference/IntermediateEmail.write_email_message.qmd diff --git a/reference/IntermediateEmail.write_preview_email.qmd b/docs/reference/IntermediateEmail.write_preview_email.qmd similarity index 100% rename from reference/IntermediateEmail.write_preview_email.qmd rename to docs/reference/IntermediateEmail.write_preview_email.qmd diff --git a/reference/_styles-quartodoc.css b/docs/reference/_styles-quartodoc.css similarity index 100% rename from reference/_styles-quartodoc.css rename to docs/reference/_styles-quartodoc.css diff --git a/reference/emailer_lib.IntermediateEmail.preview_send_email.qmd b/docs/reference/emailer_lib.IntermediateEmail.preview_send_email.qmd similarity index 100% rename from reference/emailer_lib.IntermediateEmail.preview_send_email.qmd rename to docs/reference/emailer_lib.IntermediateEmail.preview_send_email.qmd diff --git a/reference/emailer_lib.IntermediateEmail.write_email_message.qmd b/docs/reference/emailer_lib.IntermediateEmail.write_email_message.qmd similarity index 100% rename from reference/emailer_lib.IntermediateEmail.write_email_message.qmd rename to docs/reference/emailer_lib.IntermediateEmail.write_email_message.qmd diff --git a/reference/emailer_lib.IntermediateEmail.write_preview_email.qmd b/docs/reference/emailer_lib.IntermediateEmail.write_preview_email.qmd similarity index 100% rename from reference/emailer_lib.IntermediateEmail.write_preview_email.qmd rename to docs/reference/emailer_lib.IntermediateEmail.write_preview_email.qmd diff --git a/reference/index.qmd b/docs/reference/index.qmd similarity index 100% rename from reference/index.qmd rename to docs/reference/index.qmd diff --git a/reference/mjml_to_intermediate_email.qmd b/docs/reference/mjml_to_intermediate_email.qmd similarity index 100% rename from reference/mjml_to_intermediate_email.qmd rename to docs/reference/mjml_to_intermediate_email.qmd diff --git a/reference/quarto_json_to_intermediate_email.qmd b/docs/reference/quarto_json_to_intermediate_email.qmd similarity index 100% rename from reference/quarto_json_to_intermediate_email.qmd rename to docs/reference/quarto_json_to_intermediate_email.qmd diff --git a/reference/redmail_to_intermediate_email.qmd b/docs/reference/redmail_to_intermediate_email.qmd similarity index 100% rename from reference/redmail_to_intermediate_email.qmd rename to docs/reference/redmail_to_intermediate_email.qmd diff --git a/reference/send_intermediate_email_with_gmail.qmd b/docs/reference/send_intermediate_email_with_gmail.qmd similarity index 100% rename from reference/send_intermediate_email_with_gmail.qmd rename to docs/reference/send_intermediate_email_with_gmail.qmd diff --git a/reference/send_intermediate_email_with_mailgun.qmd b/docs/reference/send_intermediate_email_with_mailgun.qmd similarity index 100% rename from reference/send_intermediate_email_with_mailgun.qmd rename to docs/reference/send_intermediate_email_with_mailgun.qmd diff --git a/reference/send_intermediate_email_with_redmail.qmd b/docs/reference/send_intermediate_email_with_redmail.qmd similarity index 100% rename from reference/send_intermediate_email_with_redmail.qmd rename to docs/reference/send_intermediate_email_with_redmail.qmd diff --git a/reference/send_intermediate_email_with_smtp.qmd b/docs/reference/send_intermediate_email_with_smtp.qmd similarity index 100% rename from reference/send_intermediate_email_with_smtp.qmd rename to docs/reference/send_intermediate_email_with_smtp.qmd diff --git a/reference/send_intermediate_email_with_yagmail.qmd b/docs/reference/send_intermediate_email_with_yagmail.qmd similarity index 100% rename from reference/send_intermediate_email_with_yagmail.qmd rename to docs/reference/send_intermediate_email_with_yagmail.qmd diff --git a/reference/send_quarto_email_with_gmail.qmd b/docs/reference/send_quarto_email_with_gmail.qmd similarity index 100% rename from reference/send_quarto_email_with_gmail.qmd rename to docs/reference/send_quarto_email_with_gmail.qmd diff --git a/reference/styles.css b/docs/reference/styles.css similarity index 100% rename from reference/styles.css rename to docs/reference/styles.css diff --git a/reference/write_email_message_to_file.qmd b/docs/reference/write_email_message_to_file.qmd similarity index 100% rename from reference/write_email_message_to_file.qmd rename to docs/reference/write_email_message_to_file.qmd diff --git a/reference/yagmail_to_intermediate_email.qmd b/docs/reference/yagmail_to_intermediate_email.qmd similarity index 100% rename from reference/yagmail_to_intermediate_email.qmd rename to docs/reference/yagmail_to_intermediate_email.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 - - - - - - - -[![Documentation](https://img.shields.io/badge/docs-project_website-blue.svg)](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/emailer_lib/structs.py b/emailer_lib/structs.py similarity index 100% rename from emailer-lib/emailer_lib/structs.py rename to emailer_lib/structs.py diff --git a/emailer-lib/emailer_lib/tests/test_egress.py b/emailer_lib/tests/test_egress.py similarity index 100% rename from emailer-lib/emailer_lib/tests/test_egress.py rename to emailer_lib/tests/test_egress.py diff --git a/emailer-lib/emailer_lib/tests/test_end_to_end.py b/emailer_lib/tests/test_end_to_end.py similarity index 100% rename from emailer-lib/emailer_lib/tests/test_end_to_end.py rename to emailer_lib/tests/test_end_to_end.py diff --git a/emailer-lib/emailer_lib/tests/test_ingress.py b/emailer_lib/tests/test_ingress.py similarity index 100% rename from emailer-lib/emailer_lib/tests/test_ingress.py rename to emailer_lib/tests/test_ingress.py diff --git a/emailer-lib/emailer_lib/tests/test_structs.py b/emailer_lib/tests/test_structs.py similarity index 100% rename from emailer-lib/emailer_lib/tests/test_structs.py rename to emailer_lib/tests/test_structs.py diff --git a/emailer-lib/emailer_lib/tests/test_utils.py b/emailer_lib/tests/test_utils.py similarity index 100% rename from emailer-lib/emailer_lib/tests/test_utils.py rename to emailer_lib/tests/test_utils.py diff --git a/emailer-lib/emailer_lib/utils.py b/emailer_lib/utils.py similarity index 100% rename from emailer-lib/emailer_lib/utils.py rename to emailer_lib/utils.py diff --git a/pyproject.toml b/pyproject.toml index 38a56ca..b5ae5da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,41 @@ +[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 = "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/*" +] \ 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" From 2f4a1a99c48c9a409e3531b9e5ef3b4fdb896293 Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:34:37 -0400 Subject: [PATCH 02/29] redirect import --- examples/conditional.py | 2 +- examples/embedded.py | 2 +- examples/mjml_email.py | 2 +- examples/whole_game.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/conditional.py b/examples/conditional.py index dac4b89..85c9ebf 100644 --- a/examples/conditional.py +++ b/examples/conditional.py @@ -7,7 +7,7 @@ # import sys # sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + "/..")) -from data_polars import sp500 +from docs.data_polars import sp500 load_dotenv() diff --git a/examples/embedded.py b/examples/embedded.py index 42e5896..f5fc646 100644 --- a/examples/embedded.py +++ b/examples/embedded.py @@ -10,7 +10,7 @@ # import sys # sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/..')) -from data_polars import sp500 +from docs.data_polars import sp500 import polars as pl from plotnine import ( ggplot, diff --git a/examples/mjml_email.py b/examples/mjml_email.py index 58862f8..a1ad150 100644 --- a/examples/mjml_email.py +++ b/examples/mjml_email.py @@ -10,7 +10,7 @@ # import sys # sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/..')) -from data_polars import sp500 +from docs.data_polars import sp500 import polars as pl from plotnine import ( ggplot, diff --git a/examples/whole_game.py b/examples/whole_game.py index dd1916a..e9e81dd 100644 --- a/examples/whole_game.py +++ b/examples/whole_game.py @@ -4,7 +4,7 @@ # import sys # sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/..')) -from data_polars import sp500 +from docs.data_polars import sp500 load_dotenv() From c4aa4811959664e05ac6957240e928040cf96722 Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:33:31 -0400 Subject: [PATCH 03/29] mjml generating tags --- Makefile | 5 +- docs/_quarto.yml | 39 +- docs/objects.json | 2 +- docs/reference/index.qmd | 43 +- emailer_lib/mjml/README.md | 198 +++++++ emailer_lib/mjml/__init__.py | 77 +++ emailer_lib/mjml/_core.py | 139 +++++ emailer_lib/mjml/tags.py | 825 ++++++++++++++++++++++++++++++ emailer_lib/mjml/tests/.gitfolder | 0 scripts/generate-tags.py | 147 ++++++ 10 files changed, 1471 insertions(+), 4 deletions(-) create mode 100644 emailer_lib/mjml/README.md create mode 100644 emailer_lib/mjml/__init__.py create mode 100644 emailer_lib/mjml/_core.py create mode 100644 emailer_lib/mjml/tags.py create mode 100644 emailer_lib/mjml/tests/.gitfolder create mode 100644 scripts/generate-tags.py diff --git a/Makefile b/Makefile index 106ca17..4e641ee 100644 --- a/Makefile +++ b/Makefile @@ -8,4 +8,7 @@ test: pytest --cov-report=xml test-update: - pytest --snapshot-update \ No newline at end of file + pytest --snapshot-update + +generate-mjml-tags: + python3 scripts/generate-tags.py diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 28841ce..9f16e21 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -70,13 +70,50 @@ 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.include + - mjml.attributes + - mjml.breakpoint + - mjml.font + - mjml.html_attributes + - 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/docs/objects.json b/docs/objects.json index 348111e..7dc4189 100644 --- a/docs/objects.json +++ b/docs/objects.json @@ -1 +1 @@ -{"project": "emailer_lib", "version": "0.0.9999", "count": 36, "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"}]} \ No newline at end of file +{"project": "emailer_lib", "version": "0.0.9999", "count": 102, "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.include", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.include.html#emailer_lib.mjml.include", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.include", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.include.html#emailer_lib.mjml.include", "dispname": "emailer_lib.mjml.include"}, {"name": "emailer_lib.mjml.attributes", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.attributes.html#emailer_lib.mjml.attributes", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.attributes", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.attributes.html#emailer_lib.mjml.attributes", "dispname": "emailer_lib.mjml.attributes"}, {"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.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/docs/reference/index.qmd b/docs/reference/index.qmd index 9d86d34..5f26bdd 100644 --- a/docs/reference/index.qmd +++ b/docs/reference/index.qmd @@ -45,4 +45,45 @@ 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 +| [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). | + +## MJML Authoring + +Write responsive emails with MJML + + +| | | +| --- | --- | +| [mjml.mjml](mjml.mjml.qmd#emailer_lib.mjml.mjml) | Create an MJML `` tag. | +| [mjml.head](mjml.head.qmd#emailer_lib.mjml.head) | Create an MJML `` tag. | +| [mjml.body](mjml.body.qmd#emailer_lib.mjml.body) | Create an MJML `` tag. | +| [mjml.include](mjml.include.qmd#emailer_lib.mjml.include) | Create an MJML `` tag. | +| [mjml.attributes](mjml.attributes.qmd#emailer_lib.mjml.attributes) | Create an MJML `` tag. | +| [mjml.breakpoint](mjml.breakpoint.qmd#emailer_lib.mjml.breakpoint) | Create an MJML `` tag. | +| [mjml.font](mjml.font.qmd#emailer_lib.mjml.font) | Create an MJML `` tag. | +| [mjml.html_attributes](mjml.html_attributes.qmd#emailer_lib.mjml.html_attributes) | Create an MJML `` tag. | +| [mjml.preview](mjml.preview.qmd#emailer_lib.mjml.preview) | Create an MJML `` tag. | +| [mjml.style](mjml.style.qmd#emailer_lib.mjml.style) | Create an MJML `` tag. | +| [mjml.title](mjml.title.qmd#emailer_lib.mjml.title) | Create an MJML `` tag. | +| [mjml.accordion](mjml.accordion.qmd#emailer_lib.mjml.accordion) | Create an MJML `` tag. | +| [mjml.accordion_element](mjml.accordion_element.qmd#emailer_lib.mjml.accordion_element) | Create an MJML `` tag. | +| [mjml.accordion_text](mjml.accordion_text.qmd#emailer_lib.mjml.accordion_text) | Create an MJML `` tag. | +| [mjml.accordion_title](mjml.accordion_title.qmd#emailer_lib.mjml.accordion_title) | Create an MJML `` tag. | +| [mjml.button](mjml.button.qmd#emailer_lib.mjml.button) | Create an MJML `` tag. | +| [mjml.carousel](mjml.carousel.qmd#emailer_lib.mjml.carousel) | Create an MJML `` tag. | +| [mjml.carousel_image](mjml.carousel_image.qmd#emailer_lib.mjml.carousel_image) | Create an MJML `` tag. | +| [mjml.column](mjml.column.qmd#emailer_lib.mjml.column) | Create an MJML `` tag. | +| [mjml.divider](mjml.divider.qmd#emailer_lib.mjml.divider) | Create an MJML `` tag. | +| [mjml.group](mjml.group.qmd#emailer_lib.mjml.group) | Create an MJML `` tag. | +| [mjml.hero](mjml.hero.qmd#emailer_lib.mjml.hero) | Create an MJML `` tag. | +| [mjml.image](mjml.image.qmd#emailer_lib.mjml.image) | Create an MJML `` tag. | +| [mjml.navbar](mjml.navbar.qmd#emailer_lib.mjml.navbar) | Create an MJML `` tag. | +| [mjml.navbar_link](mjml.navbar_link.qmd#emailer_lib.mjml.navbar_link) | Create an MJML `` tag. | +| [mjml.raw](mjml.raw.qmd#emailer_lib.mjml.raw) | Create an MJML `` tag. | +| [mjml.section](mjml.section.qmd#emailer_lib.mjml.section) | Create an MJML `` tag. | +| [mjml.social](mjml.social.qmd#emailer_lib.mjml.social) | Create an MJML `` tag. | +| [mjml.social_element](mjml.social_element.qmd#emailer_lib.mjml.social_element) | Create an MJML `` tag. | +| [mjml.spacer](mjml.spacer.qmd#emailer_lib.mjml.spacer) | Create an MJML `` tag. | +| [mjml.table](mjml.table.qmd#emailer_lib.mjml.table) | Create an MJML `` tag. | +| [mjml.text](mjml.text.qmd#emailer_lib.mjml.text) | Create an MJML `` tag. | +| [mjml.wrapper](mjml.wrapper.qmd#emailer_lib.mjml.wrapper) | Create an MJML `` tag. | \ No newline at end of file diff --git a/emailer_lib/mjml/README.md b/emailer_lib/mjml/README.md new file mode 100644 index 0000000..8d1f4dc --- /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 +``` + +## 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..7a9ab0b --- /dev/null +++ b/emailer_lib/mjml/__init__.py @@ -0,0 +1,77 @@ +# MJMLTools package init +# Exposes MJMLTag, TagAttrDict, and all tag functions + +from ._core import MJMLTag, TagAttrDict +from .tags import ( + mjml, + head, + body, + include, + attributes, + breakpoint, + font, + html_attributes, + 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", + "include", + "attributes", + "breakpoint", + "font", + "html_attributes", + "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..398bff6 --- /dev/null +++ b/emailer_lib/mjml/_core.py @@ -0,0 +1,139 @@ +# 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 Any, Dict, Iterable, Mapping, Optional, Sequence, TypeVar, Union +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], **kwargs: TagAttrValue + ) -> None: + super().__init__() + for mapping in args: + for k, v in mapping.items(): + if v is not None: + self[self._to_kebab_case(k)] = str(v) + for k, v in kwargs.items(): + if v is not None: + self[self._to_kebab_case(k)] = str(v) + + @staticmethod + def _to_kebab_case(s: str) -> str: + return s.replace('_', '-') + + def update(self, *args: Mapping[str, TagAttrValue], **kwargs: TagAttrValue) -> None: + for mapping in args: + for k, v in mapping.items(): + if v is not None: + self[self._to_kebab_case(k)] = str(v) + for k, v in kwargs.items(): + if v is not None: + self[self._to_kebab_case(k)] = str(v) + + +class MJMLTag: + """ + MJML tag class. + Differences from htmltools Tag: + - 'name' renamed to 'tagName' for MJML + - 'content' field for leaf tags (optional) + """ + + def __init__( + self, + tagName: str, + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, + ) -> None: + self.tagName = tagName + self.attrs = TagAttrDict() + self.children = [] + self.content = content + # Collect attributes and children + for arg in args: + if isinstance(arg, dict) or isinstance(arg, TagAttrDict): + self.attrs.update(arg) + elif ( + 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) + # Keyword attributes + self.attrs.update(**kwargs) + # If content is provided, children should be empty + if self.content is not None: + self.children = [] + + def render(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 + elif isinstance(c, Sequence) and not isinstance(c, str): + yield from _flatten(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(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}" + else: + return f"{pad}<{self.tagName}{attr_str}>" + + def _repr_html_(self): + return self.render() + + def __repr__(self) -> str: + return f"MJMLTag({self.tagName!r}, attrs={dict(self.attrs)!r}, children={self.children!r}, content={self.content!r})" + + def to_html(self, **mjml2html_kwargs): + """ + Render MJMLTag to HTML using mjml2html, only if this is the top-level tag. + """ + if self.tagName != "mjml": + raise TypeError("to_html() is only available on the top-level tag.") + + mjml_markup = self.render() + return mjml2html(mjml_markup, **mjml2html_kwargs) diff --git a/emailer_lib/mjml/tags.py b/emailer_lib/mjml/tags.py new file mode 100644 index 0000000..372cfd7 --- /dev/null +++ b/emailer_lib/mjml/tags.py @@ -0,0 +1,825 @@ +# 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: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mjml", *args, content=content, **kwargs) + + +def head( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-head", *args, content=content, **kwargs) + + +def body( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-body", *args, content=content, **kwargs) + + +def include( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-include", *args, content=content, **kwargs) + + +def attributes( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-attributes", *args, content=content, **kwargs) + + +def breakpoint( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-breakpoint", *args, content=content, **kwargs) + + +def font( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-font", *args, content=content, **kwargs) + + +def html_attributes( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-html-attributes", *args, content=content, **kwargs) + + +def preview( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-preview", *args, content=content, **kwargs) + + +def style( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-style", *args, content=content, **kwargs) + + +def title( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-title", *args, content=content, **kwargs) + + +def accordion( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-accordion", *args, content=content, **kwargs) + + +def accordion_element( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-accordion-element", *args, content=content, **kwargs) + + +def carousel( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-carousel", *args, content=content, **kwargs) + + +def column( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-column", *args, content=content, **kwargs) + + +def divider( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-divider", *args, content=content, **kwargs) + + +def group( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-group", *args, content=content, **kwargs) + + +def hero( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-hero", *args, content=content, **kwargs) + + +def image( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-image", *args, content=content, **kwargs) + + +def navbar( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-navbar", *args, content=content, **kwargs) + + +def section( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-section", *args, content=content, **kwargs) + + +def social( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-social", *args, content=content, **kwargs) + + +def spacer( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-spacer", *args, content=content, **kwargs) + + +def wrapper( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-wrapper", *args, content=content, **kwargs) + + +def accordion_text( + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + 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 + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-accordion-text", content=content, **kwargs) + + +def accordion_title( + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + 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 + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-accordion-title", content=content, **kwargs) + + +def button( + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + 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 + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-button", content=content, **kwargs) + + +def carousel_image( + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + 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 + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-carousel-image", content=content, **kwargs) + + +def navbar_link( + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + 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 + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-navbar-link", content=content, **kwargs) + + +def raw( + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + 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 + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-raw", content=content, **kwargs) + + +def social_element( + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + 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 + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-social-element", content=content, **kwargs) + + +def table( + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + 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 + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-table", content=content, **kwargs) + + +def text( + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + 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 + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-text", content=content, **kwargs) diff --git a/emailer_lib/mjml/tests/.gitfolder b/emailer_lib/mjml/tests/.gitfolder new file mode 100644 index 0000000..e69de29 diff --git a/scripts/generate-tags.py b/scripts/generate-tags.py new file mode 100644 index 0000000..19565d9 --- /dev/null +++ b/scripts/generate-tags.py @@ -0,0 +1,147 @@ +#!/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", + "mj-include", + # Head components + "mj-attributes", + "mj-breakpoint", + "mj-font", + "mj-html-attributes", + "mj-preview", + "mj-style", + "mj-title", + # 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", +] + + +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 = tag_name.replace("-", "_").replace("mj_", "") + + function_code = f''' +def {py_name}( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `<{tag_name}>` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing <{tag_name}> + """ + return MJMLTag("{tag_name}", *args, content=content, **kwargs) +''' + functions.append(function_code) + + # Generate leaf tags (accept content but not MJML children) + for tag_name in LEAF_TAGS: + py_name = tag_name.replace("-", "_").replace("mj_", "") + + function_code = f''' +def {py_name}( + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + 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 + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing <{tag_name}> + """ + return MJMLTag("{tag_name}", content=content, **kwargs) +''' + 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 / "emailer_lib" / "mjml" / "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() From e987381c446f937ada58bb669a31a7fbc2a47ca7 Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:34:46 -0400 Subject: [PATCH 04/29] docs --- docs/reference/mjml.accordion.qmd | 27 +++ docs/reference/mjml.accordion_element.qmd | 27 +++ docs/reference/mjml.accordion_text.qmd | 25 +++ docs/reference/mjml.accordion_title.qmd | 25 +++ docs/reference/mjml.attributes.qmd | 27 +++ docs/reference/mjml.body.qmd | 27 +++ docs/reference/mjml.breakpoint.qmd | 27 +++ docs/reference/mjml.button.qmd | 25 +++ docs/reference/mjml.carousel.qmd | 27 +++ docs/reference/mjml.carousel_image.qmd | 25 +++ docs/reference/mjml.column.qmd | 27 +++ docs/reference/mjml.divider.qmd | 27 +++ docs/reference/mjml.font.qmd | 27 +++ docs/reference/mjml.group.qmd | 27 +++ docs/reference/mjml.head.qmd | 27 +++ docs/reference/mjml.hero.qmd | 27 +++ docs/reference/mjml.html_attributes.qmd | 27 +++ docs/reference/mjml.image.qmd | 27 +++ docs/reference/mjml.include.qmd | 27 +++ docs/reference/mjml.mjml.qmd | 27 +++ docs/reference/mjml.navbar.qmd | 27 +++ docs/reference/mjml.navbar_link.qmd | 25 +++ docs/reference/mjml.preview.qmd | 27 +++ docs/reference/mjml.raw.qmd | 25 +++ docs/reference/mjml.section.qmd | 27 +++ docs/reference/mjml.social.qmd | 27 +++ docs/reference/mjml.social_element.qmd | 25 +++ docs/reference/mjml.spacer.qmd | 27 +++ docs/reference/mjml.style.qmd | 27 +++ docs/reference/mjml.table.qmd | 25 +++ docs/reference/mjml.text.qmd | 25 +++ docs/reference/mjml.title.qmd | 27 +++ docs/reference/mjml.wrapper.qmd | 27 +++ docs/summary.quarto_ipynb | 251 ++++++++++++++++++++++ emailer_lib/mjml/README.md | 2 +- 35 files changed, 1125 insertions(+), 1 deletion(-) create mode 100644 docs/reference/mjml.accordion.qmd create mode 100644 docs/reference/mjml.accordion_element.qmd create mode 100644 docs/reference/mjml.accordion_text.qmd create mode 100644 docs/reference/mjml.accordion_title.qmd create mode 100644 docs/reference/mjml.attributes.qmd create mode 100644 docs/reference/mjml.body.qmd create mode 100644 docs/reference/mjml.breakpoint.qmd create mode 100644 docs/reference/mjml.button.qmd create mode 100644 docs/reference/mjml.carousel.qmd create mode 100644 docs/reference/mjml.carousel_image.qmd create mode 100644 docs/reference/mjml.column.qmd create mode 100644 docs/reference/mjml.divider.qmd create mode 100644 docs/reference/mjml.font.qmd create mode 100644 docs/reference/mjml.group.qmd create mode 100644 docs/reference/mjml.head.qmd create mode 100644 docs/reference/mjml.hero.qmd create mode 100644 docs/reference/mjml.html_attributes.qmd create mode 100644 docs/reference/mjml.image.qmd create mode 100644 docs/reference/mjml.include.qmd create mode 100644 docs/reference/mjml.mjml.qmd create mode 100644 docs/reference/mjml.navbar.qmd create mode 100644 docs/reference/mjml.navbar_link.qmd create mode 100644 docs/reference/mjml.preview.qmd create mode 100644 docs/reference/mjml.raw.qmd create mode 100644 docs/reference/mjml.section.qmd create mode 100644 docs/reference/mjml.social.qmd create mode 100644 docs/reference/mjml.social_element.qmd create mode 100644 docs/reference/mjml.spacer.qmd create mode 100644 docs/reference/mjml.style.qmd create mode 100644 docs/reference/mjml.table.qmd create mode 100644 docs/reference/mjml.text.qmd create mode 100644 docs/reference/mjml.title.qmd create mode 100644 docs/reference/mjml.wrapper.qmd create mode 100644 docs/summary.quarto_ipynb diff --git a/docs/reference/mjml.accordion.qmd b/docs/reference/mjml.accordion.qmd new file mode 100644 index 0000000..1f7bb5d --- /dev/null +++ b/docs/reference/mjml.accordion.qmd @@ -0,0 +1,27 @@ +# mjml.accordion { #emailer_lib.mjml.accordion } + +```python +mjml.accordion(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.accordion_element.qmd b/docs/reference/mjml.accordion_element.qmd new file mode 100644 index 0000000..ec8de8f --- /dev/null +++ b/docs/reference/mjml.accordion_element.qmd @@ -0,0 +1,27 @@ +# mjml.accordion_element { #emailer_lib.mjml.accordion_element } + +```python +mjml.accordion_element(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.accordion_text.qmd b/docs/reference/mjml.accordion_text.qmd new file mode 100644 index 0000000..6d760c2 --- /dev/null +++ b/docs/reference/mjml.accordion_text.qmd @@ -0,0 +1,25 @@ +# mjml.accordion_text { #emailer_lib.mjml.accordion_text } + +```python +mjml.accordion_text(content=None, **kwargs) +``` + +Create an MJML `` tag. + +This is an ending tag that accepts text/HTML content but not MJML children. + +## Parameters {.doc-section .doc-section-parameters} + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Text or HTML content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.accordion_title.qmd b/docs/reference/mjml.accordion_title.qmd new file mode 100644 index 0000000..e3dd138 --- /dev/null +++ b/docs/reference/mjml.accordion_title.qmd @@ -0,0 +1,25 @@ +# mjml.accordion_title { #emailer_lib.mjml.accordion_title } + +```python +mjml.accordion_title(content=None, **kwargs) +``` + +Create an MJML `` tag. + +This is an ending tag that accepts text/HTML content but not MJML children. + +## Parameters {.doc-section .doc-section-parameters} + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Text or HTML content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.attributes.qmd b/docs/reference/mjml.attributes.qmd new file mode 100644 index 0000000..a769245 --- /dev/null +++ b/docs/reference/mjml.attributes.qmd @@ -0,0 +1,27 @@ +# mjml.attributes { #emailer_lib.mjml.attributes } + +```python +mjml.attributes(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.body.qmd b/docs/reference/mjml.body.qmd new file mode 100644 index 0000000..ae833a8 --- /dev/null +++ b/docs/reference/mjml.body.qmd @@ -0,0 +1,27 @@ +# mjml.body { #emailer_lib.mjml.body } + +```python +mjml.body(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.breakpoint.qmd b/docs/reference/mjml.breakpoint.qmd new file mode 100644 index 0000000..097aed1 --- /dev/null +++ b/docs/reference/mjml.breakpoint.qmd @@ -0,0 +1,27 @@ +# mjml.breakpoint { #emailer_lib.mjml.breakpoint } + +```python +mjml.breakpoint(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.button.qmd b/docs/reference/mjml.button.qmd new file mode 100644 index 0000000..943ebc0 --- /dev/null +++ b/docs/reference/mjml.button.qmd @@ -0,0 +1,25 @@ +# mjml.button { #emailer_lib.mjml.button } + +```python +mjml.button(content=None, **kwargs) +``` + +Create an MJML `` tag. + +This is an ending tag that accepts text/HTML content but not MJML children. + +## Parameters {.doc-section .doc-section-parameters} + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Text or HTML content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.carousel.qmd b/docs/reference/mjml.carousel.qmd new file mode 100644 index 0000000..c2fdc71 --- /dev/null +++ b/docs/reference/mjml.carousel.qmd @@ -0,0 +1,27 @@ +# mjml.carousel { #emailer_lib.mjml.carousel } + +```python +mjml.carousel(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.carousel_image.qmd b/docs/reference/mjml.carousel_image.qmd new file mode 100644 index 0000000..f1cf996 --- /dev/null +++ b/docs/reference/mjml.carousel_image.qmd @@ -0,0 +1,25 @@ +# mjml.carousel_image { #emailer_lib.mjml.carousel_image } + +```python +mjml.carousel_image(content=None, **kwargs) +``` + +Create an MJML `` tag. + +This is an ending tag that accepts text/HTML content but not MJML children. + +## Parameters {.doc-section .doc-section-parameters} + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Text or HTML content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.column.qmd b/docs/reference/mjml.column.qmd new file mode 100644 index 0000000..3609607 --- /dev/null +++ b/docs/reference/mjml.column.qmd @@ -0,0 +1,27 @@ +# mjml.column { #emailer_lib.mjml.column } + +```python +mjml.column(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.divider.qmd b/docs/reference/mjml.divider.qmd new file mode 100644 index 0000000..8282212 --- /dev/null +++ b/docs/reference/mjml.divider.qmd @@ -0,0 +1,27 @@ +# mjml.divider { #emailer_lib.mjml.divider } + +```python +mjml.divider(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.font.qmd b/docs/reference/mjml.font.qmd new file mode 100644 index 0000000..c091a61 --- /dev/null +++ b/docs/reference/mjml.font.qmd @@ -0,0 +1,27 @@ +# mjml.font { #emailer_lib.mjml.font } + +```python +mjml.font(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.group.qmd b/docs/reference/mjml.group.qmd new file mode 100644 index 0000000..78b5a42 --- /dev/null +++ b/docs/reference/mjml.group.qmd @@ -0,0 +1,27 @@ +# mjml.group { #emailer_lib.mjml.group } + +```python +mjml.group(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.head.qmd b/docs/reference/mjml.head.qmd new file mode 100644 index 0000000..9f67652 --- /dev/null +++ b/docs/reference/mjml.head.qmd @@ -0,0 +1,27 @@ +# mjml.head { #emailer_lib.mjml.head } + +```python +mjml.head(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.hero.qmd b/docs/reference/mjml.hero.qmd new file mode 100644 index 0000000..c130649 --- /dev/null +++ b/docs/reference/mjml.hero.qmd @@ -0,0 +1,27 @@ +# mjml.hero { #emailer_lib.mjml.hero } + +```python +mjml.hero(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.html_attributes.qmd b/docs/reference/mjml.html_attributes.qmd new file mode 100644 index 0000000..0ea7807 --- /dev/null +++ b/docs/reference/mjml.html_attributes.qmd @@ -0,0 +1,27 @@ +# mjml.html_attributes { #emailer_lib.mjml.html_attributes } + +```python +mjml.html_attributes(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.image.qmd b/docs/reference/mjml.image.qmd new file mode 100644 index 0000000..cd23326 --- /dev/null +++ b/docs/reference/mjml.image.qmd @@ -0,0 +1,27 @@ +# mjml.image { #emailer_lib.mjml.image } + +```python +mjml.image(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.include.qmd b/docs/reference/mjml.include.qmd new file mode 100644 index 0000000..ded3ee6 --- /dev/null +++ b/docs/reference/mjml.include.qmd @@ -0,0 +1,27 @@ +# mjml.include { #emailer_lib.mjml.include } + +```python +mjml.include(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.mjml.qmd b/docs/reference/mjml.mjml.qmd new file mode 100644 index 0000000..bcb5a22 --- /dev/null +++ b/docs/reference/mjml.mjml.qmd @@ -0,0 +1,27 @@ +# mjml.mjml { #emailer_lib.mjml.mjml } + +```python +mjml.mjml(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.navbar.qmd b/docs/reference/mjml.navbar.qmd new file mode 100644 index 0000000..24d0771 --- /dev/null +++ b/docs/reference/mjml.navbar.qmd @@ -0,0 +1,27 @@ +# mjml.navbar { #emailer_lib.mjml.navbar } + +```python +mjml.navbar(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.navbar_link.qmd b/docs/reference/mjml.navbar_link.qmd new file mode 100644 index 0000000..f1cc908 --- /dev/null +++ b/docs/reference/mjml.navbar_link.qmd @@ -0,0 +1,25 @@ +# mjml.navbar_link { #emailer_lib.mjml.navbar_link } + +```python +mjml.navbar_link(content=None, **kwargs) +``` + +Create an MJML `` tag. + +This is an ending tag that accepts text/HTML content but not MJML children. + +## Parameters {.doc-section .doc-section-parameters} + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Text or HTML content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.preview.qmd b/docs/reference/mjml.preview.qmd new file mode 100644 index 0000000..b9b6234 --- /dev/null +++ b/docs/reference/mjml.preview.qmd @@ -0,0 +1,27 @@ +# mjml.preview { #emailer_lib.mjml.preview } + +```python +mjml.preview(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.raw.qmd b/docs/reference/mjml.raw.qmd new file mode 100644 index 0000000..01f75e0 --- /dev/null +++ b/docs/reference/mjml.raw.qmd @@ -0,0 +1,25 @@ +# mjml.raw { #emailer_lib.mjml.raw } + +```python +mjml.raw(content=None, **kwargs) +``` + +Create an MJML `` tag. + +This is an ending tag that accepts text/HTML content but not MJML children. + +## Parameters {.doc-section .doc-section-parameters} + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Text or HTML content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.section.qmd b/docs/reference/mjml.section.qmd new file mode 100644 index 0000000..830e26c --- /dev/null +++ b/docs/reference/mjml.section.qmd @@ -0,0 +1,27 @@ +# mjml.section { #emailer_lib.mjml.section } + +```python +mjml.section(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.social.qmd b/docs/reference/mjml.social.qmd new file mode 100644 index 0000000..e7ae4c0 --- /dev/null +++ b/docs/reference/mjml.social.qmd @@ -0,0 +1,27 @@ +# mjml.social { #emailer_lib.mjml.social } + +```python +mjml.social(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.social_element.qmd b/docs/reference/mjml.social_element.qmd new file mode 100644 index 0000000..f4ffb32 --- /dev/null +++ b/docs/reference/mjml.social_element.qmd @@ -0,0 +1,25 @@ +# mjml.social_element { #emailer_lib.mjml.social_element } + +```python +mjml.social_element(content=None, **kwargs) +``` + +Create an MJML `` tag. + +This is an ending tag that accepts text/HTML content but not MJML children. + +## Parameters {.doc-section .doc-section-parameters} + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Text or HTML content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.spacer.qmd b/docs/reference/mjml.spacer.qmd new file mode 100644 index 0000000..c5f77c6 --- /dev/null +++ b/docs/reference/mjml.spacer.qmd @@ -0,0 +1,27 @@ +# mjml.spacer { #emailer_lib.mjml.spacer } + +```python +mjml.spacer(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.style.qmd b/docs/reference/mjml.style.qmd new file mode 100644 index 0000000..2f2b4a5 --- /dev/null +++ b/docs/reference/mjml.style.qmd @@ -0,0 +1,27 @@ +# mjml.style { #emailer_lib.mjml.style } + +```python +mjml.style(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.table.qmd b/docs/reference/mjml.table.qmd new file mode 100644 index 0000000..38e485e --- /dev/null +++ b/docs/reference/mjml.table.qmd @@ -0,0 +1,25 @@ +# mjml.table { #emailer_lib.mjml.table } + +```python +mjml.table(content=None, **kwargs) +``` + +Create an MJML `` tag. + +This is an ending tag that accepts text/HTML content but not MJML children. + +## Parameters {.doc-section .doc-section-parameters} + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Text or HTML content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.text.qmd b/docs/reference/mjml.text.qmd new file mode 100644 index 0000000..43e9a17 --- /dev/null +++ b/docs/reference/mjml.text.qmd @@ -0,0 +1,25 @@ +# mjml.text { #emailer_lib.mjml.text } + +```python +mjml.text(content=None, **kwargs) +``` + +Create an MJML `` tag. + +This is an ending tag that accepts text/HTML content but not MJML children. + +## Parameters {.doc-section .doc-section-parameters} + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Text or HTML content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.title.qmd b/docs/reference/mjml.title.qmd new file mode 100644 index 0000000..cd774b5 --- /dev/null +++ b/docs/reference/mjml.title.qmd @@ -0,0 +1,27 @@ +# mjml.title { #emailer_lib.mjml.title } + +```python +mjml.title(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.wrapper.qmd b/docs/reference/mjml.wrapper.qmd new file mode 100644 index 0000000..f5bb671 --- /dev/null +++ b/docs/reference/mjml.wrapper.qmd @@ -0,0 +1,27 @@ +# mjml.wrapper { #emailer_lib.mjml.wrapper } + +```python +mjml.wrapper(*args, content=None, **kwargs) +``` + +Create an MJML `` tag. + +## Parameters {.doc-section .doc-section-parameters} + +[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} + +: Children or attribute dicts + +[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} + +: Optional text content for the tag + +[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} + +: Tag attributes + +## Returns {.doc-section .doc-section-returns} + +[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} + +: MJMLTag object representing \ No newline at end of file diff --git a/docs/summary.quarto_ipynb b/docs/summary.quarto_ipynb new file mode 100644 index 0000000..6d1da82 --- /dev/null +++ b/docs/summary.quarto_ipynb @@ -0,0 +1,251 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# The whole game\n", + "\n", + "\n", + "\n", + "Emailing reports is a critical but challenging task for data science.\n", + "Mainly because you have to figure out generating the email content, configuring pieces like attachments, and orchestrating it (e.g. testing, or sending on a schedule). Moreover, content can range from simple layouts to more complex ones.\n", + "\n", + "In this tutorial, we'll walk through the whole game of sending email. We'll start with this simple example:\n" + ], + "id": "e23a4943" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# | code-fold: true\n", + "# | eval: false\n", + "import os\n", + "from dotenv import load_dotenv\n", + "from data_polars import sp500\n", + "import redmail\n", + "\n", + "load_dotenv()\n", + "gmail_address = os.environ[\"GMAIL_ADDRESS\"]\n", + "gmail_app_password = os.environ[\"GMAIL_APP_PASSWORD\"]\n", + "\n", + "\n", + "email_subject = \"Report on Cars\"\n", + "email_body = sp500.head(10).style.as_raw_html(inline_css=True)\n", + "\n", + "# This is here to emphasize the sender does not have to be the same as the receiver\n", + "email_receiver = gmail_address\n", + "\n", + "redmail.gmail.username = gmail_address\n", + "redmail.gmail.password = gmail_app_password\n", + "\n", + "redmail.gmail.send(\n", + " subject=email_subject,\n", + " receivers=[email_receiver],\n", + " html=email_body,\n", + ")" + ], + "id": "0c2962ac", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](./assets/whole-game-email-annotated.png){width=50% fig-align=\"center\"}\n", + "\n", + "* **Content**: writing the text of the email, including plots and tables.\n", + "* **Composing**: setting up the subject, sender, and receivers.\n", + "* **Orchestrating**: previewing, testing, and scheduling the email.\n", + "\n", + "We'll also quickly review writing more advanced content layouts, and authoring email reports that involve running code with Quarto.\n", + "\n", + "## A simple email\n", + "\n", + "![](./assets/whole-game-email.png){width=50% fig-align=\"center\"}\n", + "\n", + "* Generate and preview\n", + "* Authenticate (may need to refer to its own authentication page in guide)\n", + "* Send\n" + ], + "id": "42715cd2" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# | code-fold: true\n", + "# | eval: false\n", + "import os\n", + "from dotenv import load_dotenv\n", + "from data_polars import sp500\n", + "import redmail\n", + "\n", + "load_dotenv()\n", + "gmail_address = os.environ[\"GMAIL_ADDRESS\"]\n", + "gmail_app_password = os.environ[\"GMAIL_APP_PASSWORD\"]\n", + "\n", + "\n", + "email_subject = \"Report on Cars\"\n", + "email_body = sp500.head(10).style.as_raw_html(inline_css=True)\n", + "\n", + "# This is here to emphasize the sender does not have to be the same as the receiver\n", + "email_receiver = gmail_address\n", + "\n", + "redmail.gmail.username = gmail_address\n", + "redmail.gmail.password = gmail_app_password\n", + "\n", + "redmail.gmail.send(\n", + " subject=email_subject,\n", + " receivers=[email_receiver],\n", + " html=email_body,\n", + ")" + ], + "id": "b18f6823", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configure: subject, recipients, attachments\n", + "\n", + "* you could attach the data as a CSV attachment\n", + "\n", + "\n", + "## Orchestrate: save and preview\n", + "\n", + "* previewing email\n", + "* intermediate json, easy for sending email later\n", + "* embedding images makes previewing hard\n", + "* can always email to yourself (or use a test service like Litmus)\n", + "\n", + "## Content: Quarto authoring\n", + "\n", + "Here's our same simple email generated using quarto.\n", + "\n", + "\n", + "\n", + "![](./assets/whole-game-quarto.png){width=50% fig-align=\"center\"}\n", + "\n", + "* Focused on basic configuring, and content\n", + "* Sending happens via our tool\n", + "* Generate using quarto render\n", + "* Can preview email\n", + "\n", + "## Content: advanced layouts\n", + "\n", + "We'll highlight the key pieces (discussed later in this guide) to go from that simple email, to a more advanced on like below:\n", + "\n", + "![](./assets/whole-game-fancy.png){width=50% fig-align=\"center\"}\n", + "\n", + "\n", + "## Fridge\n", + "\n", + "In this tutorial, we are going to send an email from a Gmail account. To do so, you will need to [create an App Password](https://support.google.com/accounts/answer/185833). Note this is only possible if you've [enabled 2-step verification](https://support.google.com/accounts/answer/185839).\n", + "\n", + "\n", + "\n", + "::: {.callout-tip}\n", + "This is just one of many options: it is also possible to send emails in Python from other email providers (Outlook, ProtonMail, etc.), or even from a custom domain. To skip ahead to a discussion of alternative sending methods, see [Authentication](orchestrating-auth.qmd)\n", + ":::\n", + "\n", + "Once you've created your App Password, that is used as your Gmail password for sending with Python. \n", + "\n", + "There are many ways to store the password seperate from your email-sending code, so as to not expose any sensitive information. One such approach uses a `.env` file, and the ``dotenv` and `os` packages.\n", + "\n", + "```{.sh filename=\".env\"}\n", + "GMAIL_APP_PASSWORD=abcd abcd abcd abcd\n", + "```\n", + "\n", + "```{.python filename=\"main.py\"}\n", + "import os\n", + "from dotenv import load_dotenv\n", + "\n", + "load_dotenv()\n", + "\n", + "your_gmail_address = \"YourGmail@gmail.com\"\n", + "gmail_app_password = os.environ[\"GMAIL_APP_PASSWORD\"]\n", + "```\n", + "\n", + "Check out the email content we will send.\n" + ], + "id": "e40499c1" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from data_polars import sp500\n", + "\n", + "sp500.head(10).style" + ], + "id": "60ababf8", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And now we send the email!\n" + ], + "id": "14aeb803" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# | eval: false\n", + "import redmail\n", + "\n", + "redmail.gmail.username = your_gmail_address\n", + "redmail.gmail.password = gmail_app_password\n", + "\n", + "redmail.gmail.send(\n", + " subject=\"An Example Email\",\n", + " receivers=[username],\n", + " html=email_html,\n", + " text=email_plaintext,\n", + ")" + ], + "id": "402bc819", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ], + "id": "658bb6d8" + } + ], + "metadata": { + "kernelspec": { + "name": "python3", + "language": "python", + "display_name": "Python 3 (ipykernel)", + "path": "/Users/jules-wg/Library/Python/3.9/share/jupyter/kernels/python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/emailer_lib/mjml/README.md b/emailer_lib/mjml/README.md index 8d1f4dc..16efad3 100644 --- a/emailer_lib/mjml/README.md +++ b/emailer_lib/mjml/README.md @@ -17,7 +17,7 @@ This module provides Python functions for creating MJML markup, the responsive e This module is part of the `emailer_lib` package: ```python -from emailer_lib import mjml +from emailer_lib import mjml as mj ``` ## Quick Start From 9b99e44aa2c86e13f11c2b92675357e66d7ad36f Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:41:03 -0400 Subject: [PATCH 05/29] ci new path to emailer-lib --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf9eb43..55c5397 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ 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 @@ -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 From 12babfe93189b338dc5a2ea420d9a9408d7fab25 Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:43:45 -0400 Subject: [PATCH 06/29] ci new path to docs --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55c5397..154caae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: - 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 From 7fbc8d9211fd596ad8131f66b7fb94f3072d6bc0 Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:47:12 -0400 Subject: [PATCH 07/29] remove tinytex --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 154caae..ec19631 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,8 +35,8 @@ jobs: # quarto docs build ---- - uses: quarto-dev/quarto-actions/setup@v2 - with: - tinytex: true + # with: + # tinytex: true - name: Build docs run: | From 09e0ed947d30fd3c8f7da58853e7e1a132bfd5f3 Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:49:48 -0400 Subject: [PATCH 08/29] ci new path to _site --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec19631..38026f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 From 6db9e5d4d848e68d8fd841aa6d6ef725776b486d Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:50:46 -0400 Subject: [PATCH 09/29] ci new path to _site --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38026f9..6115fc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: if: ${{ github.ref == 'refs/heads/main' }} with: force: false - folder: _site + folder: docs/_site clean-exclude: pr-preview - name: Deploy (preview) From f53be06edc248489c7ea056daed807a57abaa3e1 Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:09:52 -0400 Subject: [PATCH 10/29] move scripts --- Makefile | 2 +- {scripts => emailer_lib/mjml/scripts}/generate-tags.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename {scripts => emailer_lib/mjml/scripts}/generate-tags.py (97%) diff --git a/Makefile b/Makefile index 4e641ee..a5b1460 100644 --- a/Makefile +++ b/Makefile @@ -11,4 +11,4 @@ test-update: pytest --snapshot-update generate-mjml-tags: - python3 scripts/generate-tags.py + python3 emailer_lib/mjml/scripts/generate-tags.py diff --git a/scripts/generate-tags.py b/emailer_lib/mjml/scripts/generate-tags.py similarity index 97% rename from scripts/generate-tags.py rename to emailer_lib/mjml/scripts/generate-tags.py index 19565d9..57228f5 100644 --- a/scripts/generate-tags.py +++ b/emailer_lib/mjml/scripts/generate-tags.py @@ -135,7 +135,7 @@ def {py_name}( # Write to file - navigate from scripts/ to emailer_lib/mjml/tags.py script_dir = Path(__file__).parent - tags_file = script_dir.parent / "emailer_lib" / "mjml" / "tags.py" + tags_file = script_dir.parent / "tags.py" with open(tags_file, "w") as f: f.write(output) From ad78ffe0cb41349a45287a20f661e97c0e5699cb Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:09:59 -0400 Subject: [PATCH 11/29] update gitignores --- .gitignore | 2 +- emailer_lib/mjml/.gitignore | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 emailer_lib/mjml/.gitignore 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/emailer_lib/mjml/.gitignore b/emailer_lib/mjml/.gitignore new file mode 100644 index 0000000..20960b9 --- /dev/null +++ b/emailer_lib/mjml/.gitignore @@ -0,0 +1,3 @@ +playground.qmd +output.html +sample_mjml.mjml From fe140f921e764019248b182b42ab16bac60f88da Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Fri, 24 Oct 2025 15:31:27 -0400 Subject: [PATCH 12/29] update to_html --- emailer_lib/mjml/_core.py | 53 +++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/emailer_lib/mjml/_core.py b/emailer_lib/mjml/_core.py index 398bff6..6d3051a 100644 --- a/emailer_lib/mjml/_core.py +++ b/emailer_lib/mjml/_core.py @@ -4,6 +4,7 @@ # https://documentation.mjml.io/#ending-tags from typing import Any, Dict, Iterable, Mapping, Optional, Sequence, TypeVar, Union +import warnings from mjml import mjml2html @@ -79,7 +80,7 @@ def __init__( if self.content is not None: self.children = [] - def render(self, indent: int = 0, eol: str = "\n") -> str: + 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. @@ -109,7 +110,7 @@ def _flatten(children): child_strs = [] for child in _flatten(self.children): if isinstance(child, MJMLTag): - child_strs.append(child.render(indent + 2, eol)) + child_strs.append(child.render_mjml(indent + 2, eol)) else: child_strs.append(str(child)) if child_strs: @@ -123,17 +124,53 @@ def _flatten(children): return f"{pad}<{self.tagName}{attr_str}>" def _repr_html_(self): - return self.render() + return self.render_mjml() def __repr__(self) -> str: return f"MJMLTag({self.tagName!r}, attrs={dict(self.attrs)!r}, children={self.children!r}, content={self.content!r})" def to_html(self, **mjml2html_kwargs): """ - Render MJMLTag to HTML using mjml2html, only if this is the top-level tag. + 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": - raise TypeError("to_html() is only available on the top-level tag.") - - mjml_markup = self.render() + 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) From b90a8849f0a82f04b3f37821be8e06d68f7b0452 Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Fri, 24 Oct 2025 15:31:53 -0400 Subject: [PATCH 13/29] ignore html output files --- emailer_lib/mjml/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/emailer_lib/mjml/.gitignore b/emailer_lib/mjml/.gitignore index 20960b9..9ce3ea5 100644 --- a/emailer_lib/mjml/.gitignore +++ b/emailer_lib/mjml/.gitignore @@ -1,3 +1,3 @@ playground.qmd -output.html +*.html sample_mjml.mjml From 71de7e0155913b1dc67c8cb7fc43c884bb6bc923 Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:21:14 -0400 Subject: [PATCH 14/29] re-generate leaf tags --- Makefile | 2 +- .../{generate-tags.py => generate_tags.py} | 2 +- emailer_lib/mjml/tags.py | 18 +++++++++--------- emailer_lib/mjml/tests/.gitfolder | 0 4 files changed, 11 insertions(+), 11 deletions(-) rename emailer_lib/mjml/scripts/{generate-tags.py => generate_tags.py} (97%) delete mode 100644 emailer_lib/mjml/tests/.gitfolder diff --git a/Makefile b/Makefile index a5b1460..55294f3 100644 --- a/Makefile +++ b/Makefile @@ -11,4 +11,4 @@ test-update: pytest --snapshot-update generate-mjml-tags: - python3 emailer_lib/mjml/scripts/generate-tags.py + python3 emailer_lib/mjml/scripts/generate_tags.py diff --git a/emailer_lib/mjml/scripts/generate-tags.py b/emailer_lib/mjml/scripts/generate_tags.py similarity index 97% rename from emailer_lib/mjml/scripts/generate-tags.py rename to emailer_lib/mjml/scripts/generate_tags.py index 57228f5..495b991 100644 --- a/emailer_lib/mjml/scripts/generate-tags.py +++ b/emailer_lib/mjml/scripts/generate_tags.py @@ -126,7 +126,7 @@ def {py_name}( MJMLTag MJMLTag object representing <{tag_name}> """ - return MJMLTag("{tag_name}", content=content, **kwargs) + return MJMLTag("{tag_name}", content=content, _is_leaf=True, **kwargs) ''' functions.append(function_code) diff --git a/emailer_lib/mjml/tags.py b/emailer_lib/mjml/tags.py index 372cfd7..10bcf43 100644 --- a/emailer_lib/mjml/tags.py +++ b/emailer_lib/mjml/tags.py @@ -630,7 +630,7 @@ def accordion_text( MJMLTag MJMLTag object representing """ - return MJMLTag("mj-accordion-text", content=content, **kwargs) + return MJMLTag("mj-accordion-text", content=content, _is_leaf=True, **kwargs) def accordion_title( @@ -654,7 +654,7 @@ def accordion_title( MJMLTag MJMLTag object representing """ - return MJMLTag("mj-accordion-title", content=content, **kwargs) + return MJMLTag("mj-accordion-title", content=content, _is_leaf=True, **kwargs) def button( @@ -678,7 +678,7 @@ def button( MJMLTag MJMLTag object representing """ - return MJMLTag("mj-button", content=content, **kwargs) + return MJMLTag("mj-button", content=content, _is_leaf=True, **kwargs) def carousel_image( @@ -702,7 +702,7 @@ def carousel_image( MJMLTag MJMLTag object representing """ - return MJMLTag("mj-carousel-image", content=content, **kwargs) + return MJMLTag("mj-carousel-image", content=content, _is_leaf=True, **kwargs) def navbar_link( @@ -726,7 +726,7 @@ def navbar_link( MJMLTag MJMLTag object representing """ - return MJMLTag("mj-navbar-link", content=content, **kwargs) + return MJMLTag("mj-navbar-link", content=content, _is_leaf=True, **kwargs) def raw( @@ -750,7 +750,7 @@ def raw( MJMLTag MJMLTag object representing """ - return MJMLTag("mj-raw", content=content, **kwargs) + return MJMLTag("mj-raw", content=content, _is_leaf=True, **kwargs) def social_element( @@ -774,7 +774,7 @@ def social_element( MJMLTag MJMLTag object representing """ - return MJMLTag("mj-social-element", content=content, **kwargs) + return MJMLTag("mj-social-element", content=content, _is_leaf=True, **kwargs) def table( @@ -798,7 +798,7 @@ def table( MJMLTag MJMLTag object representing """ - return MJMLTag("mj-table", content=content, **kwargs) + return MJMLTag("mj-table", content=content, _is_leaf=True, **kwargs) def text( @@ -822,4 +822,4 @@ def text( MJMLTag MJMLTag object representing """ - return MJMLTag("mj-text", content=content, **kwargs) + return MJMLTag("mj-text", content=content, _is_leaf=True, **kwargs) diff --git a/emailer_lib/mjml/tests/.gitfolder b/emailer_lib/mjml/tests/.gitfolder deleted file mode 100644 index e69de29..0000000 From 1c4c86bfe508596ac579c4641fca24708933a689 Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:23:20 -0400 Subject: [PATCH 15/29] update test paths --- Makefile | 4 ++-- pyproject.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 55294f3..50e7498 100644 --- a/Makefile +++ b/Makefile @@ -5,10 +5,10 @@ preview: cd docs && quarto preview test: - pytest --cov-report=xml + pytest emailer_lib/tests emailer_lib/mjml/tests --cov-report=xml test-update: - pytest --snapshot-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/pyproject.toml b/pyproject.toml index b5ae5da..b62cc54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [tool.pytest.ini_options] minversion = "6.0" addopts = "-ra --cov=emailer_lib --cov-report=term-missing" -testpaths = ["emailer_lib/tests"] +testpaths = ["emailer_lib/tests", "emailer_lib/mjml/tests"] [project] name = "emailer-lib" @@ -53,4 +53,4 @@ exclude_also = [ include = ["emailer_lib/*"] omit = [ "emailer_lib/tests/*" -] \ No newline at end of file +] From c9d76863af38a886438abe81d15afeaa1c2abd3d Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:17:07 -0400 Subject: [PATCH 16/29] add tags for mj-class, mj-all, mj-html-attribute, remove mj-include --- docs/_quarto.yml | 4 +- emailer_lib/mjml/__init__.py | 8 +- emailer_lib/mjml/scripts/generate_tags.py | 22 +++++- emailer_lib/mjml/tags.py | 90 +++++++++++++++++++++-- 4 files changed, 110 insertions(+), 14 deletions(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 9f16e21..8ddd737 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -84,11 +84,13 @@ quartodoc: - mjml.mjml - mjml.head - mjml.body - - mjml.include - mjml.attributes + - mjml.mj_all + - mjml.mj_class - mjml.breakpoint - mjml.font - mjml.html_attributes + - mjml.html_attribute - mjml.preview - mjml.style - mjml.title diff --git a/emailer_lib/mjml/__init__.py b/emailer_lib/mjml/__init__.py index 7a9ab0b..62562c9 100644 --- a/emailer_lib/mjml/__init__.py +++ b/emailer_lib/mjml/__init__.py @@ -6,11 +6,13 @@ mjml, head, body, - include, attributes, + mj_all, + mj_class, breakpoint, font, html_attributes, + html_attribute, preview, style, title, @@ -44,11 +46,13 @@ "mjml", "head", "body", - "include", "attributes", + "mj_all", + "mj_class", "breakpoint", "font", "html_attributes", + "html_attribute", "preview", "style", "title", diff --git a/emailer_lib/mjml/scripts/generate_tags.py b/emailer_lib/mjml/scripts/generate_tags.py index 495b991..52d2295 100644 --- a/emailer_lib/mjml/scripts/generate_tags.py +++ b/emailer_lib/mjml/scripts/generate_tags.py @@ -13,12 +13,14 @@ "mjml", "mj-head", "mj-body", - "mj-include", # 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", "mj-title", @@ -49,8 +51,22 @@ "mj-social-element", "mj-table", "mj-text", + "mj-title" ] +# Tags that should keep the mj- prefix in the function name +KEEP_MJ_PREFIX = ["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.""" @@ -71,7 +87,7 @@ def generate_tags_file(): # Generate regular MJML tags (accept children and optional content) for tag_name in MJML_TAGS: - py_name = tag_name.replace("-", "_").replace("mj_", "") + py_name = get_python_name(tag_name) function_code = f''' def {py_name}( @@ -102,7 +118,7 @@ def {py_name}( # Generate leaf tags (accept content but not MJML children) for tag_name in LEAF_TAGS: - py_name = tag_name.replace("-", "_").replace("mj_", "") + py_name = get_python_name(tag_name) function_code = f''' def {py_name}( diff --git a/emailer_lib/mjml/tags.py b/emailer_lib/mjml/tags.py index 10bcf43..c1674f1 100644 --- a/emailer_lib/mjml/tags.py +++ b/emailer_lib/mjml/tags.py @@ -84,13 +84,13 @@ def body( return MJMLTag("mj-body", *args, content=content, **kwargs) -def include( +def attributes( *args: Union[TagChild, TagAttrs], content: Optional[str] = None, **kwargs: TagAttrValue, ): """ - Create an MJML `` tag. + Create an MJML `` tag. Parameters ---------- @@ -104,18 +104,18 @@ def include( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing """ - return MJMLTag("mj-include", *args, content=content, **kwargs) + return MJMLTag("mj-attributes", *args, content=content, **kwargs) -def attributes( +def mj_all( *args: Union[TagChild, TagAttrs], content: Optional[str] = None, **kwargs: TagAttrValue, ): """ - Create an MJML `` tag. + Create an MJML `` tag. Parameters ---------- @@ -129,9 +129,34 @@ def attributes( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing """ - return MJMLTag("mj-attributes", *args, content=content, **kwargs) + return MJMLTag("mj-all", *args, content=content, **kwargs) + + +def mj_class( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-class", *args, content=content, **kwargs) def breakpoint( @@ -209,6 +234,31 @@ def html_attributes( return MJMLTag("mj-html-attributes", *args, content=content, **kwargs) +def html_attribute( + *args: Union[TagChild, TagAttrs], + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + Create an MJML `` tag. + + Parameters + ---------- + *args + Children or attribute dicts + content + Optional text content for the tag + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-html-attribute", *args, content=content, **kwargs) + + def preview( *args: Union[TagChild, TagAttrs], content: Optional[str] = None, @@ -823,3 +873,27 @@ def text( MJMLTag object representing """ return MJMLTag("mj-text", content=content, _is_leaf=True, **kwargs) + + +def title( + content: Optional[str] = None, + **kwargs: TagAttrValue, +): + """ + 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 + **kwargs + Tag attributes + + Returns + ------- + MJMLTag + MJMLTag object representing + """ + return MJMLTag("mj-title", content=content, _is_leaf=True, **kwargs) From 2605e102fe058b215a8b7b9e12464b248bf068bb Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:06:52 -0400 Subject: [PATCH 17/29] rename attributes --- docs/_quarto.yml | 2 +- emailer_lib/mjml/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 8ddd737..cfa6598 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -84,7 +84,7 @@ quartodoc: - mjml.mjml - mjml.head - mjml.body - - mjml.attributes + - mjml.mj_attributes - mjml.mj_all - mjml.mj_class - mjml.breakpoint diff --git a/emailer_lib/mjml/__init__.py b/emailer_lib/mjml/__init__.py index 62562c9..959139c 100644 --- a/emailer_lib/mjml/__init__.py +++ b/emailer_lib/mjml/__init__.py @@ -6,7 +6,7 @@ mjml, head, body, - attributes, + mj_attributes, mj_all, mj_class, breakpoint, @@ -46,7 +46,7 @@ "mjml", "head", "body", - "attributes", + "mj_attributes", "mj_all", "mj_class", "breakpoint", From 14f341dd3723ef1700f90adae5b1e3587ec2da6d Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:09:04 -0400 Subject: [PATCH 18/29] explicitly define attrs, not as **kwargs or *args --- emailer_lib/mjml/_core.py | 71 +- emailer_lib/mjml/scripts/generate_tags.py | 47 +- emailer_lib/mjml/tags.py | 870 +++++++++++++++++----- 3 files changed, 749 insertions(+), 239 deletions(-) diff --git a/emailer_lib/mjml/_core.py b/emailer_lib/mjml/_core.py index 6d3051a..06b9540 100644 --- a/emailer_lib/mjml/_core.py +++ b/emailer_lib/mjml/_core.py @@ -3,7 +3,7 @@ ## TODO: make sure Ending tags are rendered as needed # https://documentation.mjml.io/#ending-tags -from typing import Any, Dict, Iterable, Mapping, Optional, Sequence, TypeVar, Union +from typing import Dict, Mapping, Optional, Sequence, Union import warnings from mjml import mjml2html @@ -20,29 +20,19 @@ class TagAttrDict(Dict[str, str]): """ def __init__( - self, *args: Mapping[str, TagAttrValue], **kwargs: TagAttrValue + self, *args: Mapping[str, TagAttrValue] ) -> None: super().__init__() for mapping in args: for k, v in mapping.items(): if v is not None: - self[self._to_kebab_case(k)] = str(v) - for k, v in kwargs.items(): - if v is not None: - self[self._to_kebab_case(k)] = str(v) + self[k] = str(v) - @staticmethod - def _to_kebab_case(s: str) -> str: - return s.replace('_', '-') - - def update(self, *args: Mapping[str, TagAttrValue], **kwargs: TagAttrValue) -> None: + def update(self, *args: Mapping[str, TagAttrValue]) -> None: for mapping in args: for k, v in mapping.items(): if v is not None: - self[self._to_kebab_case(k)] = str(v) - for k, v in kwargs.items(): - if v is not None: - self[self._to_kebab_case(k)] = str(v) + self[k] = str(v) class MJMLTag: @@ -58,24 +48,45 @@ def __init__( tagName: str, *args: Union[TagChild, TagAttrs], content: Optional[str] = None, - **kwargs: TagAttrValue, + _is_leaf: bool = False, ) -> None: self.tagName = tagName self.attrs = TagAttrDict() self.children = [] self.content = content - # Collect attributes and children - for arg in args: - if isinstance(arg, dict) or isinstance(arg, TagAttrDict): - self.attrs.update(arg) - elif ( - 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) - # Keyword attributes - self.attrs.update(**kwargs) + self._is_leaf = _is_leaf + + # Runtime validation for leaf tags + if self._is_leaf: + # Leaf tags should not accept positional arguments (children) + if args: + # Check if it's just an attributes dict + if len(args) == 1 and isinstance(args[0], (dict, TagAttrDict)): + self.attrs.update(args[0]) + else: + raise TypeError( + f"<{tagName}> is a leaf tag and does not accept children. " + f"Use the content parameter instead: {tagName}(content='...')" + ) + # Leaf tags content should be string-like + if content is not None and not isinstance(content, (str, int, float)): + raise TypeError( + f"<{tagName}> content must be a string, int, or float, " + f"got {type(content).__name__}" + ) + + # Collect attributes and children (for non-leaf tags) + if not self._is_leaf: + for arg in args: + if isinstance(arg, dict) or isinstance(arg, TagAttrDict): + self.attrs.update(arg) + elif ( + 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) + # If content is provided, children should be empty if self.content is not None: self.children = [] @@ -124,10 +135,10 @@ def _flatten(children): return f"{pad}<{self.tagName}{attr_str}>" def _repr_html_(self): - return self.render_mjml() + return self.to_html() def __repr__(self) -> str: - return f"MJMLTag({self.tagName!r}, attrs={dict(self.attrs)!r}, children={self.children!r}, content={self.content!r})" + return self.render_mjml() def to_html(self, **mjml2html_kwargs): """ diff --git a/emailer_lib/mjml/scripts/generate_tags.py b/emailer_lib/mjml/scripts/generate_tags.py index 52d2295..2655e46 100644 --- a/emailer_lib/mjml/scripts/generate_tags.py +++ b/emailer_lib/mjml/scripts/generate_tags.py @@ -55,7 +55,7 @@ ] # Tags that should keep the mj- prefix in the function name -KEEP_MJ_PREFIX = ["mj-all", "mj-class"] +KEEP_MJ_PREFIX = ["mj-attributes", "mj-all", "mj-class"] def get_python_name(tag_name: str) -> str: @@ -91,9 +91,9 @@ def generate_tags_file(): function_code = f''' def {py_name}( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `<{tag_name}>` tag. @@ -101,18 +101,32 @@ def {py_name}( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing <{tag_name}> + + Examples + -------- + With children: + {py_name}(child1, child2) + + With attributes: + {py_name}(attributes={{"attr": "value"}}) + + With both: + {py_name}(child1, child2, attributes={{"attr": "value"}}) """ - return MJMLTag("{tag_name}", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("{tag_name}", *args, content=content) + else: + return MJMLTag("{tag_name}", *args, attributes, content=content) ''' functions.append(function_code) @@ -122,8 +136,8 @@ def {py_name}( function_code = f''' def {py_name}( + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `<{tag_name}>` tag. @@ -132,17 +146,28 @@ def {py_name}( Parameters ---------- + attributes + Optional dict of tag attributes content Text or HTML content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing <{tag_name}> + + Examples + -------- + With content: + {py_name}(content="Hello") + + With attributes and content: + {py_name}(attributes={{"color": "red"}}, content="Hello") """ - return MJMLTag("{tag_name}", content=content, _is_leaf=True, **kwargs) + if attributes is None: + return MJMLTag("{tag_name}", content=content, _is_leaf=True) + else: + return MJMLTag("{tag_name}", attributes, content=content, _is_leaf=True) ''' functions.append(function_code) diff --git a/emailer_lib/mjml/tags.py b/emailer_lib/mjml/tags.py index c1674f1..5663c51 100644 --- a/emailer_lib/mjml/tags.py +++ b/emailer_lib/mjml/tags.py @@ -10,9 +10,9 @@ def mjml( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -20,24 +20,38 @@ def mjml( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + mjml(child1, child2) + + With attributes: + mjml(attributes={"attr": "value"}) + + With both: + mjml(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mjml", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mjml", *args, content=content) + else: + return MJMLTag("mjml", *args, attributes, content=content) def head( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -45,24 +59,38 @@ def head( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + head(child1, child2) + + With attributes: + head(attributes={"attr": "value"}) + + With both: + head(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-head", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-head", *args, content=content) + else: + return MJMLTag("mj-head", *args, attributes, content=content) def body( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -70,24 +98,38 @@ def body( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + body(child1, child2) + + With attributes: + body(attributes={"attr": "value"}) + + With both: + body(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-body", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-body", *args, content=content) + else: + return MJMLTag("mj-body", *args, attributes, content=content) -def attributes( - *args: Union[TagChild, TagAttrs], +def mj_attributes( + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -95,24 +137,38 @@ def attributes( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + mj_attributes(child1, child2) + + With attributes: + mj_attributes(attributes={"attr": "value"}) + + With both: + mj_attributes(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-attributes", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-attributes", *args, content=content) + else: + return MJMLTag("mj-attributes", *args, attributes, content=content) def mj_all( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -120,24 +176,38 @@ def mj_all( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + mj_all(child1, child2) + + With attributes: + mj_all(attributes={"attr": "value"}) + + With both: + mj_all(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-all", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-all", *args, content=content) + else: + return MJMLTag("mj-all", *args, attributes, content=content) def mj_class( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -145,24 +215,38 @@ def mj_class( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + mj_class(child1, child2) + + With attributes: + mj_class(attributes={"attr": "value"}) + + With both: + mj_class(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-class", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-class", *args, content=content) + else: + return MJMLTag("mj-class", *args, attributes, content=content) def breakpoint( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -170,24 +254,38 @@ def breakpoint( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + breakpoint(child1, child2) + + With attributes: + breakpoint(attributes={"attr": "value"}) + + With both: + breakpoint(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-breakpoint", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-breakpoint", *args, content=content) + else: + return MJMLTag("mj-breakpoint", *args, attributes, content=content) def font( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -195,24 +293,38 @@ def font( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + font(child1, child2) + + With attributes: + font(attributes={"attr": "value"}) + + With both: + font(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-font", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-font", *args, content=content) + else: + return MJMLTag("mj-font", *args, attributes, content=content) def html_attributes( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -220,24 +332,38 @@ def html_attributes( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + html_attributes(child1, child2) + + With attributes: + html_attributes(attributes={"attr": "value"}) + + With both: + html_attributes(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-html-attributes", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-html-attributes", *args, content=content) + else: + return MJMLTag("mj-html-attributes", *args, attributes, content=content) def html_attribute( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -245,24 +371,38 @@ def html_attribute( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + html_attribute(child1, child2) + + With attributes: + html_attribute(attributes={"attr": "value"}) + + With both: + html_attribute(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-html-attribute", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-html-attribute", *args, content=content) + else: + return MJMLTag("mj-html-attribute", *args, attributes, content=content) def preview( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -270,24 +410,38 @@ def preview( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + preview(child1, child2) + + With attributes: + preview(attributes={"attr": "value"}) + + With both: + preview(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-preview", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-preview", *args, content=content) + else: + return MJMLTag("mj-preview", *args, attributes, content=content) def style( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -295,24 +449,38 @@ def style( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + style(child1, child2) + + With attributes: + style(attributes={"attr": "value"}) + + With both: + style(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-style", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-style", *args, content=content) + else: + return MJMLTag("mj-style", *args, attributes, content=content) def title( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -320,24 +488,38 @@ def title( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + title(child1, child2) + + With attributes: + title(attributes={"attr": "value"}) + + With both: + title(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-title", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-title", *args, content=content) + else: + return MJMLTag("mj-title", *args, attributes, content=content) def accordion( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -345,24 +527,38 @@ def accordion( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + accordion(child1, child2) + + With attributes: + accordion(attributes={"attr": "value"}) + + With both: + accordion(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-accordion", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-accordion", *args, content=content) + else: + return MJMLTag("mj-accordion", *args, attributes, content=content) def accordion_element( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -370,24 +566,38 @@ def accordion_element( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + accordion_element(child1, child2) + + With attributes: + accordion_element(attributes={"attr": "value"}) + + With both: + accordion_element(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-accordion-element", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-accordion-element", *args, content=content) + else: + return MJMLTag("mj-accordion-element", *args, attributes, content=content) def carousel( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -395,24 +605,38 @@ def carousel( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + carousel(child1, child2) + + With attributes: + carousel(attributes={"attr": "value"}) + + With both: + carousel(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-carousel", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-carousel", *args, content=content) + else: + return MJMLTag("mj-carousel", *args, attributes, content=content) def column( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -420,24 +644,38 @@ def column( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + column(child1, child2) + + With attributes: + column(attributes={"attr": "value"}) + + With both: + column(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-column", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-column", *args, content=content) + else: + return MJMLTag("mj-column", *args, attributes, content=content) def divider( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -445,24 +683,38 @@ def divider( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + divider(child1, child2) + + With attributes: + divider(attributes={"attr": "value"}) + + With both: + divider(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-divider", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-divider", *args, content=content) + else: + return MJMLTag("mj-divider", *args, attributes, content=content) def group( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -470,24 +722,38 @@ def group( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + group(child1, child2) + + With attributes: + group(attributes={"attr": "value"}) + + With both: + group(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-group", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-group", *args, content=content) + else: + return MJMLTag("mj-group", *args, attributes, content=content) def hero( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -495,24 +761,38 @@ def hero( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + hero(child1, child2) + + With attributes: + hero(attributes={"attr": "value"}) + + With both: + hero(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-hero", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-hero", *args, content=content) + else: + return MJMLTag("mj-hero", *args, attributes, content=content) def image( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -520,24 +800,38 @@ def image( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + image(child1, child2) + + With attributes: + image(attributes={"attr": "value"}) + + With both: + image(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-image", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-image", *args, content=content) + else: + return MJMLTag("mj-image", *args, attributes, content=content) def navbar( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -545,24 +839,38 @@ def navbar( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + navbar(child1, child2) + + With attributes: + navbar(attributes={"attr": "value"}) + + With both: + navbar(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-navbar", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-navbar", *args, content=content) + else: + return MJMLTag("mj-navbar", *args, attributes, content=content) def section( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -570,24 +878,38 @@ def section( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + section(child1, child2) + + With attributes: + section(attributes={"attr": "value"}) + + With both: + section(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-section", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-section", *args, content=content) + else: + return MJMLTag("mj-section", *args, attributes, content=content) def social( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -595,24 +917,38 @@ def social( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + social(child1, child2) + + With attributes: + social(attributes={"attr": "value"}) + + With both: + social(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-social", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-social", *args, content=content) + else: + return MJMLTag("mj-social", *args, attributes, content=content) def spacer( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -620,24 +956,38 @@ def spacer( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + spacer(child1, child2) + + With attributes: + spacer(attributes={"attr": "value"}) + + With both: + spacer(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-spacer", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-spacer", *args, content=content) + else: + return MJMLTag("mj-spacer", *args, attributes, content=content) def wrapper( - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -645,23 +995,37 @@ def wrapper( Parameters ---------- *args - Children or attribute dicts + Children (MJMLTag objects) + attributes + Optional dict of tag attributes content Optional text content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With children: + wrapper(child1, child2) + + With attributes: + wrapper(attributes={"attr": "value"}) + + With both: + wrapper(child1, child2, attributes={"attr": "value"}) """ - return MJMLTag("mj-wrapper", *args, content=content, **kwargs) + if attributes is None: + return MJMLTag("mj-wrapper", *args, content=content) + else: + return MJMLTag("mj-wrapper", *args, attributes, content=content) def accordion_text( + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -670,22 +1034,33 @@ def accordion_text( Parameters ---------- + attributes + Optional dict of tag attributes content Text or HTML content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With content: + accordion_text(content="Hello") + + With attributes and content: + accordion_text(attributes={"color": "red"}, content="Hello") """ - return MJMLTag("mj-accordion-text", content=content, _is_leaf=True, **kwargs) + if attributes is None: + return MJMLTag("mj-accordion-text", content=content, _is_leaf=True) + else: + return MJMLTag("mj-accordion-text", attributes, content=content, _is_leaf=True) def accordion_title( + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -694,22 +1069,33 @@ def accordion_title( Parameters ---------- + attributes + Optional dict of tag attributes content Text or HTML content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With content: + accordion_title(content="Hello") + + With attributes and content: + accordion_title(attributes={"color": "red"}, content="Hello") """ - return MJMLTag("mj-accordion-title", content=content, _is_leaf=True, **kwargs) + if attributes is None: + return MJMLTag("mj-accordion-title", content=content, _is_leaf=True) + else: + return MJMLTag("mj-accordion-title", attributes, content=content, _is_leaf=True) def button( + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -718,22 +1104,33 @@ def button( Parameters ---------- + attributes + Optional dict of tag attributes content Text or HTML content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With content: + button(content="Hello") + + With attributes and content: + button(attributes={"color": "red"}, content="Hello") """ - return MJMLTag("mj-button", content=content, _is_leaf=True, **kwargs) + if attributes is None: + return MJMLTag("mj-button", content=content, _is_leaf=True) + else: + return MJMLTag("mj-button", attributes, content=content, _is_leaf=True) def carousel_image( + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -742,22 +1139,33 @@ def carousel_image( Parameters ---------- + attributes + Optional dict of tag attributes content Text or HTML content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With content: + carousel_image(content="Hello") + + With attributes and content: + carousel_image(attributes={"color": "red"}, content="Hello") """ - return MJMLTag("mj-carousel-image", content=content, _is_leaf=True, **kwargs) + if attributes is None: + return MJMLTag("mj-carousel-image", content=content, _is_leaf=True) + else: + return MJMLTag("mj-carousel-image", attributes, content=content, _is_leaf=True) def navbar_link( + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -766,22 +1174,33 @@ def navbar_link( Parameters ---------- + attributes + Optional dict of tag attributes content Text or HTML content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With content: + navbar_link(content="Hello") + + With attributes and content: + navbar_link(attributes={"color": "red"}, content="Hello") """ - return MJMLTag("mj-navbar-link", content=content, _is_leaf=True, **kwargs) + if attributes is None: + return MJMLTag("mj-navbar-link", content=content, _is_leaf=True) + else: + return MJMLTag("mj-navbar-link", attributes, content=content, _is_leaf=True) def raw( + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -790,22 +1209,33 @@ def raw( Parameters ---------- + attributes + Optional dict of tag attributes content Text or HTML content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With content: + raw(content="Hello") + + With attributes and content: + raw(attributes={"color": "red"}, content="Hello") """ - return MJMLTag("mj-raw", content=content, _is_leaf=True, **kwargs) + if attributes is None: + return MJMLTag("mj-raw", content=content, _is_leaf=True) + else: + return MJMLTag("mj-raw", attributes, content=content, _is_leaf=True) def social_element( + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -814,22 +1244,33 @@ def social_element( Parameters ---------- + attributes + Optional dict of tag attributes content Text or HTML content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With content: + social_element(content="Hello") + + With attributes and content: + social_element(attributes={"color": "red"}, content="Hello") """ - return MJMLTag("mj-social-element", content=content, _is_leaf=True, **kwargs) + if attributes is None: + return MJMLTag("mj-social-element", content=content, _is_leaf=True) + else: + return MJMLTag("mj-social-element", attributes, content=content, _is_leaf=True) def table( + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -838,22 +1279,33 @@ def table( Parameters ---------- + attributes + Optional dict of tag attributes content Text or HTML content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With content: + table(content="Hello") + + With attributes and content: + table(attributes={"color": "red"}, content="Hello") """ - return MJMLTag("mj-table", content=content, _is_leaf=True, **kwargs) + if attributes is None: + return MJMLTag("mj-table", content=content, _is_leaf=True) + else: + return MJMLTag("mj-table", attributes, content=content, _is_leaf=True) def text( + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -862,22 +1314,33 @@ def text( Parameters ---------- + attributes + Optional dict of tag attributes content Text or HTML content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing + + Examples + -------- + With content: + text(content="Hello") + + With attributes and content: + text(attributes={"color": "red"}, content="Hello") """ - return MJMLTag("mj-text", content=content, _is_leaf=True, **kwargs) + if attributes is None: + return MJMLTag("mj-text", content=content, _is_leaf=True) + else: + return MJMLTag("mj-text", attributes, content=content, _is_leaf=True) def title( + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, - **kwargs: TagAttrValue, ): """ Create an MJML `` tag. @@ -886,14 +1349,25 @@ def title( Parameters ---------- + attributes + Optional dict of tag attributes content Text or HTML content for the tag - **kwargs - Tag attributes Returns ------- MJMLTag MJMLTag object representing - """ - return MJMLTag("mj-title", content=content, _is_leaf=True, **kwargs) + + Examples + -------- + With content: + title(content="Hello") + + With attributes and content: + title(attributes={"color": "red"}, content="Hello") + """ + if attributes is None: + return MJMLTag("mj-title", content=content, _is_leaf=True) + else: + return MJMLTag("mj-title", attributes, content=content, _is_leaf=True) From d1a13fa44ee1cd609a8402f21ed231a17b408018 Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:10:23 -0400 Subject: [PATCH 19/29] add test_tags --- emailer_lib/mjml/tests/test_tags.py | 267 ++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 emailer_lib/mjml/tests/test_tags.py 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 == "\n
Custom HTML
\n
" + + +def test_table_tag(): + tbl = table(content="
Cell
") + assert tbl.tagName == "mj-table" + assert "" in tbl.content + + mjml_content = tbl.render_mjml() + assert "" in mjml_content + assert "
Cell
" 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 ' Date: Mon, 27 Oct 2025 15:01:39 -0400 Subject: [PATCH 20/29] pass attributes separately --- emailer_lib/mjml/_core.py | 38 ++--- emailer_lib/mjml/scripts/generate_tags.py | 10 +- emailer_lib/mjml/tags.py | 180 +++++----------------- emailer_lib/mjml/tests/test_core.py | 135 ++++++++++++++++ 4 files changed, 193 insertions(+), 170 deletions(-) create mode 100644 emailer_lib/mjml/tests/test_core.py diff --git a/emailer_lib/mjml/_core.py b/emailer_lib/mjml/_core.py index 06b9540..82104ba 100644 --- a/emailer_lib/mjml/_core.py +++ b/emailer_lib/mjml/_core.py @@ -38,15 +38,13 @@ def update(self, *args: Mapping[str, TagAttrValue]) -> None: class MJMLTag: """ MJML tag class. - Differences from htmltools Tag: - - 'name' renamed to 'tagName' for MJML - - 'content' field for leaf tags (optional) """ def __init__( self, tagName: str, - *args: Union[TagChild, TagAttrs], + *args: TagChild, + attributes: Optional[TagAttrs] = None, content: Optional[str] = None, _is_leaf: bool = False, ) -> None: @@ -56,37 +54,41 @@ def __init__( self.content = content self._is_leaf = _is_leaf + # 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." + ) + # Runtime validation for leaf tags if self._is_leaf: # Leaf tags should not accept positional arguments (children) if args: - # Check if it's just an attributes dict - if len(args) == 1 and isinstance(args[0], (dict, TagAttrDict)): - self.attrs.update(args[0]) - else: - raise TypeError( - f"<{tagName}> is a leaf tag and does not accept children. " - f"Use the content parameter instead: {tagName}(content='...')" - ) + raise TypeError( + f"<{tagName}> is a leaf tag and does not accept children. " + f"Use the content parameter instead: {tagName}(content='...')" + ) # Leaf tags content should be string-like if content is not None and not isinstance(content, (str, int, float)): raise TypeError( f"<{tagName}> content must be a string, int, or float, " f"got {type(content).__name__}" ) - - # Collect attributes and children (for non-leaf tags) - if not self._is_leaf: + else: + # Collect children (for non-leaf tags only) for arg in args: - if isinstance(arg, dict) or isinstance(arg, TagAttrDict): - self.attrs.update(arg) - elif ( + 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) + # If content is provided, children should be empty if self.content is not None: self.children = [] diff --git a/emailer_lib/mjml/scripts/generate_tags.py b/emailer_lib/mjml/scripts/generate_tags.py index 2655e46..85fe503 100644 --- a/emailer_lib/mjml/scripts/generate_tags.py +++ b/emailer_lib/mjml/scripts/generate_tags.py @@ -123,10 +123,7 @@ def {py_name}( With both: {py_name}(child1, child2, attributes={{"attr": "value"}}) """ - if attributes is None: - return MJMLTag("{tag_name}", *args, content=content) - else: - return MJMLTag("{tag_name}", *args, attributes, content=content) + return MJMLTag("{tag_name}", *args, attributes=attributes, content=content) ''' functions.append(function_code) @@ -164,10 +161,7 @@ def {py_name}( With attributes and content: {py_name}(attributes={{"color": "red"}}, content="Hello") """ - if attributes is None: - return MJMLTag("{tag_name}", content=content, _is_leaf=True) - else: - return MJMLTag("{tag_name}", attributes, content=content, _is_leaf=True) + return MJMLTag("{tag_name}", attributes=attributes, content=content, _is_leaf=True) ''' functions.append(function_code) diff --git a/emailer_lib/mjml/tags.py b/emailer_lib/mjml/tags.py index 5663c51..380937d 100644 --- a/emailer_lib/mjml/tags.py +++ b/emailer_lib/mjml/tags.py @@ -42,10 +42,7 @@ def mjml( With both: mjml(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mjml", *args, content=content) - else: - return MJMLTag("mjml", *args, attributes, content=content) + return MJMLTag("mjml", *args, attributes=attributes, content=content) def head( @@ -81,10 +78,7 @@ def head( With both: head(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-head", *args, content=content) - else: - return MJMLTag("mj-head", *args, attributes, content=content) + return MJMLTag("mj-head", *args, attributes=attributes, content=content) def body( @@ -120,10 +114,7 @@ def body( With both: body(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-body", *args, content=content) - else: - return MJMLTag("mj-body", *args, attributes, content=content) + return MJMLTag("mj-body", *args, attributes=attributes, content=content) def mj_attributes( @@ -159,10 +150,7 @@ def mj_attributes( With both: mj_attributes(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-attributes", *args, content=content) - else: - return MJMLTag("mj-attributes", *args, attributes, content=content) + return MJMLTag("mj-attributes", *args, attributes=attributes, content=content) def mj_all( @@ -198,10 +186,7 @@ def mj_all( With both: mj_all(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-all", *args, content=content) - else: - return MJMLTag("mj-all", *args, attributes, content=content) + return MJMLTag("mj-all", *args, attributes=attributes, content=content) def mj_class( @@ -237,10 +222,7 @@ def mj_class( With both: mj_class(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-class", *args, content=content) - else: - return MJMLTag("mj-class", *args, attributes, content=content) + return MJMLTag("mj-class", *args, attributes=attributes, content=content) def breakpoint( @@ -276,10 +258,7 @@ def breakpoint( With both: breakpoint(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-breakpoint", *args, content=content) - else: - return MJMLTag("mj-breakpoint", *args, attributes, content=content) + return MJMLTag("mj-breakpoint", *args, attributes=attributes, content=content) def font( @@ -315,10 +294,7 @@ def font( With both: font(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-font", *args, content=content) - else: - return MJMLTag("mj-font", *args, attributes, content=content) + return MJMLTag("mj-font", *args, attributes=attributes, content=content) def html_attributes( @@ -354,10 +330,7 @@ def html_attributes( With both: html_attributes(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-html-attributes", *args, content=content) - else: - return MJMLTag("mj-html-attributes", *args, attributes, content=content) + return MJMLTag("mj-html-attributes", *args, attributes=attributes, content=content) def html_attribute( @@ -393,10 +366,7 @@ def html_attribute( With both: html_attribute(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-html-attribute", *args, content=content) - else: - return MJMLTag("mj-html-attribute", *args, attributes, content=content) + return MJMLTag("mj-html-attribute", *args, attributes=attributes, content=content) def preview( @@ -432,10 +402,7 @@ def preview( With both: preview(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-preview", *args, content=content) - else: - return MJMLTag("mj-preview", *args, attributes, content=content) + return MJMLTag("mj-preview", *args, attributes=attributes, content=content) def style( @@ -471,10 +438,7 @@ def style( With both: style(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-style", *args, content=content) - else: - return MJMLTag("mj-style", *args, attributes, content=content) + return MJMLTag("mj-style", *args, attributes=attributes, content=content) def title( @@ -510,10 +474,7 @@ def title( With both: title(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-title", *args, content=content) - else: - return MJMLTag("mj-title", *args, attributes, content=content) + return MJMLTag("mj-title", *args, attributes=attributes, content=content) def accordion( @@ -549,10 +510,7 @@ def accordion( With both: accordion(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-accordion", *args, content=content) - else: - return MJMLTag("mj-accordion", *args, attributes, content=content) + return MJMLTag("mj-accordion", *args, attributes=attributes, content=content) def accordion_element( @@ -588,10 +546,7 @@ def accordion_element( With both: accordion_element(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-accordion-element", *args, content=content) - else: - return MJMLTag("mj-accordion-element", *args, attributes, content=content) + return MJMLTag("mj-accordion-element", *args, attributes=attributes, content=content) def carousel( @@ -627,10 +582,7 @@ def carousel( With both: carousel(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-carousel", *args, content=content) - else: - return MJMLTag("mj-carousel", *args, attributes, content=content) + return MJMLTag("mj-carousel", *args, attributes=attributes, content=content) def column( @@ -666,10 +618,7 @@ def column( With both: column(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-column", *args, content=content) - else: - return MJMLTag("mj-column", *args, attributes, content=content) + return MJMLTag("mj-column", *args, attributes=attributes, content=content) def divider( @@ -705,10 +654,7 @@ def divider( With both: divider(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-divider", *args, content=content) - else: - return MJMLTag("mj-divider", *args, attributes, content=content) + return MJMLTag("mj-divider", *args, attributes=attributes, content=content) def group( @@ -744,10 +690,7 @@ def group( With both: group(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-group", *args, content=content) - else: - return MJMLTag("mj-group", *args, attributes, content=content) + return MJMLTag("mj-group", *args, attributes=attributes, content=content) def hero( @@ -783,10 +726,7 @@ def hero( With both: hero(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-hero", *args, content=content) - else: - return MJMLTag("mj-hero", *args, attributes, content=content) + return MJMLTag("mj-hero", *args, attributes=attributes, content=content) def image( @@ -822,10 +762,7 @@ def image( With both: image(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-image", *args, content=content) - else: - return MJMLTag("mj-image", *args, attributes, content=content) + return MJMLTag("mj-image", *args, attributes=attributes, content=content) def navbar( @@ -861,10 +798,7 @@ def navbar( With both: navbar(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-navbar", *args, content=content) - else: - return MJMLTag("mj-navbar", *args, attributes, content=content) + return MJMLTag("mj-navbar", *args, attributes=attributes, content=content) def section( @@ -900,10 +834,7 @@ def section( With both: section(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-section", *args, content=content) - else: - return MJMLTag("mj-section", *args, attributes, content=content) + return MJMLTag("mj-section", *args, attributes=attributes, content=content) def social( @@ -939,10 +870,7 @@ def social( With both: social(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-social", *args, content=content) - else: - return MJMLTag("mj-social", *args, attributes, content=content) + return MJMLTag("mj-social", *args, attributes=attributes, content=content) def spacer( @@ -978,10 +906,7 @@ def spacer( With both: spacer(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-spacer", *args, content=content) - else: - return MJMLTag("mj-spacer", *args, attributes, content=content) + return MJMLTag("mj-spacer", *args, attributes=attributes, content=content) def wrapper( @@ -1017,10 +942,7 @@ def wrapper( With both: wrapper(child1, child2, attributes={"attr": "value"}) """ - if attributes is None: - return MJMLTag("mj-wrapper", *args, content=content) - else: - return MJMLTag("mj-wrapper", *args, attributes, content=content) + return MJMLTag("mj-wrapper", *args, attributes=attributes, content=content) def accordion_text( @@ -1052,10 +974,7 @@ def accordion_text( With attributes and content: accordion_text(attributes={"color": "red"}, content="Hello") """ - if attributes is None: - return MJMLTag("mj-accordion-text", content=content, _is_leaf=True) - else: - return MJMLTag("mj-accordion-text", attributes, content=content, _is_leaf=True) + return MJMLTag("mj-accordion-text", attributes=attributes, content=content, _is_leaf=True) def accordion_title( @@ -1087,10 +1006,7 @@ def accordion_title( With attributes and content: accordion_title(attributes={"color": "red"}, content="Hello") """ - if attributes is None: - return MJMLTag("mj-accordion-title", content=content, _is_leaf=True) - else: - return MJMLTag("mj-accordion-title", attributes, content=content, _is_leaf=True) + return MJMLTag("mj-accordion-title", attributes=attributes, content=content, _is_leaf=True) def button( @@ -1122,10 +1038,7 @@ def button( With attributes and content: button(attributes={"color": "red"}, content="Hello") """ - if attributes is None: - return MJMLTag("mj-button", content=content, _is_leaf=True) - else: - return MJMLTag("mj-button", attributes, content=content, _is_leaf=True) + return MJMLTag("mj-button", attributes=attributes, content=content, _is_leaf=True) def carousel_image( @@ -1157,10 +1070,7 @@ def carousel_image( With attributes and content: carousel_image(attributes={"color": "red"}, content="Hello") """ - if attributes is None: - return MJMLTag("mj-carousel-image", content=content, _is_leaf=True) - else: - return MJMLTag("mj-carousel-image", attributes, content=content, _is_leaf=True) + return MJMLTag("mj-carousel-image", attributes=attributes, content=content, _is_leaf=True) def navbar_link( @@ -1192,10 +1102,7 @@ def navbar_link( With attributes and content: navbar_link(attributes={"color": "red"}, content="Hello") """ - if attributes is None: - return MJMLTag("mj-navbar-link", content=content, _is_leaf=True) - else: - return MJMLTag("mj-navbar-link", attributes, content=content, _is_leaf=True) + return MJMLTag("mj-navbar-link", attributes=attributes, content=content, _is_leaf=True) def raw( @@ -1227,10 +1134,7 @@ def raw( With attributes and content: raw(attributes={"color": "red"}, content="Hello") """ - if attributes is None: - return MJMLTag("mj-raw", content=content, _is_leaf=True) - else: - return MJMLTag("mj-raw", attributes, content=content, _is_leaf=True) + return MJMLTag("mj-raw", attributes=attributes, content=content, _is_leaf=True) def social_element( @@ -1262,10 +1166,7 @@ def social_element( With attributes and content: social_element(attributes={"color": "red"}, content="Hello") """ - if attributes is None: - return MJMLTag("mj-social-element", content=content, _is_leaf=True) - else: - return MJMLTag("mj-social-element", attributes, content=content, _is_leaf=True) + return MJMLTag("mj-social-element", attributes=attributes, content=content, _is_leaf=True) def table( @@ -1297,10 +1198,7 @@ def table( With attributes and content: table(attributes={"color": "red"}, content="Hello") """ - if attributes is None: - return MJMLTag("mj-table", content=content, _is_leaf=True) - else: - return MJMLTag("mj-table", attributes, content=content, _is_leaf=True) + return MJMLTag("mj-table", attributes=attributes, content=content, _is_leaf=True) def text( @@ -1332,10 +1230,7 @@ def text( With attributes and content: text(attributes={"color": "red"}, content="Hello") """ - if attributes is None: - return MJMLTag("mj-text", content=content, _is_leaf=True) - else: - return MJMLTag("mj-text", attributes, content=content, _is_leaf=True) + return MJMLTag("mj-text", attributes=attributes, content=content, _is_leaf=True) def title( @@ -1367,7 +1262,4 @@ def title( With attributes and content: title(attributes={"color": "red"}, content="Hello") """ - if attributes is None: - return MJMLTag("mj-title", content=content, _is_leaf=True) - else: - return MJMLTag("mj-title", attributes, content=content, _is_leaf=True) + return MJMLTag("mj-title", attributes=attributes, content=content, _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..ad5f511 --- /dev/null +++ b/emailer_lib/mjml/tests/test_core.py @@ -0,0 +1,135 @@ +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) + print(tag) + + 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 " Date: Mon, 27 Oct 2025 15:11:58 -0400 Subject: [PATCH 21/29] remove unneccessary flattening --- emailer_lib/mjml/_core.py | 9 ++++---- emailer_lib/mjml/tests/test_core.py | 32 ++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/emailer_lib/mjml/_core.py b/emailer_lib/mjml/_core.py index 82104ba..afde4a3 100644 --- a/emailer_lib/mjml/_core.py +++ b/emailer_lib/mjml/_core.py @@ -89,9 +89,10 @@ def __init__( if attributes is not None: self.attrs.update(attributes) - # If content is provided, children should be empty - if self.content is not None: - self.children = [] + # 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: """ @@ -107,8 +108,6 @@ def _flatten(children): yield c elif isinstance(c, (str, float)): yield c - elif isinstance(c, Sequence) and not isinstance(c, str): - yield from _flatten(c) # Build attribute string attr_str = "" diff --git a/emailer_lib/mjml/tests/test_core.py b/emailer_lib/mjml/tests/test_core.py index ad5f511..95ec28d 100644 --- a/emailer_lib/mjml/tests/test_core.py +++ b/emailer_lib/mjml/tests/test_core.py @@ -29,7 +29,6 @@ def test_update_method(): def test_tag_with_dict_attributes(): attrs_dict = {"background-color": "#fff", "padding": "20px"} tag = MJMLTag("mj-section", attributes=attrs_dict) - print(tag) assert tag.attrs["background-color"] == "#fff" assert tag.attrs["padding"] == "20px" @@ -42,6 +41,7 @@ def test_tag_filters_none_children(): # 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() @@ -133,3 +133,33 @@ def test_to_html_passes_kwargs_to_mjml2html(): result = tag.to_html(social_icon_origin="https://www.example.com") assert "html" in result + + +def test_leaf_tag_raises_on_children(): + with pytest.raises(TypeError, match="is a leaf tag and does not accept children"): + MJMLTag("mj-text", MJMLTag("mj-column"), _is_leaf=True) + + +def test_leaf_tag_content_type_validation(): + with pytest.raises(TypeError, match="content must be a string, int, or float"): + MJMLTag("mj-text", content=["invalid", "list"], _is_leaf=True) + + +def test_children_sequence_flattening(): + child1 = MJMLTag("mj-text", content="Text 1") + child2 = MJMLTag("mj-text", content="Text 2") + child3 = MJMLTag("mj-text", content="Text 3") + + tag = MJMLTag("mj-column", [child1, child2], child3) + + assert len(tag.children) == 3 + assert tag.children[0] == child1 + assert tag.children[1] == child2 + assert tag.children[2] == child3 + + mjml_content = tag.render_mjml() + + assert mjml_content.count("") == 3 + assert "Text 1" in mjml_content + assert "Text 2" in mjml_content + assert "Text 3" in mjml_content From ff3ecdbc274f36cedfe418c7e0170449ff8d07f7 Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:25:00 -0400 Subject: [PATCH 22/29] Stop tracking docs/reference directory --- .../IntermediateEmail.preview_send_email.qmd | 22 ----- docs/reference/IntermediateEmail.qmd | 73 --------------- .../IntermediateEmail.write_email_message.qmd | 22 ----- .../IntermediateEmail.write_preview_email.qmd | 32 ------- docs/reference/_styles-quartodoc.css | 22 ----- ...b.IntermediateEmail.preview_send_email.qmd | 22 ----- ....IntermediateEmail.write_email_message.qmd | 22 ----- ....IntermediateEmail.write_preview_email.qmd | 32 ------- docs/reference/index.qmd | 89 ------------------ docs/reference/mjml.accordion.qmd | 27 ------ docs/reference/mjml.accordion_element.qmd | 27 ------ docs/reference/mjml.accordion_text.qmd | 25 ----- docs/reference/mjml.accordion_title.qmd | 25 ----- docs/reference/mjml.attributes.qmd | 27 ------ docs/reference/mjml.body.qmd | 27 ------ docs/reference/mjml.breakpoint.qmd | 27 ------ docs/reference/mjml.button.qmd | 25 ----- docs/reference/mjml.carousel.qmd | 27 ------ docs/reference/mjml.carousel_image.qmd | 25 ----- docs/reference/mjml.column.qmd | 27 ------ docs/reference/mjml.divider.qmd | 27 ------ docs/reference/mjml.font.qmd | 27 ------ docs/reference/mjml.group.qmd | 27 ------ docs/reference/mjml.head.qmd | 27 ------ docs/reference/mjml.hero.qmd | 27 ------ docs/reference/mjml.html_attributes.qmd | 27 ------ docs/reference/mjml.image.qmd | 27 ------ docs/reference/mjml.include.qmd | 27 ------ docs/reference/mjml.mjml.qmd | 27 ------ docs/reference/mjml.navbar.qmd | 27 ------ docs/reference/mjml.navbar_link.qmd | 25 ----- docs/reference/mjml.preview.qmd | 27 ------ docs/reference/mjml.raw.qmd | 25 ----- docs/reference/mjml.section.qmd | 27 ------ docs/reference/mjml.social.qmd | 27 ------ docs/reference/mjml.social_element.qmd | 25 ----- docs/reference/mjml.spacer.qmd | 27 ------ docs/reference/mjml.style.qmd | 27 ------ docs/reference/mjml.table.qmd | 25 ----- docs/reference/mjml.text.qmd | 25 ----- docs/reference/mjml.title.qmd | 27 ------ docs/reference/mjml.wrapper.qmd | 27 ------ docs/reference/mjml_to_intermediate_email.qmd | 19 ---- .../quarto_json_to_intermediate_email.qmd | 13 --- .../redmail_to_intermediate_email.qmd | 14 --- .../send_intermediate_email_with_gmail.qmd | 39 -------- .../send_intermediate_email_with_mailgun.qmd | 23 ----- .../send_intermediate_email_with_redmail.qmd | 23 ----- .../send_intermediate_email_with_smtp.qmd | 92 ------------------- .../send_intermediate_email_with_yagmail.qmd | 23 ----- .../send_quarto_email_with_gmail.qmd | 42 --------- docs/reference/styles.css | 4 - .../reference/write_email_message_to_file.qmd | 20 ---- .../yagmail_to_intermediate_email.qmd | 13 --- 54 files changed, 1534 deletions(-) delete mode 100644 docs/reference/IntermediateEmail.preview_send_email.qmd delete mode 100644 docs/reference/IntermediateEmail.qmd delete mode 100644 docs/reference/IntermediateEmail.write_email_message.qmd delete mode 100644 docs/reference/IntermediateEmail.write_preview_email.qmd delete mode 100644 docs/reference/_styles-quartodoc.css delete mode 100644 docs/reference/emailer_lib.IntermediateEmail.preview_send_email.qmd delete mode 100644 docs/reference/emailer_lib.IntermediateEmail.write_email_message.qmd delete mode 100644 docs/reference/emailer_lib.IntermediateEmail.write_preview_email.qmd delete mode 100644 docs/reference/index.qmd delete mode 100644 docs/reference/mjml.accordion.qmd delete mode 100644 docs/reference/mjml.accordion_element.qmd delete mode 100644 docs/reference/mjml.accordion_text.qmd delete mode 100644 docs/reference/mjml.accordion_title.qmd delete mode 100644 docs/reference/mjml.attributes.qmd delete mode 100644 docs/reference/mjml.body.qmd delete mode 100644 docs/reference/mjml.breakpoint.qmd delete mode 100644 docs/reference/mjml.button.qmd delete mode 100644 docs/reference/mjml.carousel.qmd delete mode 100644 docs/reference/mjml.carousel_image.qmd delete mode 100644 docs/reference/mjml.column.qmd delete mode 100644 docs/reference/mjml.divider.qmd delete mode 100644 docs/reference/mjml.font.qmd delete mode 100644 docs/reference/mjml.group.qmd delete mode 100644 docs/reference/mjml.head.qmd delete mode 100644 docs/reference/mjml.hero.qmd delete mode 100644 docs/reference/mjml.html_attributes.qmd delete mode 100644 docs/reference/mjml.image.qmd delete mode 100644 docs/reference/mjml.include.qmd delete mode 100644 docs/reference/mjml.mjml.qmd delete mode 100644 docs/reference/mjml.navbar.qmd delete mode 100644 docs/reference/mjml.navbar_link.qmd delete mode 100644 docs/reference/mjml.preview.qmd delete mode 100644 docs/reference/mjml.raw.qmd delete mode 100644 docs/reference/mjml.section.qmd delete mode 100644 docs/reference/mjml.social.qmd delete mode 100644 docs/reference/mjml.social_element.qmd delete mode 100644 docs/reference/mjml.spacer.qmd delete mode 100644 docs/reference/mjml.style.qmd delete mode 100644 docs/reference/mjml.table.qmd delete mode 100644 docs/reference/mjml.text.qmd delete mode 100644 docs/reference/mjml.title.qmd delete mode 100644 docs/reference/mjml.wrapper.qmd delete mode 100644 docs/reference/mjml_to_intermediate_email.qmd delete mode 100644 docs/reference/quarto_json_to_intermediate_email.qmd delete mode 100644 docs/reference/redmail_to_intermediate_email.qmd delete mode 100644 docs/reference/send_intermediate_email_with_gmail.qmd delete mode 100644 docs/reference/send_intermediate_email_with_mailgun.qmd delete mode 100644 docs/reference/send_intermediate_email_with_redmail.qmd delete mode 100644 docs/reference/send_intermediate_email_with_smtp.qmd delete mode 100644 docs/reference/send_intermediate_email_with_yagmail.qmd delete mode 100644 docs/reference/send_quarto_email_with_gmail.qmd delete mode 100644 docs/reference/styles.css delete mode 100644 docs/reference/write_email_message_to_file.qmd delete mode 100644 docs/reference/yagmail_to_intermediate_email.qmd diff --git a/docs/reference/IntermediateEmail.preview_send_email.qmd b/docs/reference/IntermediateEmail.preview_send_email.qmd deleted file mode 100644 index 6a4976d..0000000 --- a/docs/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/docs/reference/IntermediateEmail.qmd b/docs/reference/IntermediateEmail.qmd deleted file mode 100644 index 5db5e37..0000000 --- a/docs/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/docs/reference/IntermediateEmail.write_email_message.qmd b/docs/reference/IntermediateEmail.write_email_message.qmd deleted file mode 100644 index 63d4921..0000000 --- a/docs/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/docs/reference/IntermediateEmail.write_preview_email.qmd b/docs/reference/IntermediateEmail.write_preview_email.qmd deleted file mode 100644 index 21894f5..0000000 --- a/docs/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/docs/reference/_styles-quartodoc.css b/docs/reference/_styles-quartodoc.css deleted file mode 100644 index 51714ba..0000000 --- a/docs/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/docs/reference/emailer_lib.IntermediateEmail.preview_send_email.qmd b/docs/reference/emailer_lib.IntermediateEmail.preview_send_email.qmd deleted file mode 100644 index 7398b00..0000000 --- a/docs/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/docs/reference/emailer_lib.IntermediateEmail.write_email_message.qmd b/docs/reference/emailer_lib.IntermediateEmail.write_email_message.qmd deleted file mode 100644 index e23a485..0000000 --- a/docs/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/docs/reference/emailer_lib.IntermediateEmail.write_preview_email.qmd b/docs/reference/emailer_lib.IntermediateEmail.write_preview_email.qmd deleted file mode 100644 index 2946061..0000000 --- a/docs/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/docs/reference/index.qmd b/docs/reference/index.qmd deleted file mode 100644 index 5f26bdd..0000000 --- a/docs/reference/index.qmd +++ /dev/null @@ -1,89 +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). | - -## MJML Authoring - -Write responsive emails with MJML - - -| | | -| --- | --- | -| [mjml.mjml](mjml.mjml.qmd#emailer_lib.mjml.mjml) | Create an MJML `` tag. | -| [mjml.head](mjml.head.qmd#emailer_lib.mjml.head) | Create an MJML `` tag. | -| [mjml.body](mjml.body.qmd#emailer_lib.mjml.body) | Create an MJML `` tag. | -| [mjml.include](mjml.include.qmd#emailer_lib.mjml.include) | Create an MJML `` tag. | -| [mjml.attributes](mjml.attributes.qmd#emailer_lib.mjml.attributes) | Create an MJML `` tag. | -| [mjml.breakpoint](mjml.breakpoint.qmd#emailer_lib.mjml.breakpoint) | Create an MJML `` tag. | -| [mjml.font](mjml.font.qmd#emailer_lib.mjml.font) | Create an MJML `` tag. | -| [mjml.html_attributes](mjml.html_attributes.qmd#emailer_lib.mjml.html_attributes) | Create an MJML `` tag. | -| [mjml.preview](mjml.preview.qmd#emailer_lib.mjml.preview) | Create an MJML `` tag. | -| [mjml.style](mjml.style.qmd#emailer_lib.mjml.style) | Create an MJML `` tag. | -| [mjml.title](mjml.title.qmd#emailer_lib.mjml.title) | Create an MJML `` tag. | -| [mjml.accordion](mjml.accordion.qmd#emailer_lib.mjml.accordion) | Create an MJML `` tag. | -| [mjml.accordion_element](mjml.accordion_element.qmd#emailer_lib.mjml.accordion_element) | Create an MJML `` tag. | -| [mjml.accordion_text](mjml.accordion_text.qmd#emailer_lib.mjml.accordion_text) | Create an MJML `` tag. | -| [mjml.accordion_title](mjml.accordion_title.qmd#emailer_lib.mjml.accordion_title) | Create an MJML `` tag. | -| [mjml.button](mjml.button.qmd#emailer_lib.mjml.button) | Create an MJML `` tag. | -| [mjml.carousel](mjml.carousel.qmd#emailer_lib.mjml.carousel) | Create an MJML `` tag. | -| [mjml.carousel_image](mjml.carousel_image.qmd#emailer_lib.mjml.carousel_image) | Create an MJML `` tag. | -| [mjml.column](mjml.column.qmd#emailer_lib.mjml.column) | Create an MJML `` tag. | -| [mjml.divider](mjml.divider.qmd#emailer_lib.mjml.divider) | Create an MJML `` tag. | -| [mjml.group](mjml.group.qmd#emailer_lib.mjml.group) | Create an MJML `` tag. | -| [mjml.hero](mjml.hero.qmd#emailer_lib.mjml.hero) | Create an MJML `` tag. | -| [mjml.image](mjml.image.qmd#emailer_lib.mjml.image) | Create an MJML `` tag. | -| [mjml.navbar](mjml.navbar.qmd#emailer_lib.mjml.navbar) | Create an MJML `` tag. | -| [mjml.navbar_link](mjml.navbar_link.qmd#emailer_lib.mjml.navbar_link) | Create an MJML `` tag. | -| [mjml.raw](mjml.raw.qmd#emailer_lib.mjml.raw) | Create an MJML `` tag. | -| [mjml.section](mjml.section.qmd#emailer_lib.mjml.section) | Create an MJML `` tag. | -| [mjml.social](mjml.social.qmd#emailer_lib.mjml.social) | Create an MJML `` tag. | -| [mjml.social_element](mjml.social_element.qmd#emailer_lib.mjml.social_element) | Create an MJML `` tag. | -| [mjml.spacer](mjml.spacer.qmd#emailer_lib.mjml.spacer) | Create an MJML `` tag. | -| [mjml.table](mjml.table.qmd#emailer_lib.mjml.table) | Create an MJML `` tag. | -| [mjml.text](mjml.text.qmd#emailer_lib.mjml.text) | Create an MJML `` tag. | -| [mjml.wrapper](mjml.wrapper.qmd#emailer_lib.mjml.wrapper) | Create an MJML `` tag. | \ No newline at end of file diff --git a/docs/reference/mjml.accordion.qmd b/docs/reference/mjml.accordion.qmd deleted file mode 100644 index 1f7bb5d..0000000 --- a/docs/reference/mjml.accordion.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.accordion { #emailer_lib.mjml.accordion } - -```python -mjml.accordion(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.accordion_element.qmd b/docs/reference/mjml.accordion_element.qmd deleted file mode 100644 index ec8de8f..0000000 --- a/docs/reference/mjml.accordion_element.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.accordion_element { #emailer_lib.mjml.accordion_element } - -```python -mjml.accordion_element(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.accordion_text.qmd b/docs/reference/mjml.accordion_text.qmd deleted file mode 100644 index 6d760c2..0000000 --- a/docs/reference/mjml.accordion_text.qmd +++ /dev/null @@ -1,25 +0,0 @@ -# mjml.accordion_text { #emailer_lib.mjml.accordion_text } - -```python -mjml.accordion_text(content=None, **kwargs) -``` - -Create an MJML `` tag. - -This is an ending tag that accepts text/HTML content but not MJML children. - -## Parameters {.doc-section .doc-section-parameters} - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Text or HTML content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.accordion_title.qmd b/docs/reference/mjml.accordion_title.qmd deleted file mode 100644 index e3dd138..0000000 --- a/docs/reference/mjml.accordion_title.qmd +++ /dev/null @@ -1,25 +0,0 @@ -# mjml.accordion_title { #emailer_lib.mjml.accordion_title } - -```python -mjml.accordion_title(content=None, **kwargs) -``` - -Create an MJML `` tag. - -This is an ending tag that accepts text/HTML content but not MJML children. - -## Parameters {.doc-section .doc-section-parameters} - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Text or HTML content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.attributes.qmd b/docs/reference/mjml.attributes.qmd deleted file mode 100644 index a769245..0000000 --- a/docs/reference/mjml.attributes.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.attributes { #emailer_lib.mjml.attributes } - -```python -mjml.attributes(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.body.qmd b/docs/reference/mjml.body.qmd deleted file mode 100644 index ae833a8..0000000 --- a/docs/reference/mjml.body.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.body { #emailer_lib.mjml.body } - -```python -mjml.body(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.breakpoint.qmd b/docs/reference/mjml.breakpoint.qmd deleted file mode 100644 index 097aed1..0000000 --- a/docs/reference/mjml.breakpoint.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.breakpoint { #emailer_lib.mjml.breakpoint } - -```python -mjml.breakpoint(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.button.qmd b/docs/reference/mjml.button.qmd deleted file mode 100644 index 943ebc0..0000000 --- a/docs/reference/mjml.button.qmd +++ /dev/null @@ -1,25 +0,0 @@ -# mjml.button { #emailer_lib.mjml.button } - -```python -mjml.button(content=None, **kwargs) -``` - -Create an MJML `` tag. - -This is an ending tag that accepts text/HTML content but not MJML children. - -## Parameters {.doc-section .doc-section-parameters} - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Text or HTML content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.carousel.qmd b/docs/reference/mjml.carousel.qmd deleted file mode 100644 index c2fdc71..0000000 --- a/docs/reference/mjml.carousel.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.carousel { #emailer_lib.mjml.carousel } - -```python -mjml.carousel(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.carousel_image.qmd b/docs/reference/mjml.carousel_image.qmd deleted file mode 100644 index f1cf996..0000000 --- a/docs/reference/mjml.carousel_image.qmd +++ /dev/null @@ -1,25 +0,0 @@ -# mjml.carousel_image { #emailer_lib.mjml.carousel_image } - -```python -mjml.carousel_image(content=None, **kwargs) -``` - -Create an MJML `` tag. - -This is an ending tag that accepts text/HTML content but not MJML children. - -## Parameters {.doc-section .doc-section-parameters} - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Text or HTML content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.column.qmd b/docs/reference/mjml.column.qmd deleted file mode 100644 index 3609607..0000000 --- a/docs/reference/mjml.column.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.column { #emailer_lib.mjml.column } - -```python -mjml.column(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.divider.qmd b/docs/reference/mjml.divider.qmd deleted file mode 100644 index 8282212..0000000 --- a/docs/reference/mjml.divider.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.divider { #emailer_lib.mjml.divider } - -```python -mjml.divider(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.font.qmd b/docs/reference/mjml.font.qmd deleted file mode 100644 index c091a61..0000000 --- a/docs/reference/mjml.font.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.font { #emailer_lib.mjml.font } - -```python -mjml.font(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.group.qmd b/docs/reference/mjml.group.qmd deleted file mode 100644 index 78b5a42..0000000 --- a/docs/reference/mjml.group.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.group { #emailer_lib.mjml.group } - -```python -mjml.group(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.head.qmd b/docs/reference/mjml.head.qmd deleted file mode 100644 index 9f67652..0000000 --- a/docs/reference/mjml.head.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.head { #emailer_lib.mjml.head } - -```python -mjml.head(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.hero.qmd b/docs/reference/mjml.hero.qmd deleted file mode 100644 index c130649..0000000 --- a/docs/reference/mjml.hero.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.hero { #emailer_lib.mjml.hero } - -```python -mjml.hero(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.html_attributes.qmd b/docs/reference/mjml.html_attributes.qmd deleted file mode 100644 index 0ea7807..0000000 --- a/docs/reference/mjml.html_attributes.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.html_attributes { #emailer_lib.mjml.html_attributes } - -```python -mjml.html_attributes(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.image.qmd b/docs/reference/mjml.image.qmd deleted file mode 100644 index cd23326..0000000 --- a/docs/reference/mjml.image.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.image { #emailer_lib.mjml.image } - -```python -mjml.image(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.include.qmd b/docs/reference/mjml.include.qmd deleted file mode 100644 index ded3ee6..0000000 --- a/docs/reference/mjml.include.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.include { #emailer_lib.mjml.include } - -```python -mjml.include(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.mjml.qmd b/docs/reference/mjml.mjml.qmd deleted file mode 100644 index bcb5a22..0000000 --- a/docs/reference/mjml.mjml.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.mjml { #emailer_lib.mjml.mjml } - -```python -mjml.mjml(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.navbar.qmd b/docs/reference/mjml.navbar.qmd deleted file mode 100644 index 24d0771..0000000 --- a/docs/reference/mjml.navbar.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.navbar { #emailer_lib.mjml.navbar } - -```python -mjml.navbar(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.navbar_link.qmd b/docs/reference/mjml.navbar_link.qmd deleted file mode 100644 index f1cc908..0000000 --- a/docs/reference/mjml.navbar_link.qmd +++ /dev/null @@ -1,25 +0,0 @@ -# mjml.navbar_link { #emailer_lib.mjml.navbar_link } - -```python -mjml.navbar_link(content=None, **kwargs) -``` - -Create an MJML `` tag. - -This is an ending tag that accepts text/HTML content but not MJML children. - -## Parameters {.doc-section .doc-section-parameters} - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Text or HTML content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.preview.qmd b/docs/reference/mjml.preview.qmd deleted file mode 100644 index b9b6234..0000000 --- a/docs/reference/mjml.preview.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.preview { #emailer_lib.mjml.preview } - -```python -mjml.preview(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.raw.qmd b/docs/reference/mjml.raw.qmd deleted file mode 100644 index 01f75e0..0000000 --- a/docs/reference/mjml.raw.qmd +++ /dev/null @@ -1,25 +0,0 @@ -# mjml.raw { #emailer_lib.mjml.raw } - -```python -mjml.raw(content=None, **kwargs) -``` - -Create an MJML `` tag. - -This is an ending tag that accepts text/HTML content but not MJML children. - -## Parameters {.doc-section .doc-section-parameters} - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Text or HTML content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.section.qmd b/docs/reference/mjml.section.qmd deleted file mode 100644 index 830e26c..0000000 --- a/docs/reference/mjml.section.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.section { #emailer_lib.mjml.section } - -```python -mjml.section(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.social.qmd b/docs/reference/mjml.social.qmd deleted file mode 100644 index e7ae4c0..0000000 --- a/docs/reference/mjml.social.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.social { #emailer_lib.mjml.social } - -```python -mjml.social(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.social_element.qmd b/docs/reference/mjml.social_element.qmd deleted file mode 100644 index f4ffb32..0000000 --- a/docs/reference/mjml.social_element.qmd +++ /dev/null @@ -1,25 +0,0 @@ -# mjml.social_element { #emailer_lib.mjml.social_element } - -```python -mjml.social_element(content=None, **kwargs) -``` - -Create an MJML `` tag. - -This is an ending tag that accepts text/HTML content but not MJML children. - -## Parameters {.doc-section .doc-section-parameters} - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Text or HTML content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.spacer.qmd b/docs/reference/mjml.spacer.qmd deleted file mode 100644 index c5f77c6..0000000 --- a/docs/reference/mjml.spacer.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.spacer { #emailer_lib.mjml.spacer } - -```python -mjml.spacer(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.style.qmd b/docs/reference/mjml.style.qmd deleted file mode 100644 index 2f2b4a5..0000000 --- a/docs/reference/mjml.style.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.style { #emailer_lib.mjml.style } - -```python -mjml.style(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.table.qmd b/docs/reference/mjml.table.qmd deleted file mode 100644 index 38e485e..0000000 --- a/docs/reference/mjml.table.qmd +++ /dev/null @@ -1,25 +0,0 @@ -# mjml.table { #emailer_lib.mjml.table } - -```python -mjml.table(content=None, **kwargs) -``` - -Create an MJML `` tag. - -This is an ending tag that accepts text/HTML content but not MJML children. - -## Parameters {.doc-section .doc-section-parameters} - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Text or HTML content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.text.qmd b/docs/reference/mjml.text.qmd deleted file mode 100644 index 43e9a17..0000000 --- a/docs/reference/mjml.text.qmd +++ /dev/null @@ -1,25 +0,0 @@ -# mjml.text { #emailer_lib.mjml.text } - -```python -mjml.text(content=None, **kwargs) -``` - -Create an MJML `` tag. - -This is an ending tag that accepts text/HTML content but not MJML children. - -## Parameters {.doc-section .doc-section-parameters} - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Text or HTML content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.title.qmd b/docs/reference/mjml.title.qmd deleted file mode 100644 index cd774b5..0000000 --- a/docs/reference/mjml.title.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.title { #emailer_lib.mjml.title } - -```python -mjml.title(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml.wrapper.qmd b/docs/reference/mjml.wrapper.qmd deleted file mode 100644 index f5bb671..0000000 --- a/docs/reference/mjml.wrapper.qmd +++ /dev/null @@ -1,27 +0,0 @@ -# mjml.wrapper { #emailer_lib.mjml.wrapper } - -```python -mjml.wrapper(*args, content=None, **kwargs) -``` - -Create an MJML `` tag. - -## Parameters {.doc-section .doc-section-parameters} - -[***args**]{.parameter-name} [:]{.parameter-annotation-sep} [Union\[TagChild, TagAttrs\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [()]{.parameter-default} - -: Children or attribute dicts - -[**content**]{.parameter-name} [:]{.parameter-annotation-sep} [Optional\[str\]]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default} - -: Optional text content for the tag - -[****kwargs**]{.parameter-name} [:]{.parameter-annotation-sep} [TagAttrValue]{.parameter-annotation} [ = ]{.parameter-default-sep} [{}]{.parameter-default} - -: Tag attributes - -## Returns {.doc-section .doc-section-returns} - -[]{.parameter-name} [:]{.parameter-annotation-sep} [MJMLTag]{.parameter-annotation} - -: MJMLTag object representing \ No newline at end of file diff --git a/docs/reference/mjml_to_intermediate_email.qmd b/docs/reference/mjml_to_intermediate_email.qmd deleted file mode 100644 index b828f86..0000000 --- a/docs/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/docs/reference/quarto_json_to_intermediate_email.qmd b/docs/reference/quarto_json_to_intermediate_email.qmd deleted file mode 100644 index b3c8539..0000000 --- a/docs/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/docs/reference/redmail_to_intermediate_email.qmd b/docs/reference/redmail_to_intermediate_email.qmd deleted file mode 100644 index 7e03d00..0000000 --- a/docs/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/docs/reference/send_intermediate_email_with_gmail.qmd b/docs/reference/send_intermediate_email_with_gmail.qmd deleted file mode 100644 index 509d95b..0000000 --- a/docs/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/docs/reference/send_intermediate_email_with_mailgun.qmd b/docs/reference/send_intermediate_email_with_mailgun.qmd deleted file mode 100644 index fc7f9f5..0000000 --- a/docs/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/docs/reference/send_intermediate_email_with_redmail.qmd b/docs/reference/send_intermediate_email_with_redmail.qmd deleted file mode 100644 index 32c0d50..0000000 --- a/docs/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/docs/reference/send_intermediate_email_with_smtp.qmd b/docs/reference/send_intermediate_email_with_smtp.qmd deleted file mode 100644 index b353990..0000000 --- a/docs/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/docs/reference/send_intermediate_email_with_yagmail.qmd b/docs/reference/send_intermediate_email_with_yagmail.qmd deleted file mode 100644 index 9cda074..0000000 --- a/docs/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/docs/reference/send_quarto_email_with_gmail.qmd b/docs/reference/send_quarto_email_with_gmail.qmd deleted file mode 100644 index b152f26..0000000 --- a/docs/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/docs/reference/styles.css b/docs/reference/styles.css deleted file mode 100644 index 9be2d3a..0000000 --- a/docs/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/docs/reference/write_email_message_to_file.qmd b/docs/reference/write_email_message_to_file.qmd deleted file mode 100644 index 6f1fd86..0000000 --- a/docs/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/docs/reference/yagmail_to_intermediate_email.qmd b/docs/reference/yagmail_to_intermediate_email.qmd deleted file mode 100644 index a8f9f7f..0000000 --- a/docs/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 From e1af59370c9c129dba002331a7e694332106befa Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:57:52 -0400 Subject: [PATCH 23/29] ignore reference --- Makefile | 2 +- docs/.gitignore | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 50e7498..c0c4950 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -build-docs: +build-docs: generate-mjml-tags cd docs && uv run quartodoc build --verbose && quarto render preview: diff --git a/docs/.gitignore b/docs/.gitignore index 075b254..0709e39 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1 +1,2 @@ /.quarto/ +/reference/ From 19d5c326e7275ab352d40db7237acf5deb77f175 Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:58:04 -0400 Subject: [PATCH 24/29] update docs --- docs/objects.json | 2 +- docs/summary.quarto_ipynb | 251 -------------------------------------- 2 files changed, 1 insertion(+), 252 deletions(-) delete mode 100644 docs/summary.quarto_ipynb diff --git a/docs/objects.json b/docs/objects.json index 7dc4189..6165209 100644 --- a/docs/objects.json +++ b/docs/objects.json @@ -1 +1 @@ -{"project": "emailer_lib", "version": "0.0.9999", "count": 102, "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.include", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.include.html#emailer_lib.mjml.include", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.include", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.include.html#emailer_lib.mjml.include", "dispname": "emailer_lib.mjml.include"}, {"name": "emailer_lib.mjml.attributes", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.attributes.html#emailer_lib.mjml.attributes", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.attributes", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.attributes.html#emailer_lib.mjml.attributes", "dispname": "emailer_lib.mjml.attributes"}, {"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.preview", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.preview.html#emailer_lib.mjml.preview", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.preview", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.preview.html#emailer_lib.mjml.preview", "dispname": "emailer_lib.mjml.preview"}, {"name": "emailer_lib.mjml.style", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.style.html#emailer_lib.mjml.style", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.style", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.style.html#emailer_lib.mjml.style", "dispname": "emailer_lib.mjml.style"}, {"name": "emailer_lib.mjml.title", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.title.html#emailer_lib.mjml.title", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.title", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.title.html#emailer_lib.mjml.title", "dispname": "emailer_lib.mjml.title"}, {"name": "emailer_lib.mjml.accordion", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion.html#emailer_lib.mjml.accordion", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.accordion", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion.html#emailer_lib.mjml.accordion", "dispname": "emailer_lib.mjml.accordion"}, {"name": "emailer_lib.mjml.accordion_element", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_element.html#emailer_lib.mjml.accordion_element", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.accordion_element", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_element.html#emailer_lib.mjml.accordion_element", "dispname": "emailer_lib.mjml.accordion_element"}, {"name": "emailer_lib.mjml.accordion_text", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_text.html#emailer_lib.mjml.accordion_text", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.accordion_text", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_text.html#emailer_lib.mjml.accordion_text", "dispname": "emailer_lib.mjml.accordion_text"}, {"name": "emailer_lib.mjml.accordion_title", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_title.html#emailer_lib.mjml.accordion_title", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.accordion_title", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.accordion_title.html#emailer_lib.mjml.accordion_title", "dispname": "emailer_lib.mjml.accordion_title"}, {"name": "emailer_lib.mjml.button", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.button.html#emailer_lib.mjml.button", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.button", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.button.html#emailer_lib.mjml.button", "dispname": "emailer_lib.mjml.button"}, {"name": "emailer_lib.mjml.carousel", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.carousel.html#emailer_lib.mjml.carousel", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.carousel", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.carousel.html#emailer_lib.mjml.carousel", "dispname": "emailer_lib.mjml.carousel"}, {"name": "emailer_lib.mjml.carousel_image", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.carousel_image.html#emailer_lib.mjml.carousel_image", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.carousel_image", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.carousel_image.html#emailer_lib.mjml.carousel_image", "dispname": "emailer_lib.mjml.carousel_image"}, {"name": "emailer_lib.mjml.column", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.column.html#emailer_lib.mjml.column", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.column", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.column.html#emailer_lib.mjml.column", "dispname": "emailer_lib.mjml.column"}, {"name": "emailer_lib.mjml.divider", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.divider.html#emailer_lib.mjml.divider", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.divider", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.divider.html#emailer_lib.mjml.divider", "dispname": "emailer_lib.mjml.divider"}, {"name": "emailer_lib.mjml.group", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.group.html#emailer_lib.mjml.group", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.group", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.group.html#emailer_lib.mjml.group", "dispname": "emailer_lib.mjml.group"}, {"name": "emailer_lib.mjml.hero", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.hero.html#emailer_lib.mjml.hero", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.hero", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.hero.html#emailer_lib.mjml.hero", "dispname": "emailer_lib.mjml.hero"}, {"name": "emailer_lib.mjml.image", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.image.html#emailer_lib.mjml.image", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.image", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.image.html#emailer_lib.mjml.image", "dispname": "emailer_lib.mjml.image"}, {"name": "emailer_lib.mjml.navbar", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.navbar.html#emailer_lib.mjml.navbar", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.navbar", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.navbar.html#emailer_lib.mjml.navbar", "dispname": "emailer_lib.mjml.navbar"}, {"name": "emailer_lib.mjml.navbar_link", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.navbar_link.html#emailer_lib.mjml.navbar_link", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.navbar_link", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.navbar_link.html#emailer_lib.mjml.navbar_link", "dispname": "emailer_lib.mjml.navbar_link"}, {"name": "emailer_lib.mjml.raw", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.raw.html#emailer_lib.mjml.raw", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.raw", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.raw.html#emailer_lib.mjml.raw", "dispname": "emailer_lib.mjml.raw"}, {"name": "emailer_lib.mjml.section", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.section.html#emailer_lib.mjml.section", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.section", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.section.html#emailer_lib.mjml.section", "dispname": "emailer_lib.mjml.section"}, {"name": "emailer_lib.mjml.social", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.social.html#emailer_lib.mjml.social", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.social", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.social.html#emailer_lib.mjml.social", "dispname": "emailer_lib.mjml.social"}, {"name": "emailer_lib.mjml.social_element", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.social_element.html#emailer_lib.mjml.social_element", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.social_element", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.social_element.html#emailer_lib.mjml.social_element", "dispname": "emailer_lib.mjml.social_element"}, {"name": "emailer_lib.mjml.spacer", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.spacer.html#emailer_lib.mjml.spacer", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.spacer", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.spacer.html#emailer_lib.mjml.spacer", "dispname": "emailer_lib.mjml.spacer"}, {"name": "emailer_lib.mjml.table", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.table.html#emailer_lib.mjml.table", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.table", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.table.html#emailer_lib.mjml.table", "dispname": "emailer_lib.mjml.table"}, {"name": "emailer_lib.mjml.text", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.text.html#emailer_lib.mjml.text", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.text", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.text.html#emailer_lib.mjml.text", "dispname": "emailer_lib.mjml.text"}, {"name": "emailer_lib.mjml.wrapper", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.wrapper.html#emailer_lib.mjml.wrapper", "dispname": "-"}, {"name": "emailer_lib.mjml.tags.wrapper", "domain": "py", "role": "function", "priority": "1", "uri": "reference/mjml.wrapper.html#emailer_lib.mjml.wrapper", "dispname": "emailer_lib.mjml.wrapper"}]} \ No newline at end of file +{"project": "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/docs/summary.quarto_ipynb b/docs/summary.quarto_ipynb deleted file mode 100644 index 6d1da82..0000000 --- a/docs/summary.quarto_ipynb +++ /dev/null @@ -1,251 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# The whole game\n", - "\n", - "\n", - "\n", - "Emailing reports is a critical but challenging task for data science.\n", - "Mainly because you have to figure out generating the email content, configuring pieces like attachments, and orchestrating it (e.g. testing, or sending on a schedule). Moreover, content can range from simple layouts to more complex ones.\n", - "\n", - "In this tutorial, we'll walk through the whole game of sending email. We'll start with this simple example:\n" - ], - "id": "e23a4943" - }, - { - "cell_type": "code", - "metadata": {}, - "source": [ - "# | code-fold: true\n", - "# | eval: false\n", - "import os\n", - "from dotenv import load_dotenv\n", - "from data_polars import sp500\n", - "import redmail\n", - "\n", - "load_dotenv()\n", - "gmail_address = os.environ[\"GMAIL_ADDRESS\"]\n", - "gmail_app_password = os.environ[\"GMAIL_APP_PASSWORD\"]\n", - "\n", - "\n", - "email_subject = \"Report on Cars\"\n", - "email_body = sp500.head(10).style.as_raw_html(inline_css=True)\n", - "\n", - "# This is here to emphasize the sender does not have to be the same as the receiver\n", - "email_receiver = gmail_address\n", - "\n", - "redmail.gmail.username = gmail_address\n", - "redmail.gmail.password = gmail_app_password\n", - "\n", - "redmail.gmail.send(\n", - " subject=email_subject,\n", - " receivers=[email_receiver],\n", - " html=email_body,\n", - ")" - ], - "id": "0c2962ac", - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![](./assets/whole-game-email-annotated.png){width=50% fig-align=\"center\"}\n", - "\n", - "* **Content**: writing the text of the email, including plots and tables.\n", - "* **Composing**: setting up the subject, sender, and receivers.\n", - "* **Orchestrating**: previewing, testing, and scheduling the email.\n", - "\n", - "We'll also quickly review writing more advanced content layouts, and authoring email reports that involve running code with Quarto.\n", - "\n", - "## A simple email\n", - "\n", - "![](./assets/whole-game-email.png){width=50% fig-align=\"center\"}\n", - "\n", - "* Generate and preview\n", - "* Authenticate (may need to refer to its own authentication page in guide)\n", - "* Send\n" - ], - "id": "42715cd2" - }, - { - "cell_type": "code", - "metadata": {}, - "source": [ - "# | code-fold: true\n", - "# | eval: false\n", - "import os\n", - "from dotenv import load_dotenv\n", - "from data_polars import sp500\n", - "import redmail\n", - "\n", - "load_dotenv()\n", - "gmail_address = os.environ[\"GMAIL_ADDRESS\"]\n", - "gmail_app_password = os.environ[\"GMAIL_APP_PASSWORD\"]\n", - "\n", - "\n", - "email_subject = \"Report on Cars\"\n", - "email_body = sp500.head(10).style.as_raw_html(inline_css=True)\n", - "\n", - "# This is here to emphasize the sender does not have to be the same as the receiver\n", - "email_receiver = gmail_address\n", - "\n", - "redmail.gmail.username = gmail_address\n", - "redmail.gmail.password = gmail_app_password\n", - "\n", - "redmail.gmail.send(\n", - " subject=email_subject,\n", - " receivers=[email_receiver],\n", - " html=email_body,\n", - ")" - ], - "id": "b18f6823", - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Configure: subject, recipients, attachments\n", - "\n", - "* you could attach the data as a CSV attachment\n", - "\n", - "\n", - "## Orchestrate: save and preview\n", - "\n", - "* previewing email\n", - "* intermediate json, easy for sending email later\n", - "* embedding images makes previewing hard\n", - "* can always email to yourself (or use a test service like Litmus)\n", - "\n", - "## Content: Quarto authoring\n", - "\n", - "Here's our same simple email generated using quarto.\n", - "\n", - "\n", - "\n", - "![](./assets/whole-game-quarto.png){width=50% fig-align=\"center\"}\n", - "\n", - "* Focused on basic configuring, and content\n", - "* Sending happens via our tool\n", - "* Generate using quarto render\n", - "* Can preview email\n", - "\n", - "## Content: advanced layouts\n", - "\n", - "We'll highlight the key pieces (discussed later in this guide) to go from that simple email, to a more advanced on like below:\n", - "\n", - "![](./assets/whole-game-fancy.png){width=50% fig-align=\"center\"}\n", - "\n", - "\n", - "## Fridge\n", - "\n", - "In this tutorial, we are going to send an email from a Gmail account. To do so, you will need to [create an App Password](https://support.google.com/accounts/answer/185833). Note this is only possible if you've [enabled 2-step verification](https://support.google.com/accounts/answer/185839).\n", - "\n", - "\n", - "\n", - "::: {.callout-tip}\n", - "This is just one of many options: it is also possible to send emails in Python from other email providers (Outlook, ProtonMail, etc.), or even from a custom domain. To skip ahead to a discussion of alternative sending methods, see [Authentication](orchestrating-auth.qmd)\n", - ":::\n", - "\n", - "Once you've created your App Password, that is used as your Gmail password for sending with Python. \n", - "\n", - "There are many ways to store the password seperate from your email-sending code, so as to not expose any sensitive information. One such approach uses a `.env` file, and the ``dotenv` and `os` packages.\n", - "\n", - "```{.sh filename=\".env\"}\n", - "GMAIL_APP_PASSWORD=abcd abcd abcd abcd\n", - "```\n", - "\n", - "```{.python filename=\"main.py\"}\n", - "import os\n", - "from dotenv import load_dotenv\n", - "\n", - "load_dotenv()\n", - "\n", - "your_gmail_address = \"YourGmail@gmail.com\"\n", - "gmail_app_password = os.environ[\"GMAIL_APP_PASSWORD\"]\n", - "```\n", - "\n", - "Check out the email content we will send.\n" - ], - "id": "e40499c1" - }, - { - "cell_type": "code", - "metadata": {}, - "source": [ - "from data_polars import sp500\n", - "\n", - "sp500.head(10).style" - ], - "id": "60ababf8", - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And now we send the email!\n" - ], - "id": "14aeb803" - }, - { - "cell_type": "code", - "metadata": {}, - "source": [ - "# | eval: false\n", - "import redmail\n", - "\n", - "redmail.gmail.username = your_gmail_address\n", - "redmail.gmail.password = gmail_app_password\n", - "\n", - "redmail.gmail.send(\n", - " subject=\"An Example Email\",\n", - " receivers=[username],\n", - " html=email_html,\n", - " text=email_plaintext,\n", - ")" - ], - "id": "402bc819", - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "" - ], - "id": "658bb6d8" - } - ], - "metadata": { - "kernelspec": { - "name": "python3", - "language": "python", - "display_name": "Python 3 (ipykernel)", - "path": "/Users/jules-wg/Library/Python/3.9/share/jupyter/kernels/python3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} \ No newline at end of file From 317a90a6b1497bc5c43d8aff5997c076544a8b99 Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:58:25 -0400 Subject: [PATCH 25/29] update docs --- emailer_lib/mjml/tags.py | 662 ++++++++++++++++++++++++++++----------- 1 file changed, 478 insertions(+), 184 deletions(-) diff --git a/emailer_lib/mjml/tags.py b/emailer_lib/mjml/tags.py index 380937d..b0e060a 100644 --- a/emailer_lib/mjml/tags.py +++ b/emailer_lib/mjml/tags.py @@ -29,18 +29,27 @@ def mjml( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - mjml(child1, child2) + ```{python} + from emailer_lib.mjml import mjml, text + child = text("Hello World") + + result = mjml(child) + ``` With attributes: - mjml(attributes={"attr": "value"}) + ```{python} + result = mjml(attributes={"attr": "value"}) + ``` With both: - mjml(child1, child2, attributes={"attr": "value"}) + ```{python} + result = mjml(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mjml", *args, attributes=attributes, content=content) @@ -65,18 +74,27 @@ def head( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - head(child1, child2) + ```{python} + from emailer_lib.mjml import head, text + child = text("Hello World") + + result = head(child) + ``` With attributes: - head(attributes={"attr": "value"}) + ```{python} + result = head(attributes={"attr": "value"}) + ``` With both: - head(child1, child2, attributes={"attr": "value"}) + ```{python} + result = head(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-head", *args, attributes=attributes, content=content) @@ -101,18 +119,27 @@ def body( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - body(child1, child2) + ```{python} + from emailer_lib.mjml import body, text + child = text("Hello World") + + result = body(child) + ``` With attributes: - body(attributes={"attr": "value"}) + ```{python} + result = body(attributes={"attr": "value"}) + ``` With both: - body(child1, child2, attributes={"attr": "value"}) + ```{python} + result = body(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-body", *args, attributes=attributes, content=content) @@ -137,18 +164,27 @@ def mj_attributes( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - mj_attributes(child1, child2) + ```{python} + from emailer_lib.mjml import mj_attributes, text + child = text("Hello World") + + result = mj_attributes(child) + ``` With attributes: - mj_attributes(attributes={"attr": "value"}) + ```{python} + result = mj_attributes(attributes={"attr": "value"}) + ``` With both: - mj_attributes(child1, child2, attributes={"attr": "value"}) + ```{python} + result = mj_attributes(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-attributes", *args, attributes=attributes, content=content) @@ -173,18 +209,27 @@ def mj_all( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - mj_all(child1, child2) + ```{python} + from emailer_lib.mjml import mj_all, text + child = text("Hello World") + + result = mj_all(child) + ``` With attributes: - mj_all(attributes={"attr": "value"}) + ```{python} + result = mj_all(attributes={"attr": "value"}) + ``` With both: - mj_all(child1, child2, attributes={"attr": "value"}) + ```{python} + result = mj_all(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-all", *args, attributes=attributes, content=content) @@ -209,18 +254,27 @@ def mj_class( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - mj_class(child1, child2) + ```{python} + from emailer_lib.mjml import mj_class, text + child = text("Hello World") + + result = mj_class(child) + ``` With attributes: - mj_class(attributes={"attr": "value"}) + ```{python} + result = mj_class(attributes={"attr": "value"}) + ``` With both: - mj_class(child1, child2, attributes={"attr": "value"}) + ```{python} + result = mj_class(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-class", *args, attributes=attributes, content=content) @@ -245,18 +299,27 @@ def breakpoint( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - breakpoint(child1, child2) + ```{python} + from emailer_lib.mjml import breakpoint, text + child = text("Hello World") + + result = breakpoint(child) + ``` With attributes: - breakpoint(attributes={"attr": "value"}) + ```{python} + result = breakpoint(attributes={"attr": "value"}) + ``` With both: - breakpoint(child1, child2, attributes={"attr": "value"}) + ```{python} + result = breakpoint(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-breakpoint", *args, attributes=attributes, content=content) @@ -281,18 +344,27 @@ def font( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - font(child1, child2) + ```{python} + from emailer_lib.mjml import font, text + child = text("Hello World") + + result = font(child) + ``` With attributes: - font(attributes={"attr": "value"}) + ```{python} + result = font(attributes={"attr": "value"}) + ``` With both: - font(child1, child2, attributes={"attr": "value"}) + ```{python} + result = font(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-font", *args, attributes=attributes, content=content) @@ -317,18 +389,27 @@ def html_attributes( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - html_attributes(child1, child2) + ```{python} + from emailer_lib.mjml import html_attributes, text + child = text("Hello World") + + result = html_attributes(child) + ``` With attributes: - html_attributes(attributes={"attr": "value"}) + ```{python} + result = html_attributes(attributes={"attr": "value"}) + ``` With both: - html_attributes(child1, child2, attributes={"attr": "value"}) + ```{python} + result = html_attributes(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-html-attributes", *args, attributes=attributes, content=content) @@ -353,18 +434,27 @@ def html_attribute( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - html_attribute(child1, child2) + ```{python} + from emailer_lib.mjml import html_attribute, text + child = text("Hello World") + + result = html_attribute(child) + ``` With attributes: - html_attribute(attributes={"attr": "value"}) + ```{python} + result = html_attribute(attributes={"attr": "value"}) + ``` With both: - html_attribute(child1, child2, attributes={"attr": "value"}) + ```{python} + result = html_attribute(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-html-attribute", *args, attributes=attributes, content=content) @@ -389,18 +479,27 @@ def preview( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - preview(child1, child2) + ```{python} + from emailer_lib.mjml import preview, text + child = text("Hello World") + + result = preview(child) + ``` With attributes: - preview(attributes={"attr": "value"}) + ```{python} + result = preview(attributes={"attr": "value"}) + ``` With both: - preview(child1, child2, attributes={"attr": "value"}) + ```{python} + result = preview(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-preview", *args, attributes=attributes, content=content) @@ -425,18 +524,27 @@ def style( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - style(child1, child2) + ```{python} + from emailer_lib.mjml import style, text + child = text("Hello World") + + result = style(child) + ``` With attributes: - style(attributes={"attr": "value"}) + ```{python} + result = style(attributes={"attr": "value"}) + ``` With both: - style(child1, child2, attributes={"attr": "value"}) + ```{python} + result = style(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-style", *args, attributes=attributes, content=content) @@ -461,18 +569,27 @@ def title( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - title(child1, child2) + ```{python} + from emailer_lib.mjml import title, text + child = text("Hello World") + + result = title(child) + ``` With attributes: - title(attributes={"attr": "value"}) + ```{python} + result = title(attributes={"attr": "value"}) + ``` With both: - title(child1, child2, attributes={"attr": "value"}) + ```{python} + result = title(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-title", *args, attributes=attributes, content=content) @@ -497,18 +614,27 @@ def accordion( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - accordion(child1, child2) + ```{python} + from emailer_lib.mjml import accordion, text + child = text("Hello World") + + result = accordion(child) + ``` With attributes: - accordion(attributes={"attr": "value"}) + ```{python} + result = accordion(attributes={"attr": "value"}) + ``` With both: - accordion(child1, child2, attributes={"attr": "value"}) + ```{python} + result = accordion(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-accordion", *args, attributes=attributes, content=content) @@ -533,18 +659,27 @@ def accordion_element( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - accordion_element(child1, child2) + ```{python} + from emailer_lib.mjml import accordion_element, text + child = text("Hello World") + + result = accordion_element(child) + ``` With attributes: - accordion_element(attributes={"attr": "value"}) + ```{python} + result = accordion_element(attributes={"attr": "value"}) + ``` With both: - accordion_element(child1, child2, attributes={"attr": "value"}) + ```{python} + result = accordion_element(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-accordion-element", *args, attributes=attributes, content=content) @@ -569,18 +704,27 @@ def carousel( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - carousel(child1, child2) + ```{python} + from emailer_lib.mjml import carousel, text + child = text("Hello World") + + result = carousel(child) + ``` With attributes: - carousel(attributes={"attr": "value"}) + ```{python} + result = carousel(attributes={"attr": "value"}) + ``` With both: - carousel(child1, child2, attributes={"attr": "value"}) + ```{python} + result = carousel(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-carousel", *args, attributes=attributes, content=content) @@ -605,18 +749,27 @@ def column( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - column(child1, child2) + ```{python} + from emailer_lib.mjml import column, text + child = text("Hello World") + + result = column(child) + ``` With attributes: - column(attributes={"attr": "value"}) + ```{python} + result = column(attributes={"attr": "value"}) + ``` With both: - column(child1, child2, attributes={"attr": "value"}) + ```{python} + result = column(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-column", *args, attributes=attributes, content=content) @@ -641,18 +794,27 @@ def divider( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - divider(child1, child2) + ```{python} + from emailer_lib.mjml import divider, text + child = text("Hello World") + + result = divider(child) + ``` With attributes: - divider(attributes={"attr": "value"}) + ```{python} + result = divider(attributes={"attr": "value"}) + ``` With both: - divider(child1, child2, attributes={"attr": "value"}) + ```{python} + result = divider(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-divider", *args, attributes=attributes, content=content) @@ -677,18 +839,27 @@ def group( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - group(child1, child2) + ```{python} + from emailer_lib.mjml import group, text + child = text("Hello World") + + result = group(child) + ``` With attributes: - group(attributes={"attr": "value"}) + ```{python} + result = group(attributes={"attr": "value"}) + ``` With both: - group(child1, child2, attributes={"attr": "value"}) + ```{python} + result = group(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-group", *args, attributes=attributes, content=content) @@ -713,18 +884,27 @@ def hero( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - hero(child1, child2) + ```{python} + from emailer_lib.mjml import hero, text + child = text("Hello World") + + result = hero(child) + ``` With attributes: - hero(attributes={"attr": "value"}) + ```{python} + result = hero(attributes={"attr": "value"}) + ``` With both: - hero(child1, child2, attributes={"attr": "value"}) + ```{python} + result = hero(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-hero", *args, attributes=attributes, content=content) @@ -749,18 +929,27 @@ def image( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - image(child1, child2) + ```{python} + from emailer_lib.mjml import image, text + child = text("Hello World") + + result = image(child) + ``` With attributes: - image(attributes={"attr": "value"}) + ```{python} + result = image(attributes={"attr": "value"}) + ``` With both: - image(child1, child2, attributes={"attr": "value"}) + ```{python} + result = image(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-image", *args, attributes=attributes, content=content) @@ -785,18 +974,27 @@ def navbar( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - navbar(child1, child2) + ```{python} + from emailer_lib.mjml import navbar, text + child = text("Hello World") + + result = navbar(child) + ``` With attributes: - navbar(attributes={"attr": "value"}) + ```{python} + result = navbar(attributes={"attr": "value"}) + ``` With both: - navbar(child1, child2, attributes={"attr": "value"}) + ```{python} + result = navbar(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-navbar", *args, attributes=attributes, content=content) @@ -821,18 +1019,27 @@ def section( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - section(child1, child2) + ```{python} + from emailer_lib.mjml import section, text + child = text("Hello World") + + result = section(child) + ``` With attributes: - section(attributes={"attr": "value"}) + ```{python} + result = section(attributes={"attr": "value"}) + ``` With both: - section(child1, child2, attributes={"attr": "value"}) + ```{python} + result = section(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-section", *args, attributes=attributes, content=content) @@ -857,18 +1064,27 @@ def social( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - social(child1, child2) + ```{python} + from emailer_lib.mjml import social, text + child = text("Hello World") + + result = social(child) + ``` With attributes: - social(attributes={"attr": "value"}) + ```{python} + result = social(attributes={"attr": "value"}) + ``` With both: - social(child1, child2, attributes={"attr": "value"}) + ```{python} + result = social(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-social", *args, attributes=attributes, content=content) @@ -893,18 +1109,27 @@ def spacer( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - spacer(child1, child2) + ```{python} + from emailer_lib.mjml import spacer, text + child = text("Hello World") + + result = spacer(child) + ``` With attributes: - spacer(attributes={"attr": "value"}) + ```{python} + result = spacer(attributes={"attr": "value"}) + ``` With both: - spacer(child1, child2, attributes={"attr": "value"}) + ```{python} + result = spacer(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-spacer", *args, attributes=attributes, content=content) @@ -929,25 +1154,34 @@ def wrapper( Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With children: - wrapper(child1, child2) + ```{python} + from emailer_lib.mjml import wrapper, text + child = text("Hello World") + + result = wrapper(child) + ``` With attributes: - wrapper(attributes={"attr": "value"}) + ```{python} + result = wrapper(attributes={"attr": "value"}) + ``` With both: - wrapper(child1, child2, attributes={"attr": "value"}) + ```{python} + result = wrapper(child, attributes={"background-color": "yellow"}) + ``` """ return MJMLTag("mj-wrapper", *args, attributes=attributes, content=content) def accordion_text( - attributes: Optional[TagAttrs] = None, content: Optional[str] = None, + attributes: Optional[TagAttrs] = None, ): """ Create an MJML `` tag. @@ -956,30 +1190,36 @@ def accordion_text( Parameters ---------- - attributes - Optional dict of tag attributes content Text or HTML content for the tag + attributes + Optional dict of tag attributes Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With content: - accordion_text(content="Hello") - + ```{python} + from emailer_lib.mjml import accordion_text + + result = accordion_text("Hello") + ``` + With attributes and content: - accordion_text(attributes={"color": "red"}, content="Hello") + ```{python} + result = accordion_text("Hello", attributes={"background-color": "red"}) + ``` """ - return MJMLTag("mj-accordion-text", attributes=attributes, content=content, _is_leaf=True) + return MJMLTag("mj-accordion-text", content, attributes=attributes, _is_leaf=True) def accordion_title( - attributes: Optional[TagAttrs] = None, content: Optional[str] = None, + attributes: Optional[TagAttrs] = None, ): """ Create an MJML `` tag. @@ -988,30 +1228,36 @@ def accordion_title( Parameters ---------- - attributes - Optional dict of tag attributes content Text or HTML content for the tag + attributes + Optional dict of tag attributes Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With content: - accordion_title(content="Hello") - + ```{python} + from emailer_lib.mjml import accordion_title + + result = accordion_title("Hello") + ``` + With attributes and content: - accordion_title(attributes={"color": "red"}, content="Hello") + ```{python} + result = accordion_title("Hello", attributes={"background-color": "red"}) + ``` """ - return MJMLTag("mj-accordion-title", attributes=attributes, content=content, _is_leaf=True) + return MJMLTag("mj-accordion-title", content, attributes=attributes, _is_leaf=True) def button( - attributes: Optional[TagAttrs] = None, content: Optional[str] = None, + attributes: Optional[TagAttrs] = None, ): """ Create an MJML `` tag. @@ -1020,30 +1266,36 @@ def button( Parameters ---------- - attributes - Optional dict of tag attributes content Text or HTML content for the tag + attributes + Optional dict of tag attributes Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With content: - button(content="Hello") - + ```{python} + from emailer_lib.mjml import button + + result = button("Hello") + ``` + With attributes and content: - button(attributes={"color": "red"}, content="Hello") + ```{python} + result = button("Hello", attributes={"background-color": "red"}) + ``` """ - return MJMLTag("mj-button", attributes=attributes, content=content, _is_leaf=True) + return MJMLTag("mj-button", content, attributes=attributes, _is_leaf=True) def carousel_image( - attributes: Optional[TagAttrs] = None, content: Optional[str] = None, + attributes: Optional[TagAttrs] = None, ): """ Create an MJML `` tag. @@ -1052,30 +1304,36 @@ def carousel_image( Parameters ---------- - attributes - Optional dict of tag attributes content Text or HTML content for the tag + attributes + Optional dict of tag attributes Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With content: - carousel_image(content="Hello") - + ```{python} + from emailer_lib.mjml import carousel_image + + result = carousel_image("Hello") + ``` + With attributes and content: - carousel_image(attributes={"color": "red"}, content="Hello") + ```{python} + result = carousel_image("Hello", attributes={"background-color": "red"}) + ``` """ - return MJMLTag("mj-carousel-image", attributes=attributes, content=content, _is_leaf=True) + return MJMLTag("mj-carousel-image", content, attributes=attributes, _is_leaf=True) def navbar_link( - attributes: Optional[TagAttrs] = None, content: Optional[str] = None, + attributes: Optional[TagAttrs] = None, ): """ Create an MJML `` tag. @@ -1084,30 +1342,36 @@ def navbar_link( Parameters ---------- - attributes - Optional dict of tag attributes content Text or HTML content for the tag + attributes + Optional dict of tag attributes Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With content: - navbar_link(content="Hello") - + ```{python} + from emailer_lib.mjml import navbar_link + + result = navbar_link("Hello") + ``` + With attributes and content: - navbar_link(attributes={"color": "red"}, content="Hello") + ```{python} + result = navbar_link("Hello", attributes={"background-color": "red"}) + ``` """ - return MJMLTag("mj-navbar-link", attributes=attributes, content=content, _is_leaf=True) + return MJMLTag("mj-navbar-link", content, attributes=attributes, _is_leaf=True) def raw( - attributes: Optional[TagAttrs] = None, content: Optional[str] = None, + attributes: Optional[TagAttrs] = None, ): """ Create an MJML `` tag. @@ -1116,30 +1380,36 @@ def raw( Parameters ---------- - attributes - Optional dict of tag attributes content Text or HTML content for the tag + attributes + Optional dict of tag attributes Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With content: - raw(content="Hello") - + ```{python} + from emailer_lib.mjml import raw + + result = raw("Hello") + ``` + With attributes and content: - raw(attributes={"color": "red"}, content="Hello") + ```{python} + result = raw("Hello", attributes={"background-color": "red"}) + ``` """ - return MJMLTag("mj-raw", attributes=attributes, content=content, _is_leaf=True) + return MJMLTag("mj-raw", content, attributes=attributes, _is_leaf=True) def social_element( - attributes: Optional[TagAttrs] = None, content: Optional[str] = None, + attributes: Optional[TagAttrs] = None, ): """ Create an MJML `` tag. @@ -1148,30 +1418,36 @@ def social_element( Parameters ---------- - attributes - Optional dict of tag attributes content Text or HTML content for the tag + attributes + Optional dict of tag attributes Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With content: - social_element(content="Hello") - + ```{python} + from emailer_lib.mjml import social_element + + result = social_element("Hello") + ``` + With attributes and content: - social_element(attributes={"color": "red"}, content="Hello") + ```{python} + result = social_element("Hello", attributes={"background-color": "red"}) + ``` """ - return MJMLTag("mj-social-element", attributes=attributes, content=content, _is_leaf=True) + return MJMLTag("mj-social-element", content, attributes=attributes, _is_leaf=True) def table( - attributes: Optional[TagAttrs] = None, content: Optional[str] = None, + attributes: Optional[TagAttrs] = None, ): """ Create an MJML `` tag. @@ -1180,30 +1456,36 @@ def table( Parameters ---------- - attributes - Optional dict of tag attributes content Text or HTML content for the tag + attributes + Optional dict of tag attributes Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With content: - table(content="Hello") - + ```{python} + from emailer_lib.mjml import table + + result = table("Hello") + ``` + With attributes and content: - table(attributes={"color": "red"}, content="Hello") + ```{python} + result = table("Hello", attributes={"background-color": "red"}) + ``` """ - return MJMLTag("mj-table", attributes=attributes, content=content, _is_leaf=True) + return MJMLTag("mj-table", content, attributes=attributes, _is_leaf=True) def text( - attributes: Optional[TagAttrs] = None, content: Optional[str] = None, + attributes: Optional[TagAttrs] = None, ): """ Create an MJML `` tag. @@ -1212,30 +1494,36 @@ def text( Parameters ---------- - attributes - Optional dict of tag attributes content Text or HTML content for the tag + attributes + Optional dict of tag attributes Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With content: - text(content="Hello") - + ```{python} + from emailer_lib.mjml import text + + result = text("Hello") + ``` + With attributes and content: - text(attributes={"color": "red"}, content="Hello") + ```{python} + result = text("Hello", attributes={"background-color": "red"}) + ``` """ - return MJMLTag("mj-text", attributes=attributes, content=content, _is_leaf=True) + return MJMLTag("mj-text", content, attributes=attributes, _is_leaf=True) def title( - attributes: Optional[TagAttrs] = None, content: Optional[str] = None, + attributes: Optional[TagAttrs] = None, ): """ Create an MJML `` tag. @@ -1244,22 +1532,28 @@ def title( Parameters ---------- - attributes - Optional dict of tag attributes content Text or HTML content for the tag + attributes + Optional dict of tag attributes Returns ------- MJMLTag - MJMLTag object representing + MJMLTag object representing `` Examples -------- With content: - title(content="Hello") - + ```{python} + from emailer_lib.mjml import title + + result = title("Hello") + ``` + With attributes and content: - title(attributes={"color": "red"}, content="Hello") + ```{python} + result = title("Hello", attributes={"background-color": "red"}) + ``` """ - return MJMLTag("mj-title", attributes=attributes, content=content, _is_leaf=True) + return MJMLTag("mj-title", content, attributes=attributes, _is_leaf=True) From ba49e9ddb1ceca1dfc7b8e87549a92d64eeea553 Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:58:34 -0400 Subject: [PATCH 26/29] accept content as positional argument for leaf tags --- emailer_lib/mjml/_core.py | 54 +++++++++++++++-------- emailer_lib/mjml/scripts/generate_tags.py | 39 +++++++++++----- emailer_lib/mjml/tests/test_core.py | 4 +- 3 files changed, 64 insertions(+), 33 deletions(-) diff --git a/emailer_lib/mjml/_core.py b/emailer_lib/mjml/_core.py index afde4a3..d2068f2 100644 --- a/emailer_lib/mjml/_core.py +++ b/emailer_lib/mjml/_core.py @@ -51,31 +51,47 @@ def __init__( self.tagName = tagName self.attrs = TagAttrDict() self.children = [] - self.content = content self._is_leaf = _is_leaf - # 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." - ) - # Runtime validation for leaf tags if self._is_leaf: - # Leaf tags should not accept positional arguments (children) + # 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}> is a leaf tag and does not accept children. " - f"Use the content parameter instead: {tagName}(content='...')" + f"<{tagName}> content must be a string, int, or float, " + f"got {type(self.content).__name__}" ) - # Leaf tags content should be string-like - if content is not None and not isinstance(content, (str, int, float)): + + # Validate attributes parameter type + if attributes is not None and not isinstance(attributes, (dict, TagAttrDict)): raise TypeError( - f"<{tagName}> content must be a string, int, or float, " - f"got {type(content).__name__}" + 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 ( @@ -84,10 +100,10 @@ def __init__( 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) + + # 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 diff --git a/emailer_lib/mjml/scripts/generate_tags.py b/emailer_lib/mjml/scripts/generate_tags.py index 85fe503..ea1cede 100644 --- a/emailer_lib/mjml/scripts/generate_tags.py +++ b/emailer_lib/mjml/scripts/generate_tags.py @@ -110,18 +110,27 @@ def {py_name}( Returns ------- MJMLTag - MJMLTag object representing <{tag_name}> + MJMLTag object representing `<{tag_name}>` Examples -------- With children: - {py_name}(child1, child2) + ```{{python}} + from emailer_lib.mjml import {py_name}, text + child = text("Hello World") + + result = {py_name}(child) + ``` With attributes: - {py_name}(attributes={{"attr": "value"}}) + ```{{python}} + result = {py_name}(attributes={{"attr": "value"}}) + ``` With both: - {py_name}(child1, child2, attributes={{"attr": "value"}}) + ```{{python}} + result = {py_name}(child, attributes={{"background-color": "yellow"}}) + ``` """ return MJMLTag("{tag_name}", *args, attributes=attributes, content=content) ''' @@ -133,8 +142,8 @@ def {py_name}( function_code = f''' def {py_name}( - attributes: Optional[TagAttrs] = None, content: Optional[str] = None, + attributes: Optional[TagAttrs] = None, ): """ Create an MJML `<{tag_name}>` tag. @@ -143,25 +152,31 @@ def {py_name}( Parameters ---------- - attributes - Optional dict of tag attributes content Text or HTML content for the tag + attributes + Optional dict of tag attributes Returns ------- MJMLTag - MJMLTag object representing <{tag_name}> + MJMLTag object representing `<{tag_name}>` Examples -------- With content: - {py_name}(content="Hello") - + ```{{python}} + from emailer_lib.mjml import {py_name} + + result = {py_name}("Hello") + ``` + With attributes and content: - {py_name}(attributes={{"color": "red"}}, content="Hello") + ```{{python}} + result = {py_name}("Hello", attributes={{"background-color": "red"}}) + ``` """ - return MJMLTag("{tag_name}", attributes=attributes, content=content, _is_leaf=True) + return MJMLTag("{tag_name}", content, attributes=attributes, _is_leaf=True) ''' functions.append(function_code) diff --git a/emailer_lib/mjml/tests/test_core.py b/emailer_lib/mjml/tests/test_core.py index 95ec28d..2a1b3f7 100644 --- a/emailer_lib/mjml/tests/test_core.py +++ b/emailer_lib/mjml/tests/test_core.py @@ -136,8 +136,8 @@ def test_to_html_passes_kwargs_to_mjml2html(): def test_leaf_tag_raises_on_children(): - with pytest.raises(TypeError, match="is a leaf tag and does not accept children"): - MJMLTag("mj-text", MJMLTag("mj-column"), _is_leaf=True) + with pytest.raises(TypeError, match="is a leaf tag and accepts only one positional argument"): + MJMLTag("mj-text", "content", MJMLTag("mj-column"), _is_leaf=True) def test_leaf_tag_content_type_validation(): From 7ab176f4eadc2ccd2a7eb73a97af7247a37605c8 Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:09:12 -0400 Subject: [PATCH 27/29] remove duplicates --- emailer_lib/mjml/scripts/generate_tags.py | 1 - emailer_lib/mjml/tags.py | 45 ----------------------- 2 files changed, 46 deletions(-) diff --git a/emailer_lib/mjml/scripts/generate_tags.py b/emailer_lib/mjml/scripts/generate_tags.py index ea1cede..d84b7f0 100644 --- a/emailer_lib/mjml/scripts/generate_tags.py +++ b/emailer_lib/mjml/scripts/generate_tags.py @@ -23,7 +23,6 @@ "mj-html-attribute", # sub-attribute for mj-html-attributes "mj-preview", "mj-style", - "mj-title", # Body components "mj-accordion", "mj-accordion-element", diff --git a/emailer_lib/mjml/tags.py b/emailer_lib/mjml/tags.py index b0e060a..1cdbf1c 100644 --- a/emailer_lib/mjml/tags.py +++ b/emailer_lib/mjml/tags.py @@ -549,51 +549,6 @@ def style( return MJMLTag("mj-style", *args, attributes=attributes, content=content) -def title( - *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 title, text - child = text("Hello World") - - result = title(child) - ``` - - With attributes: - ```{python} - result = title(attributes={"attr": "value"}) - ``` - - With both: - ```{python} - result = title(child, attributes={"background-color": "yellow"}) - ``` - """ - return MJMLTag("mj-title", *args, attributes=attributes, content=content) - - def accordion( *args: TagChild, attributes: Optional[TagAttrs] = None, From 13ce57c878db7cab5119e062901f425e6583f76e Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:09:55 -0400 Subject: [PATCH 28/29] update testpaths --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b62cc54..51d4d01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [tool.pytest.ini_options] minversion = "6.0" addopts = "-ra --cov=emailer_lib --cov-report=term-missing" -testpaths = ["emailer_lib/tests", "emailer_lib/mjml/tests"] +testpaths = ["emailer_lib/**/tests"] [project] name = "emailer-lib" From 3530fb3660759b5a603a0693142a2ed0ce068cbe Mon Sep 17 00:00:00 2001 From: Jules <54960783+juleswg23@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:12:45 -0400 Subject: [PATCH 29/29] add tests --- emailer_lib/mjml/tests/test_core.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/emailer_lib/mjml/tests/test_core.py b/emailer_lib/mjml/tests/test_core.py index 2a1b3f7..935d955 100644 --- a/emailer_lib/mjml/tests/test_core.py +++ b/emailer_lib/mjml/tests/test_core.py @@ -145,6 +145,14 @@ def test_leaf_tag_content_type_validation(): MJMLTag("mj-text", content=["invalid", "list"], _is_leaf=True) +def test_leaf_tag_attributes_type_validation(): + with pytest.raises(TypeError, match="attributes must be a dict or TagAttrDict"): + MJMLTag("mj-text", "content", attributes="invalid", _is_leaf=True) + +def test_container_tag_attributes_type_validation(): + with pytest.raises(TypeError, match="attributes must be a dict or TagAttrDict.*use positional arguments for container tags"): + MJMLTag("mj-section", attributes="invalid") + def test_children_sequence_flattening(): child1 = MJMLTag("mj-text", content="Text 1") child2 = MJMLTag("mj-text", content="Text 2")