Skip to content

Commit

Permalink
seperating render logic and adding setup.py
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelcolvin committed Jun 15, 2017
1 parent e666e5a commit 4feea3e
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 83 deletions.
12 changes: 12 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,19 @@ before_script:
script:
- make lint
- make test
- python setup.py build

after_success:
- ls -lha
- bash <(curl -s https://codecov.io/bash)

deploy:
- provider: pypi
user: samuelcolvin
password:
secure: "cmrF8mXOryNCMrTOg/1pZ5a8eN+rQDjmwPeQuud6AMvPxBfIP0F3okUhiusfq7tth8mwEOuz1FUAlHXLHdZP22HgxV/w/p/5wqDlzeIsszihUyXKnFuh/Q48zCBL9fqqIkQEBZFys6CyzHlSJjfGTncxTCCGY5Io/235lamgjNxW75SM/qZgvxEAve5l/yO33wmnk17HZsJ8Wdv181hegV/MPeOpI3HH+K3xrLI+VgclYlGcXv88Bveoms7CoKNsklbqHuOb6GcCjXM5+KzYE0Gv8Hv9xFp13qcxhIKyzPOT8zIUkCRNbUJ9YWXueVCqOZYJST1XdBD1V2RiDjthkpsuOe8N5kdegwciDGAt8elPFQVCiS1g08jexikqPLsymJyEU8jTsJs9IZNrB/l6fJi5WZ1SaysgYFiXtEF4wv72RwI0CQmFeVSHEfVHuKWXsY0biAwXTKTQnTkQmD4u73Kt1zcyswCDQw760kylSIwuJTBii4dit4XyG2fxL5aUFBFsWNw8jCyZajCAclQNBjaVtaGWdKbT33O8iR/qws4HYAbaDU+kFAsKH7S0jmHhpiwjQ/Hdr+g6adTrkKuk/PXvORWM6oiUUbkuH0wMEK0nKo/fn2p7sd65BycyfcEDrP9y2qdAB8dUdk2pBngF2miYVFBuJMGp9GQwzN8LnH0="
distributions: sdist bdist_wheel
skip_upload_docs: true
on:
tags: true
python: 3.6
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ isort:
lint:
flake8 morpheus/ tests/
pytest morpheus -p no:sugar -q
python setup.py check -rms

.PHONY: test
test:
Expand Down
2 changes: 2 additions & 0 deletions morpheus/app/render/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# flake8: noqa
from .main import EmailInfo, MessageDef, render_email
89 changes: 89 additions & 0 deletions morpheus/app/render/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import logging
import re
from typing import Dict, NamedTuple

import chevron
import misaka
from misaka import HtmlRenderer, Markdown

markdown = Markdown(HtmlRenderer(flags=[misaka.HTML_HARD_WRAP]), extensions=[misaka.EXT_NO_INTRA_EMPHASIS])
logger = logging.getLogger('morpheus.render')


class MessageDef(NamedTuple):
first_name: str
last_name: str
main_template: str
mustache_partials: Dict[str, dict]
macros: Dict[str, dict]
subject_template: str
context: dict
headers: dict


class EmailInfo(NamedTuple):
full_name: str
subject: str
html_body: str
headers: dict


def _update_context(context, partials, macros):
for k, v in context.items():
if k.endswith('__md'):
yield k[:-4], markdown(v)
elif k.endswith('__render'):
v = chevron.render(
_apply_macros(v, macros),
data=context,
partials_dict=partials
)
yield k[:-8], markdown(v)


def _apply_macros(s, macros):
if macros:
for key, body in macros.items():
m = re.search('^(\S+)\((.*)\) *$', key)
if not m:
logger.warning('invalid macro "%s", skipping it', key)
continue
name, arg_defs = m.groups()
arg_defs = [a.strip(' ') for a in arg_defs.split('|') if a.strip(' ')]

def replace_macro(m):
arg_values = [a.strip(' ') for a in m.groups()[0].split('|') if a.strip(' ')]
if len(arg_defs) != len(arg_values):
logger.warning('invalid macro call "%s", not replacing', m.group())
return m.group()
else:
return chevron.render(body, data=dict(zip(arg_defs, arg_values)))

s = re.sub(r'%s\((.*?)\)' % name, replace_macro, s)
return s


def render_email(m: MessageDef) -> EmailInfo:
full_name = f'{m.first_name or ""} {m.last_name or ""}'.strip(' ')
m.context.setdefault('recipient_name', full_name)
m.context.setdefault('recipient_first_name', m.first_name or full_name)
m.context.setdefault('recipient_last_name', m.last_name)
subject = chevron.render(m.subject_template, data=m.context)
m.context.update(
subject=subject,
**dict(_update_context(m.context, m.mustache_partials, m.macros))
)
unsubscribe_link = m.context.get('unsubscribe_link')
if unsubscribe_link:
m.headers.setdefault('List-Unsubscribe', f'<{unsubscribe_link}>')

return EmailInfo(
full_name=full_name,
subject=subject,
html_body=chevron.render(
_apply_macros(m.main_template, m.macros),
data=m.context,
partials_dict=m.mustache_partials,
),
headers=m.headers,
)
85 changes: 4 additions & 81 deletions morpheus/app/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,20 @@
from pathlib import Path
from typing import Dict, List, NamedTuple

import chevron
import misaka
import msgpack
from aiohttp import ClientSession
from arq import Actor, BaseWorker, Drain, concurrent
from misaka import HtmlRenderer, Markdown

from .es import ElasticSearch
from .models import MessageStatus, SendMethod
from .render import EmailInfo, render_email
from .settings import Settings
from .utils import Mandrill

test_logger = logging.getLogger('morpheus.worker.test')
main_logger = logging.getLogger('morpheus.worker')


markdown = Markdown(HtmlRenderer(flags=[misaka.HTML_HARD_WRAP]), extensions=[misaka.EXT_NO_INTRA_EMPHASIS])


class Job(NamedTuple):
group_id: str
send_method: str
Expand All @@ -46,14 +41,6 @@ class Job(NamedTuple):
headers: dict


class EmailInfo(NamedTuple):
full_name: str
subject: str
html_body: str
signing_domain: str
headers: dict


class Sender(Actor):
def __init__(self, settings: Settings=None, **kwargs):
self.settings = settings or Settings()
Expand Down Expand Up @@ -138,7 +125,7 @@ async def send(self,
return jobs

async def _send_mandrill(self, j: Job):
email_info = self._get_email_info(j)
email_info = render_email(j)
data = {
'async': True,
'message': dict(
Expand All @@ -157,7 +144,7 @@ async def _send_mandrill(self, j: Job):
track_opens=True,
auto_text=True,
view_content_link=False,
signing_domain=email_info.signing_domain,
signing_domain=j.from_email[j.from_email.index('@') + 1:],
subaccount=j.subaccount,
tags=j.tags,
inline_css=True,
Expand All @@ -180,15 +167,14 @@ async def _send_mandrill(self, j: Job):
await self._store_msg(data['_id'], send_ts, j, email_info)

async def _send_test(self, j: Job):
email_info = self._get_email_info(j)
email_info = render_email(j)
data = dict(
from_email=j.from_email,
from_name=j.from_name,
group_id=j.group_id,
headers=email_info.headers,
to_email=j.address,
to_name=email_info.full_name,
signing_domain=email_info.signing_domain,
tags=j.tags,
important=j.important,
attachments=[dict(
Expand All @@ -215,69 +201,6 @@ async def _send_test(self, j: Job):
save_path.write_text(output)
await self._store_msg(msg_id, send_ts, j, email_info)

@classmethod
def _update_context(cls, context, partials, macros):
for k, v in context.items():
if k.endswith('__md'):
yield k[:-4], markdown(v)
elif k.endswith('__render'):
v = chevron.render(
cls._apply_macros(v, macros),
data=context,
partials_dict=partials
)
yield k[:-8], markdown(v)

@staticmethod
def _apply_macros(s, macros):
if macros:
for key, body in macros.items():
m = re.search('^(\S+)\((.*)\) *$', key)
if not m:
main_logger.warning('invalid macro "%s", skipping it', key)
continue
name, arg_defs = m.groups()
arg_defs = [a.strip(' ') for a in arg_defs.split('|') if a.strip(' ')]

def replace_macro(m):
arg_values = [a.strip(' ') for a in m.groups()[0].split('|') if a.strip(' ')]
if len(arg_defs) != len(arg_values):
main_logger.warning('invalid macro call "%s", not replacing', m.group())
return m.group()
else:
return chevron.render(body, data=dict(zip(arg_defs, arg_values)))

s = re.sub(r'%s\((.*?)\)' % name, replace_macro, s)
return s

def _get_email_info(self, j: Job) -> EmailInfo:
full_name = f'{j.first_name} {j.last_name}'.strip(' ')
j.context.update(
first_name=j.first_name,
last_name=j.last_name,
full_name=full_name,
)
subject = chevron.render(j.subject_template, data=j.context)
j.context.update(
subject=subject,
**dict(self._update_context(j.context, j.mustache_partials, j.macros))
)
unsubscribe_link = j.context.get('unsubscribe_link')
if unsubscribe_link:
j.headers.setdefault('List-Unsubscribe', f'<{unsubscribe_link}>')

return EmailInfo(
full_name=full_name,
subject=subject,
html_body=chevron.render(
self._apply_macros(j.main_template, j.macros),
data=j.context,
partials_dict=j.mustache_partials,
),
signing_domain=j.from_email[j.from_email.index('@') + 1:],
headers=j.headers,
)

async def _generate_base64_pdf(self, html):
if not self.settings.pdf_generation_url:
return 'no-pdf-generated'
Expand Down
2 changes: 1 addition & 1 deletion morpheus/requirements_two.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
aiohttp==2.1.0
aioredis==0.3.1
arq==0.8.1
chevron==0.9.0
chevron==0.10.0
click==6.7
ipython==6.1.0
misaka==2.1.0
Expand Down
39 changes: 39 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from setuptools import setup

setup(
name='morpheus-mail',
version='0.0.1',
description='Email rendering engine from morpheus',
long_description="""
Note: this is only installs the rendering logic for morpheus for testing and email preview.
Everything else is excluded to avoid installing unnecessary packages.
""",
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Console',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.6',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: MIT License',
'Operating System :: Unix',
'Operating System :: POSIX :: Linux',
'Topic :: Internet',
],
author='Samuel Colvin',
author_email='s@muelcolvin.com',
url='https://github.com/tutorcruncher/morpheus',
license='MIT',
packages=['morpheus.render'],
package_dir={'morpheus.render': 'morpheus/app/render'},
python_requires='>=3.6',
zip_safe=True,
install_requires=[
'misaka>=2.1.0',
'chevron>=0.10.0',
],
)
2 changes: 1 addition & 1 deletion tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ async def test_macro_in_message(send_message, tmpdir):
assert """
content:
<body>
<h1>hello</h1>
<h1>hello John</h1>
<div class="button">
<a href="/pay/now/123/"><span>Pay now</span></a>
Expand Down

0 comments on commit 4feea3e

Please sign in to comment.