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

- Page example + Page example

@@ -19,12 +19,22 @@ In order to send the pages by Gmail or fetch data from Google Sheets, Google Clo See the [Getting started with authentication](https://cloud.google.com/docs/authentication/getting-started) for Google Cloud API. Then put your key at `[PROJECT FOLDER]/credentials.json`. and it's ready to use! -## Install dependencies +## Install -This project has some dependencies (see [`requirements.txt`](requirements.txt)) that are not included in the standard python library, so it is necessary to install them runnung the following command: +```bash +pip install https://github.com/FelixLuciano/pypers/archive/refs/tags/1.0.0.tar.gz +``` + +You can also [download the latest version](https://github.com/FelixLuciano/pypers/archive/main.tar.gz) (but not recommended) of the package and install it locally. Or directly: + +```bash +pip install https://github.com/FelixLuciano/pypers/archive/main.tar.gz +``` + +## Create a new page ```bash -pip install -r requirements.txt +python -m pypers create [path/to/page] ``` diff --git a/public/image/example.jpg b/assets/image/example.jpg similarity index 100% rename from public/image/example.jpg rename to assets/image/example.jpg diff --git a/pages/examples/newsletter.ipynb b/pages/examples/newsletter.ipynb deleted file mode 100644 index 5f7fd23..0000000 --- a/pages/examples/newsletter.ipynb +++ /dev/null @@ -1 +0,0 @@ -{"cells":[{"cell_type":"markdown","metadata":{},"source":["# **News 📰**"]},{"cell_type":"markdown","metadata":{},"source":["## **🔧 Config**"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from src 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":null,"metadata":{},"outputs":[],"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":null,"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":null,"metadata":{},"outputs":[],"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":null,"metadata":{},"outputs":[],"source":["page.preview()\n"]},{"cell_type":"markdown","metadata":{},"source":["## **📄 Template**"]},{"cell_type":"code","execution_count":null,"metadata":{"vscode":{"languageId":"html"}},"outputs":[],"source":["%%script html\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","\"example\n","

Image: Unsplash

\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":[""]},"metadata":{},"output_type":"display_data"}],"source":["page.preview()\n"]},{"cell_type":"markdown","metadata":{},"source":["## **📄 Template**"]},{"cell_type":"code","execution_count":6,"metadata":{"vscode":{"languageId":"html"}},"outputs":[{"name":"stdout","output_type":"stream","text":["Couldn't find program: 'html'\n"]}],"source":["%%script html\n",""]},{"cell_type":"markdown","metadata":{},"source":["## **🖌️ Style**"]},{"cell_type":"code","execution_count":7,"metadata":{"vscode":{"languageId":"html"}},"outputs":[{"name":"stdout","output_type":"stream","text":["Couldn't find program: 'html'\n"]}],"source":["%%script html\n",""]},{"cell_type":"markdown","metadata":{},"source":["## 📧 Sending"]},{"cell_type":"code","execution_count":8,"metadata":{},"outputs":[{"data":{"application/vnd.jupyter.widget-view+json":{"model_id":"62a0cf38e03044fc89cc492353e974e0","version_major":2,"version_minor":0},"text/plain":["HBox(children=(Output(), VBox(children=(HBox(children=(Dropdown(description='Send as:', layout=Layout(flex='1 …"]},"execution_count":8,"metadata":{},"output_type":"execute_result"}],"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":"06533af5f6072c33c887b7ac5ff0332542541b8cfbdba87749e169dacaf4c9ce"}}},"nbformat":4,"nbformat_minor":2} diff --git a/public/components/hello.j2 b/template/public/components/hello.j2 similarity index 100% rename from public/components/hello.j2 rename to template/public/components/hello.j2 diff --git a/public/data/mailing-list.csv b/template/public/data/mailing-list.csv similarity index 100% rename from public/data/mailing-list.csv rename to template/public/data/mailing-list.csv diff --git a/public/python/sheets.py b/template/public/python/sheets.py similarity index 96% rename from public/python/sheets.py rename to template/public/python/sheets.py index 9f39f8d..53b9395 100644 --- a/public/python/sheets.py +++ b/template/public/python/sheets.py @@ -2,7 +2,7 @@ import pandas as pd from googleapiclient.discovery import build -from src import google +from pypers import google google.SCOPES.append("https://www.googleapis.com/auth/spreadsheets.readonly") diff --git a/template/public/python/utils.py b/template/public/python/utils.py new file mode 100644 index 0000000..6bb8f6b --- /dev/null +++ b/template/public/python/utils.py @@ -0,0 +1,24 @@ +def get_first_name(full_name): + return full_name.split(" ")[0] + + +def find_row(series, col, value): + for i, row in series.iterrows(): + if row[col] == value: + return i + + return -1 + + +def date_diff(base, diff, value_col, date_col): + filter_ = [] + + for _, row in base.iterrows(): + index = find_row(diff, value_col, row[value_col]) + + if index > 0: + filter_.append(row[date_col] > diff.loc[index][date_col]) + else: + filter_.append(True) + + return base[filter_] diff --git a/public/style/normalize.css b/template/public/style/normalize.css similarity index 100% rename from public/style/normalize.css rename to template/public/style/normalize.css