diff --git a/.gitignore b/.gitignore
index b7bb3c9..68bc17f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -158,11 +158,3 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
-
-# Google API oauth2 client credentials secrets
-# Sensitive file! It must never be exposed.
-/credentials.json
-
-# Google API authorized user file
-# Sensitive file! It must never be exposed.
-/token.json
diff --git a/README.md b/README.md
index 1421e6a..a89e46f 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
Mail templating and sending with Jupyter
\n","\n","\n","
News 📰\n","
\n"," {{ title }}\n","
\n","\n","
\n"," In metus est, sodales sit amet tellus id, fringilla gravida lorem\n","
\n","
💸 ECONOMICS\n","\n","
\n","\n","
\n"," Mauris volutpat pulvinar nunc, a mattis ex vehicula ac. Pellentesque molestie erat quis lacus porttitor, quis malesuada elit commodo. Mauris vehicula aliquam ligula at consequat. Sed pharetra dolor urna, posuere congue lorem porta a. In hac habitasse platea dictumst.\n","
\n","\n","
\n"," Source: News.\n","
\n","\n","
\n"," To the next, {{ first_name }}! 👋\n","
\n","\n","
\n"," We always arrive at your inbox around 06:09. Some email servers are stubborn and slow… Others are even worse and throw us into spam and/or promotions. Whenever you can't find us in your inbox, look in these two.\n","
\n","\n","
\n","\n","
\n"," Luciano Felix, {{ date }}.\n","
\n","\n","
\n"," News, A newsletter example.\n","
\n"," 200 R. Quatá, São Paulo - SP, 04546-041\n","
\n","\n","
\n"," Unsubscribe\n"," |\n"," Contact us\n","
\n","
\n","\n",""]},{"cell_type":"markdown","metadata":{},"source":["## **🖌️ Style**"]},{"cell_type":"code","execution_count":null,"metadata":{"vscode":{"languageId":"html"}},"outputs":[],"source":["%%script html\n",""]},{"cell_type":"markdown","metadata":{},"source":["## 📧 Sending"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["page.send()\n"]}],"metadata":{"kernelspec":{"display_name":"Python 3.10.4 ('env': venv)","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.10.4"},"orig_nbformat":4,"vscode":{"interpreter":{"hash":"359d27c1ede3156a31bf43fc0e3aefa4af1cc43923c1239e05b911c5a2f535d7"}}},"nbformat":4,"nbformat_minor":2}
diff --git a/public/python/utils.py b/public/python/utils.py
deleted file mode 100644
index 72902f0..0000000
--- a/public/python/utils.py
+++ /dev/null
@@ -1,2 +0,0 @@
-def get_first_name(full_name):
- return full_name.split(" ")[0]
diff --git a/requirements.txt b/requirements.txt
index 7aff8a2..16cc3ae 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,85 +1,9 @@
-argon2-cffi==21.3.0
-argon2-cffi-bindings==21.2.0
-asttokens==2.0.5
-attrs==21.4.0
-backcall==0.2.0
-beautifulsoup4==4.11.1
-bleach==5.0.1
-cachetools==5.2.0
-certifi==2022.6.15
-cffi==1.15.1
-charset-normalizer==2.1.0
-colorama==0.4.5
-cssutils==2.5.1
-debugpy==1.6.2
-decorator==5.1.1
-defusedxml==0.7.1
-entrypoints==0.4
-executing==0.9.0
-fastjsonschema==2.16.1
-google-api-core==2.8.2
-google-api-python-client==2.54.0
-google-auth==2.9.1
-google-auth-httplib2==0.1.0
-google-auth-oauthlib==0.5.2
-googleapis-common-protos==1.56.4
-httplib2==0.20.4
-idna==3.3
-ipykernel==6.15.1
-ipython==8.4.0
-ipython-genutils==0.2.0
-ipywidgets==7.7.1
-jedi==0.18.1
-Jinja2==3.1.2
-jsonschema==4.7.2
-jupyter-client==7.3.4
-jupyter-core==4.11.1
-jupyterlab-pygments==0.2.2
-jupyterlab-widgets==1.1.1
-MarkupSafe==2.1.1
-matplotlib-inline==0.1.3
-mistune==0.8.4
-nbclient==0.6.6
-nbconvert==6.5.0
-nbformat==5.4.0
-nest-asyncio==1.5.5
-notebook==6.4.12
-numpy==1.23.1
-oauthlib==3.2.0
-packaging==21.3
-pandas==1.4.3
-pandocfilters==1.5.0
-parso==0.8.3
-pickleshare==0.7.5
-prometheus-client==0.14.1
-prompt-toolkit==3.0.30
-protobuf==4.21.3
-psutil==5.9.1
-pure-eval==0.2.2
-pyasn1==0.4.8
-pyasn1-modules==0.2.8
-pycparser==2.21
-Pygments==2.12.0
-pyparsing==3.0.9
-pyrsistent==0.18.1
-python-dateutil==2.8.2
-pytz==2022.1
-pywin32==304
-pywinpty==2.0.6
-pyzmq==23.2.0
-requests==2.28.1
-requests-oauthlib==1.3.1
-rsa==4.9
-Send2Trash==1.8.0
-six==1.16.0
-soupsieve==2.3.2.post1
-stack-data==0.3.0
-terminado==0.15.0
-tinycss2==1.1.1
-tornado==6.2
-traitlets==5.3.0
-uritemplate==4.1.1
-urllib3==1.26.10
-wcwidth==0.2.5
-webencodings==0.5.1
-widgetsnbextension==3.6.1
+google-api-python-client
+google-auth-httplib2
+google-auth-oauthlib
+jinja2
+beautifulsoup4
+cssutils
+pandas
+ipykernel
+ipywidgets
\ No newline at end of file
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..93d3dc6
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,22 @@
+from setuptools import setup
+
+
+def parse_requirements(filename):
+ lines = (line.strip() for line in open(filename))
+
+ return [line for line in lines if line and not line.startswith("#")]
+
+
+if __name__ == "__main__":
+ setup(
+ name="Pypers",
+ version="1.0.0",
+ description="Mail templating and sending with Jupyter",
+ url="https://github.com/FelixLuciano/pypers",
+ author="Luciano Felix",
+ packages=["pypers"],
+ package_dir={"pypers": "src"},
+ package_data={"pypers": ["data/*"]},
+ license="MIT",
+ install_requires=parse_requirements("requirements.txt"),
+ )
diff --git a/src/Create.py b/src/Create.py
index 1786920..bbb4ac9 100644
--- a/src/Create.py
+++ b/src/Create.py
@@ -3,8 +3,8 @@
class Create:
- BASEDIR = Path("pages")
- TEMPLATE_FILENAME = Path("public", "template", "New Page.ipynb")
+ BASEDIR = Path.cwd()
+ TEMPLATE_FILENAME = Path(__file__).parent.joinpath("data", "New Page.ipynb")
def __init__(self, filename: Path):
self.filename = self.BASEDIR.joinpath(filename).with_suffix(
@@ -27,3 +27,5 @@ def create_file(self):
with open(self.filename, "w", encoding="utf-8") as page_file:
json.dump(template, page_file)
+
+ print(f"Created {self.filename.absolute()}")
diff --git a/src/Google.py b/src/Google.py
index a75f0bb..7471d99 100644
--- a/src/Google.py
+++ b/src/Google.py
@@ -20,8 +20,8 @@ class Google:
@cache
@staticmethod
def authenticate(_is_retry=False):
- credentials_file = Path("env", "credentials.json")
- token_file = Path("env", "token.json")
+ credentials_file = Path("credentials.json")
+ token_file = Path("token.json")
if token_file.exists():
Google.credentials = Credentials.from_authorized_user_file(
diff --git a/src/Preview.py b/src/Preview.py
index 059cff5..eaa406e 100644
--- a/src/Preview.py
+++ b/src/Preview.py
@@ -1,3 +1,5 @@
+from pathlib import Path
+
import ipywidgets as widgets
from bs4 import BeautifulSoup
from IPython.display import HTML, clear_output, display
@@ -11,10 +13,9 @@ class Preview:
@staticmethod
def render(page, user):
content = BeautifulSoup(page.render(user), "html.parser")
+ template = Path(__file__).parent.joinpath("data", "preview.html")
- with open(
- "public/template/preview.html", "r", encoding="utf-8"
- ) as template_file:
+ with open(template, "r", encoding="utf-8") as template_file:
template = BeautifulSoup(template_file, "html.parser")
anchor = template.select_one("page-preview")
@@ -29,9 +30,16 @@ def display(page):
if hasattr(users, "name_column"):
if isinstance(users.name_column, str):
- mails = users[users.name_column] + " <" + users[users.email_column] + ">"
+ mails = (
+ users[users.name_column] + " <" + users[users.email_column] + ">"
+ )
else:
- mails = users.loc[:, users.name_column].apply(' - '.join, 1) + " <" + users[users.email_column] + ">"
+ mails = (
+ users.loc[:, users.name_column].apply(" - ".join, 1)
+ + " <"
+ + users[users.email_column]
+ + ">"
+ )
else:
mails = users[users.email_column]
diff --git a/src/Workspace.py b/src/Workspace.py
index fa82ea8..11e174a 100644
--- a/src/Workspace.py
+++ b/src/Workspace.py
@@ -6,26 +6,14 @@
class Workspace:
- HIDDEN_FILES = (
- "**/env",
- "**/src",
- "**/.vscode",
- "**/public/image",
- "**/public/template",
- ".gitignore",
- "requirements.txt",
- )
-
@staticmethod
- def check_vsc_ipynb_file():
+ def get_ipynb_file():
scope = vars(__main__)
if "__vsc_ipynb_file__" not in scope:
raise Exception("Pypers only work at VS Code Jupyter!")
- @staticmethod
- def get_ipynb_file():
- return Path(vars(__main__)["__vsc_ipynb_file__"])
+ return scope["__vsc_ipynb_file__"]
@staticmethod
def get_ipynb():
@@ -43,6 +31,6 @@ def get_html_source():
cell_source = cell["source"]
if len(cell_source) > 1 and cell_source[0] == "%%script html\n":
- source.extend(cell["source"][1:])
+ source.extend(cell_source[1:])
return "".join(source)
diff --git a/src/__init__.py b/src/__init__.py
index 832fa6d..7e56619 100644
--- a/src/__init__.py
+++ b/src/__init__.py
@@ -1,22 +1,21 @@
-import logging
-import warnings
+if __name__ != "__main__":
+ import logging
+ import warnings
-import cssutils
+ import cssutils
-from .Create import Create as create
-from .Google import Google as google
-from .Page import Page as page
-from .Preview import Preview as preview
-from .Send import Send as send
-from .Workspace import Workspace as workspace
+ from .Create import Create as create
+ from .Google import Google as google
+ from .Page import Page as page
+ from .Preview import Preview as preview
+ from .Send import Send as send
+ from .Workspace import Workspace as workspace
-workspace.check_vsc_ipynb_file()
+ warnings.simplefilter(action="ignore")
-warnings.simplefilter(action='ignore')
+ cssutils.ser.prefs.keepComments = False
+ cssutils.ser.prefs.lineSeparator = ""
+ cssutils.ser.prefs.propertyNameSpacer = ""
-cssutils.ser.prefs.keepComments = False
-cssutils.ser.prefs.lineSeparator = ""
-cssutils.ser.prefs.propertyNameSpacer = ""
-
-cssutils.log.setLevel(logging.CRITICAL)
+ cssutils.log.setLevel(logging.CRITICAL)
diff --git a/src/__main__.py b/src/__main__.py
index 378bc38..56253d5 100644
--- a/src/__main__.py
+++ b/src/__main__.py
@@ -4,17 +4,10 @@
def main(args):
- if args.action == "setup":
- import subprocess
- import venv
-
- venv.create("env", with_pip=True)
- subprocess.run([Path("env", "Scripts", "pip"), "-r", "requirements.txt"])
-
- elif args.action == "create":
+ if args.action == "create":
from os import startfile
- from Create import Create
+ from .Create import Create
new_page = Create(args.dest)
@@ -27,7 +20,6 @@ def main(args):
def parse_arguments():
parser = argparse.ArgumentParser()
subparser = parser.add_subparsers(dest="action")
- setup = subparser.add_parser("setup")
create = subparser.add_parser("create")
create.add_argument(
diff --git a/public/template/new page.ipynb b/src/data/new page.ipynb
similarity index 96%
rename from public/template/new page.ipynb
rename to src/data/new page.ipynb
index 3de7270..2791958 100644
--- a/public/template/new page.ipynb
+++ b/src/data/new page.ipynb
@@ -21,7 +21,7 @@
"outputs": [],
"source": [
"import pandas as pd\n",
- "from src import page\n",
+ "from pypers import page\n",
"\n",
"users = pd.read_csv(\"public/data/mailing-list.csv\")\n",
"users.email_column = \"email\"\n",
@@ -171,7 +171,7 @@
"orig_nbformat": 4,
"vscode": {
"interpreter": {
- "hash": "359d27c1ede3156a31bf43fc0e3aefa4af1cc43923c1239e05b911c5a2f535d7"
+ "hash": "06533af5f6072c33c887b7ac5ff0332542541b8cfbdba87749e169dacaf4c9ce"
}
}
},
diff --git a/public/template/preview.html b/src/data/preview.html
similarity index 100%
rename from public/template/preview.html
rename to src/data/preview.html
diff --git a/template/.gitignore b/template/.gitignore
new file mode 100644
index 0000000..b7bb3c9
--- /dev/null
+++ b/template/.gitignore
@@ -0,0 +1,168 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# Google API oauth2 client credentials secrets
+# Sensitive file! It must never be exposed.
+/credentials.json
+
+# Google API authorized user file
+# Sensitive file! It must never be exposed.
+/token.json
diff --git a/.vscode/extensions.json b/template/.vscode/extensions.json
similarity index 100%
rename from .vscode/extensions.json
rename to template/.vscode/extensions.json
diff --git a/.vscode/settings.json b/template/.vscode/settings.json
similarity index 100%
rename from .vscode/settings.json
rename to template/.vscode/settings.json
diff --git a/.vscode/tasks.json b/template/.vscode/tasks.json
similarity index 53%
rename from .vscode/tasks.json
rename to template/.vscode/tasks.json
index f7fba9a..9a4b12a 100644
--- a/.vscode/tasks.json
+++ b/template/.vscode/tasks.json
@@ -11,26 +11,6 @@
}
],
"tasks": [
- {
- "label": "Setup environment",
- "detail": "Setup project environsment and install dependencies (Run once!)",
- "icon": {
- "id": "package"
- },
- "options": {
- "statusbar": {
- "backgroundColor": "statusBarItem.warningBackground",
- "filePattern": "README.md"
- }
- },
- "args": [
- "src",
- "setup"
- ],
- "presentation": {
- "close": true
- }
- },
{
"label": "New page",
"detail": "Create a new page.",
@@ -38,9 +18,10 @@
"id": "file-add"
},
"args": [
- "src",
+ "-m",
+ "pypers",
"create",
- "${input:pageLocation}"
+ "pages${pathSeparator}${input:pageLocation}"
],
"presentation": {
"reveal": "never",
diff --git a/template/pages/examples/newsletter.ipynb b/template/pages/examples/newsletter.ipynb
new file mode 100644
index 0000000..18b6449
--- /dev/null
+++ b/template/pages/examples/newsletter.ipynb
@@ -0,0 +1 @@
+{"cells":[{"cell_type":"markdown","metadata":{},"source":["# **News 📰**"]},{"cell_type":"markdown","metadata":{},"source":["## **🔧 Config**"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["from pypers import page\n","from public.python.sheets import Sheets\n","\n","# https://docs.google.com/spreadsheets/d/1gHyAd0czD_clb49oRxC8hjYMl8Mvhgl3kjBG7BqnA78/edit\n","users = Sheets.fetch_table(\n"," \"1gHyAd0czD_clb49oRxC8hjYMl8Mvhgl3kjBG7BqnA78\", \"Subscribers\"\n",")\n","users.email_column = \"email\"\n","users.name_column = \"name\"\n"]},{"cell_type":"markdown","metadata":{},"source":["## **🔠 Props**"]},{"cell_type":"code","execution_count":2,"metadata":{},"outputs":[{"data":{"text/plain":["'August 22 of 2022'"]},"execution_count":2,"metadata":{},"output_type":"execute_result"}],"source":["import datetime\n","\n","_now = datetime.datetime.now()\n","date = _now.strftime(f\"%B %d of %Y\")\n","\n","date\n"]},{"cell_type":"code","execution_count":3,"metadata":{},"outputs":[],"source":["from public.python import utils\n","\n","\n","@page.user_prop\n","def first_name(user):\n"," return utils.get_first_name(user[users.name_column])\n"]},{"cell_type":"code","execution_count":4,"metadata":{},"outputs":[{"data":{"application/vnd.jupyter.widget-view+json":{"model_id":"5b005c762f51466d8a654afb086f4dce","version_major":2,"version_minor":0},"text/plain":["Text(value='Lorem ipsum dolor sit amet', description='Title:')"]},"execution_count":4,"metadata":{},"output_type":"execute_result"}],"source":["import ipywidgets as widgets\n","\n","title = widgets.Text(description=\"Title:\", value=\"Lorem ipsum dolor sit amet\")\n","\n","title\n"]},{"cell_type":"markdown","metadata":{},"source":["## **🔍 Preview**"]},{"cell_type":"code","execution_count":5,"metadata":{},"outputs":[{"data":{"application/vnd.jupyter.widget-view+json":{"model_id":"4829daab86a74ec082bf7f51352a555d","version_major":2,"version_minor":0},"text/plain":["HBox(children=(Dropdown(description='Preview as:', disabled=True, layout=Layout(flex='1 1 100%'), options=('Lu…"]},"metadata":{},"output_type":"display_data"},{"data":{"text/html":["\n","\n","\n","
\n","
News 📰\n","
\n"," Lorem ipsum dolor sit amet\n","
\n","
\n"," In metus est, sodales sit amet tellus id, fringilla gravida lorem\n","
\n","
💸 ECONOMICS\n","
\n","
\n"," Mauris volutpat pulvinar nunc, a mattis ex vehicula ac. Pellentesque molestie erat quis lacus porttitor, quis malesuada elit commodo. Mauris vehicula aliquam ligula at consequat. Sed pharetra dolor urna, posuere congue lorem porta a. In hac habitasse platea dictumst.\n","
\n","
\n"," Source: News.\n","
\n","
\n"," To the next, Luna! 👋\n","
\n","
\n"," We always arrive at your inbox around 06:09. Some email servers are stubborn and slow… Others are even worse and throw us into spam and/or promotions. Whenever you can't find us in your inbox, look in these two.\n","
\n","
\n","
\n","Luciano Felix, August 22 of 2022.\n","
\n","
\n"," News, A newsletter example.\n","
\n"," 200 R. Quatá, São Paulo - SP, 04546-041\n","
\n","
\n","Unsubscribe\n"," |\n"," Contact us\n","
\n","
\n","
\n"],"text/plain":["