From 9f556e0e591e5fe72efd93f85284591c75c187fa Mon Sep 17 00:00:00 2001 From: CyberAstronaut <42648291+CyberAstronaut101@users.noreply.github.com> Date: Sat, 11 Jan 2025 06:42:06 +0000 Subject: [PATCH 01/10] Add example .env file and update .gitignore for SMTP configuration --- smtp_send_email/.env.example | 14 ++++++++++++++ smtp_send_email/.gitignore | 3 +++ 2 files changed, 17 insertions(+) create mode 100644 smtp_send_email/.env.example create mode 100644 smtp_send_email/.gitignore diff --git a/smtp_send_email/.env.example b/smtp_send_email/.env.example new file mode 100644 index 00000000..b5aff5a0 --- /dev/null +++ b/smtp_send_email/.env.example @@ -0,0 +1,14 @@ +# SMTP Environment Variables +SMTP_SERVER = "smtp.mailgun.org" +SMTP_PORT = 587 +SMTP_USERNAME = "postmaster@domain.xyz" # Usually starts with 'postmaster@' +SMTP_PASSWORD = "PASSWD" +SENDER_EMAIL = "restack@mg.domain.xyz" + +# Restack Cloud (Optional) + +# RESTACK_ENGINE_ID= +# RESTACK_ENGINE_API_KEY= +# RESTACK_ENGINE_API_ADDRESS= +# RESTACK_ENGINE_ADDRESS= +# RESTACK_CLOUD_TOKEN= diff --git a/smtp_send_email/.gitignore b/smtp_send_email/.gitignore new file mode 100644 index 00000000..78f7ac68 --- /dev/null +++ b/smtp_send_email/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.env +poetry.lock From da5190639220562ac793d7b9b98cf0c2bcf0b821 Mon Sep 17 00:00:00 2001 From: CyberAstronaut <42648291+CyberAstronaut101@users.noreply.github.com> Date: Sat, 11 Jan 2025 06:44:10 +0000 Subject: [PATCH 02/10] Add pyproject.toml for SMTP email sending workflow example --- smtp_send_email/pyproject.toml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 smtp_send_email/pyproject.toml diff --git a/smtp_send_email/pyproject.toml b/smtp_send_email/pyproject.toml new file mode 100644 index 00000000..f0f052f2 --- /dev/null +++ b/smtp_send_email/pyproject.toml @@ -0,0 +1,25 @@ +[tool.poetry] +name = "smtp_send_email" +version = "0.0.1" +description = "Example workflow and function for sending an email using SMTP" +authors = [ + "CyberAstronaut <42648291+CyberAstronaut101@users.noreply.github.com>", +] +readme = "README.md" +packages = [{include = "src", format = ["sdist"]}] + +[tool.poetry.dependencies] +python = ">=3.10,<4.0" +watchfiles = "^1.0.0" +pydantic = "^2.10.5" +python-dotenv = "1.0.1" +restack-ai = "^0.0.52" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +dev = "src.services:watch_services" +services = "src.services:run_services" +schedule = "schedule_workflow:run_schedule_workflow" From 5139d799f17a555e30b46227d71cb9e6d9a880a6 Mon Sep 17 00:00:00 2001 From: CyberAstronaut <42648291+CyberAstronaut101@users.noreply.github.com> Date: Sat, 11 Jan 2025 06:44:33 +0000 Subject: [PATCH 03/10] Add SendEmailWorkflow for SMTP email sending functionality --- smtp_send_email/src/workflows/send_email.py | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 smtp_send_email/src/workflows/send_email.py diff --git a/smtp_send_email/src/workflows/send_email.py b/smtp_send_email/src/workflows/send_email.py new file mode 100644 index 00000000..94c5679f --- /dev/null +++ b/smtp_send_email/src/workflows/send_email.py @@ -0,0 +1,29 @@ +from restack_ai.workflow import workflow, import_functions, log, RetryPolicy +from datetime import timedelta +from pydantic import BaseModel, Field + +with import_functions(): + from src.functions.smtp_send_email import smtp_send_email, SendEmailInput + +class WorkflowInputParams(BaseModel): + body: str = Field(default="SMTP Email Body Content") + subject: str = Field(default="SMTP Email Subject") + to_email: str = Field(default="SMTP Email Recipient Address") + +@workflow.defn() +class SendEmailWorkflow: + @workflow.run + async def run(self, input: WorkflowInputParams): + + emailState = await workflow.step( + smtp_send_email, + SendEmailInput( + body=input.body, + subject=input.subject, + to_email=input.to_email, + ), + start_to_close_timeout=timedelta(seconds=15), + retry_policy=RetryPolicy(maximum_attempts=1) + ) + + return emailState From 82430c8d9bd7652de8776abc6199cb5f4f7f662d Mon Sep 17 00:00:00 2001 From: CyberAstronaut <42648291+CyberAstronaut101@users.noreply.github.com> Date: Sat, 11 Jan 2025 06:45:23 +0000 Subject: [PATCH 04/10] add rest of smtp example project files --- smtp_send_email/schedule_workflow.py | 45 +++++++++++ smtp_send_email/src/__init__.py | 0 smtp_send_email/src/client.py | 30 ++++++++ smtp_send_email/src/functions/__init__.py | 0 .../src/functions/smtp_send_email.py | 74 +++++++++++++++++++ smtp_send_email/src/services.py | 33 +++++++++ smtp_send_email/src/workflows/__init__.py | 0 7 files changed, 182 insertions(+) create mode 100644 smtp_send_email/schedule_workflow.py create mode 100644 smtp_send_email/src/__init__.py create mode 100644 smtp_send_email/src/client.py create mode 100644 smtp_send_email/src/functions/__init__.py create mode 100644 smtp_send_email/src/functions/smtp_send_email.py create mode 100644 smtp_send_email/src/services.py create mode 100644 smtp_send_email/src/workflows/__init__.py diff --git a/smtp_send_email/schedule_workflow.py b/smtp_send_email/schedule_workflow.py new file mode 100644 index 00000000..ca75151d --- /dev/null +++ b/smtp_send_email/schedule_workflow.py @@ -0,0 +1,45 @@ +import asyncio +import time +from restack_ai import Restack +from dataclasses import dataclass +import os +from dotenv import load_dotenv + +load_dotenv() + +@dataclass +class InputParams: + body: str + subject: str + to_email: str + +async def main(): + client = Restack() + + workflow_id = f"{int(time.time() * 1000)}-SendEmailWorkflow" + to_email = os.getenv("SMTP_TO_EMAIL") + if not to_email: + raise Exception("SMTP_TO_EMAIL environment variable is not set") + + run_id = await client.schedule_workflow( + workflow_name="SendEmailWorkflow", + workflow_id=workflow_id, + input={ + "email_context": "This email should contain a greeting. And telling user we have launched a new AI feature with Restack workflows. Workflows now offer logging and automatic retries when one of its steps fails. Name of user is not provided. You can set as goodbye message on the email just say 'Best regards' or something like that. No need to mention name of user or name of person sending the email.", + "subject": "Hello from Restack", + "to": to_email + } + ) + + await client.get_workflow_result( + workflow_id=workflow_id, + run_id=run_id + ) + + exit(0) + +def run_schedule_workflow(): + asyncio.run(main()) + +if __name__ == "__main__": + run_schedule_workflow() diff --git a/smtp_send_email/src/__init__.py b/smtp_send_email/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/smtp_send_email/src/client.py b/smtp_send_email/src/client.py new file mode 100644 index 00000000..b6db7391 --- /dev/null +++ b/smtp_send_email/src/client.py @@ -0,0 +1,30 @@ +import os +from restack_ai import Restack +from restack_ai.restack import CloudConnectionOptions +from dotenv import load_dotenv + +from src.functions.smtp_send_email import load_smtp_config +# Load environment variables from a .env file +load_dotenv() + +# Call and validate environment variables for critical functions - prevents app from running if we don't have the necessary environment variables +# Possible standard practice for all functions that require environment variables? +# Most examples have long blocks of checking for environment variables, so this could be a good way to consolidate that to a standard function we +# can short circuit and kill the app if we know we will have a failure state. + +## Verify ENV VARS present for SMTP Send Email function +load_smtp_config() + +engine_id = os.getenv("RESTACK_ENGINE_ID") +address = os.getenv("RESTACK_ENGINE_ADDRESS") +api_key = os.getenv("RESTACK_ENGINE_API_KEY") + +connection_options = CloudConnectionOptions( + engine_id=engine_id, + address=address, + api_key=api_key +) +client = Restack(connection_options) + + +# \ No newline at end of file diff --git a/smtp_send_email/src/functions/__init__.py b/smtp_send_email/src/functions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/smtp_send_email/src/functions/smtp_send_email.py b/smtp_send_email/src/functions/smtp_send_email.py new file mode 100644 index 00000000..05fee4e2 --- /dev/null +++ b/smtp_send_email/src/functions/smtp_send_email.py @@ -0,0 +1,74 @@ +import os +from restack_ai.function import function, FunctionFailure, log +from dataclasses import dataclass +from dotenv import load_dotenv + +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart + +import json + +load_dotenv() + +@dataclass +class SendEmailInput: + to_email: str + subject: str + body: str + +@function.defn() +async def smtp_send_email(input: SendEmailInput): + + config = load_smtp_config() + + # Verify input.to_email is a valid email address - quick n dirty + if not "@" in input.to_email: + raise FunctionFailure("SMTPSendEmail: input.to_email not valid email", non_retryable=True) + + # Create message + message = MIMEMultipart() + message["From"] = config.get("SMTP_FROM_EMAIL") + message["To"] = input.to_email + message["Subject"] = input.subject + + # Add body + message.attach(MIMEText(input.body, "plain")) + + try: + # Create SMTP session + with smtplib.SMTP(config.get("SMTP_SERVER"), config.get("SMTP_PORT")) as server: + server.starttls() + server.login(config.get("SMTP_USERNAME"), config.get("SMTP_PASSWORD")) + + # Send email + print(f"Sending email to {input.to_email}") + server.send_message(message) + print("Email sent successfully") + + return f"Email sent successfully to {input.to_email}" + + except Exception as e: + log.error("Failed to send email", error=e) + + errorMessage = json.dumps({"error": f"Failed to send email {e}"}) + raise FunctionFailure(errorMessage, non_retryable=False) + + +def load_smtp_config(): + """Validates that we have all essential environment variables set; raises an exception if not.""" + + required_vars = { + "SMTP_SERVER": os.getenv("SMTP_SERVER"), + "SMTP_PORT": os.getenv("SMTP_PORT"), + "SMTP_USERNAME": os.getenv("SMTP_USERNAME"), + "SMTP_PASSWORD": os.getenv("SMTP_PASSWORD"), + "SMTP_FROM_EMAIL": os.getenv("SMTP_FROM_EMAIL"), + } + + missing = [var for var, value in required_vars.items() if not value] + + if missing: + raise FunctionFailure(f"Missing required environment variables: {missing}", non_retryable=True) + + return required_vars \ No newline at end of file diff --git a/smtp_send_email/src/services.py b/smtp_send_email/src/services.py new file mode 100644 index 00000000..809b1102 --- /dev/null +++ b/smtp_send_email/src/services.py @@ -0,0 +1,33 @@ +import os +import asyncio +from src.client import client +from watchfiles import run_process +# import webbrowser + +## Workflow and function imports +from src.workflows.send_email import SendEmailWorkflow +from src.functions.smtp_send_email import smtp_send_email + +async def main(): + await asyncio.gather( + client.start_service( + workflows=[SendEmailWorkflow], + functions=[smtp_send_email] + ) + ) + +def run_services(): + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("Service interrupted by user. Exiting gracefully.") + +def watch_services(): + watch_path = os.getcwd() + print(f"Watching {watch_path} and its subdirectories for changes...") + # Opens default browser to Dev UI + # webbrowser.open("http://localhost:5233") + run_process(watch_path, recursive=True, target=run_services) + +if __name__ == "__main__": + run_services() diff --git a/smtp_send_email/src/workflows/__init__.py b/smtp_send_email/src/workflows/__init__.py new file mode 100644 index 00000000..e69de29b From 2215df7cfe73679a99cb8fbe761187c13ce4ce8d Mon Sep 17 00:00:00 2001 From: CyberAstronaut <42648291+CyberAstronaut101@users.noreply.github.com> Date: Sat, 11 Jan 2025 07:05:28 +0000 Subject: [PATCH 05/10] add placeholder to CONTRIBUTING --- CONTRIBUTING.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28bad810..69fc7279 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,10 @@ # Contributing -## Development +This is the place holder pending further discussion on a formal contributing process/guidelines. + +## Pull Request Process + +This is the place holder pending further discussion on a formal contributing process/guidelines. ### Prerequisites From dd219c4a99d29902a5ffb0b660bf3e41f96ddae6 Mon Sep 17 00:00:00 2001 From: CyberAstronaut <42648291+CyberAstronaut101@users.noreply.github.com> Date: Tue, 14 Jan 2025 03:14:39 +0000 Subject: [PATCH 06/10] Add README.md for SMTP Send Email example with setup instructions and usage guidelines --- smtp_send_email/README.md | 91 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 smtp_send_email/README.md diff --git a/smtp_send_email/README.md b/smtp_send_email/README.md new file mode 100644 index 00000000..3fc90b8a --- /dev/null +++ b/smtp_send_email/README.md @@ -0,0 +1,91 @@ +# Restack AI - SMTP Send Email Example + + +## Why SMTP in 2025? + + + +### The SMTP Advantage + +*"But why not use [insert latest buzzword solution here]?"* + +Listen, I get it. You're probably thinking "SMTP? In 2025? What is this, a museum?" But hear me out: + +Want to send emails from `workflow1@yourdomain.com`... `workflow100@yourdomain.com`? All you need is: +1. A domain (your digital real estate) +2. Basic DNS setup +3. A working SMTP server + +## Prerequisites + +- Python 3.10 or higher +- Poetry (for dependency management) +- Docker (for running the Restack services) +- SMTP Credentials + +## Usage + +Run Restack local engine with Docker: + +```bash +docker run -d --pull always --name restack -p 5233:5233 -p 6233:6233 -p 7233:7233 ghcr.io/restackio/restack:main +``` + +Open the web UI to see the workflows: http://localhost:5233 + +--- + +Clone this repository: + +```bash +git clone https://github.com/restackio/examples-python +cd examples-python/smtp_send_email/ +``` + +--- + +Reference `.env.example` to create a `.env` file with your SMTP credentials: + +```bash +cp .env.example .env +``` + +``` +SMTP_SERVER = "smtp.mailgun.org" +SMTP_PORT = 587 +SMTP_USERNAME = "postmaster@domain.xyz" # Usually starts with 'postmaster@' +SMTP_PASSWORD = "PASSWD" +SENDER_EMAIL = "restack@mg.domain.xyz" +``` + +Update the `.env` file with the required ENVVARs + +--- + +Install dependencies using Poetry: + + ```bash + poetry env use 3.12 + poetry shell + poetry install + poetry env info # Optional: copy the interpreter path to use in your IDE (e.g. Cursor, VSCode, etc.) + ``` + + +Run the [services](https://docs.restack.io/libraries/python/services): + +```bash +poetry run services +``` + +This will start the Restack service with the defined [workflows](https://docs.restack.io/libraries/python/workflows) and [functions](https://docs.restack.io/libraries/python/functions). + +In the Dev UI, you can use the workflow to manually kick off a test with an example JSON post, and then start inegrating more steps into a workflow that requires sending a SMTP email. + +## Development mode + +If you want to run the services in development mode, you can use the following command to watch for file changes, if you choose to copy this to build your workflow off of: + +```bash +poetry run dev +``` From 98bf83359f68a0a41c47923b0599259a5904ac4b Mon Sep 17 00:00:00 2001 From: CyberAstronaut <42648291+CyberAstronaut101@users.noreply.github.com> Date: Tue, 14 Jan 2025 03:21:36 +0000 Subject: [PATCH 07/10] removed unused schedule for function example --- smtp_send_email/pyproject.toml | 1 - smtp_send_email/schedule_workflow.py | 45 ---------------------------- 2 files changed, 46 deletions(-) delete mode 100644 smtp_send_email/schedule_workflow.py diff --git a/smtp_send_email/pyproject.toml b/smtp_send_email/pyproject.toml index f0f052f2..2c058514 100644 --- a/smtp_send_email/pyproject.toml +++ b/smtp_send_email/pyproject.toml @@ -22,4 +22,3 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] dev = "src.services:watch_services" services = "src.services:run_services" -schedule = "schedule_workflow:run_schedule_workflow" diff --git a/smtp_send_email/schedule_workflow.py b/smtp_send_email/schedule_workflow.py deleted file mode 100644 index ca75151d..00000000 --- a/smtp_send_email/schedule_workflow.py +++ /dev/null @@ -1,45 +0,0 @@ -import asyncio -import time -from restack_ai import Restack -from dataclasses import dataclass -import os -from dotenv import load_dotenv - -load_dotenv() - -@dataclass -class InputParams: - body: str - subject: str - to_email: str - -async def main(): - client = Restack() - - workflow_id = f"{int(time.time() * 1000)}-SendEmailWorkflow" - to_email = os.getenv("SMTP_TO_EMAIL") - if not to_email: - raise Exception("SMTP_TO_EMAIL environment variable is not set") - - run_id = await client.schedule_workflow( - workflow_name="SendEmailWorkflow", - workflow_id=workflow_id, - input={ - "email_context": "This email should contain a greeting. And telling user we have launched a new AI feature with Restack workflows. Workflows now offer logging and automatic retries when one of its steps fails. Name of user is not provided. You can set as goodbye message on the email just say 'Best regards' or something like that. No need to mention name of user or name of person sending the email.", - "subject": "Hello from Restack", - "to": to_email - } - ) - - await client.get_workflow_result( - workflow_id=workflow_id, - run_id=run_id - ) - - exit(0) - -def run_schedule_workflow(): - asyncio.run(main()) - -if __name__ == "__main__": - run_schedule_workflow() From 1f0de65e3725b564c410bfb0959f80367e22be04 Mon Sep 17 00:00:00 2001 From: CyberAstronaut <42648291+CyberAstronaut101@users.noreply.github.com> Date: Tue, 14 Jan 2025 03:26:37 +0000 Subject: [PATCH 08/10] rename for consistency --- email_smtp_sender/.env.example | 14 +++ email_smtp_sender/.gitignore | 3 + email_smtp_sender/README.md | 91 +++++++++++++++++++ email_smtp_sender/pyproject.toml | 24 +++++ email_smtp_sender/src/__init__.py | 0 email_smtp_sender/src/client.py | 30 ++++++ email_smtp_sender/src/functions/__init__.py | 0 .../src/functions/smtp_send_email.py | 74 +++++++++++++++ email_smtp_sender/src/services.py | 33 +++++++ email_smtp_sender/src/workflows/__init__.py | 0 email_smtp_sender/src/workflows/send_email.py | 29 ++++++ 11 files changed, 298 insertions(+) create mode 100644 email_smtp_sender/.env.example create mode 100644 email_smtp_sender/.gitignore create mode 100644 email_smtp_sender/README.md create mode 100644 email_smtp_sender/pyproject.toml create mode 100644 email_smtp_sender/src/__init__.py create mode 100644 email_smtp_sender/src/client.py create mode 100644 email_smtp_sender/src/functions/__init__.py create mode 100644 email_smtp_sender/src/functions/smtp_send_email.py create mode 100644 email_smtp_sender/src/services.py create mode 100644 email_smtp_sender/src/workflows/__init__.py create mode 100644 email_smtp_sender/src/workflows/send_email.py diff --git a/email_smtp_sender/.env.example b/email_smtp_sender/.env.example new file mode 100644 index 00000000..b5aff5a0 --- /dev/null +++ b/email_smtp_sender/.env.example @@ -0,0 +1,14 @@ +# SMTP Environment Variables +SMTP_SERVER = "smtp.mailgun.org" +SMTP_PORT = 587 +SMTP_USERNAME = "postmaster@domain.xyz" # Usually starts with 'postmaster@' +SMTP_PASSWORD = "PASSWD" +SENDER_EMAIL = "restack@mg.domain.xyz" + +# Restack Cloud (Optional) + +# RESTACK_ENGINE_ID= +# RESTACK_ENGINE_API_KEY= +# RESTACK_ENGINE_API_ADDRESS= +# RESTACK_ENGINE_ADDRESS= +# RESTACK_CLOUD_TOKEN= diff --git a/email_smtp_sender/.gitignore b/email_smtp_sender/.gitignore new file mode 100644 index 00000000..78f7ac68 --- /dev/null +++ b/email_smtp_sender/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.env +poetry.lock diff --git a/email_smtp_sender/README.md b/email_smtp_sender/README.md new file mode 100644 index 00000000..3fc90b8a --- /dev/null +++ b/email_smtp_sender/README.md @@ -0,0 +1,91 @@ +# Restack AI - SMTP Send Email Example + + +## Why SMTP in 2025? + + + +### The SMTP Advantage + +*"But why not use [insert latest buzzword solution here]?"* + +Listen, I get it. You're probably thinking "SMTP? In 2025? What is this, a museum?" But hear me out: + +Want to send emails from `workflow1@yourdomain.com`... `workflow100@yourdomain.com`? All you need is: +1. A domain (your digital real estate) +2. Basic DNS setup +3. A working SMTP server + +## Prerequisites + +- Python 3.10 or higher +- Poetry (for dependency management) +- Docker (for running the Restack services) +- SMTP Credentials + +## Usage + +Run Restack local engine with Docker: + +```bash +docker run -d --pull always --name restack -p 5233:5233 -p 6233:6233 -p 7233:7233 ghcr.io/restackio/restack:main +``` + +Open the web UI to see the workflows: http://localhost:5233 + +--- + +Clone this repository: + +```bash +git clone https://github.com/restackio/examples-python +cd examples-python/smtp_send_email/ +``` + +--- + +Reference `.env.example` to create a `.env` file with your SMTP credentials: + +```bash +cp .env.example .env +``` + +``` +SMTP_SERVER = "smtp.mailgun.org" +SMTP_PORT = 587 +SMTP_USERNAME = "postmaster@domain.xyz" # Usually starts with 'postmaster@' +SMTP_PASSWORD = "PASSWD" +SENDER_EMAIL = "restack@mg.domain.xyz" +``` + +Update the `.env` file with the required ENVVARs + +--- + +Install dependencies using Poetry: + + ```bash + poetry env use 3.12 + poetry shell + poetry install + poetry env info # Optional: copy the interpreter path to use in your IDE (e.g. Cursor, VSCode, etc.) + ``` + + +Run the [services](https://docs.restack.io/libraries/python/services): + +```bash +poetry run services +``` + +This will start the Restack service with the defined [workflows](https://docs.restack.io/libraries/python/workflows) and [functions](https://docs.restack.io/libraries/python/functions). + +In the Dev UI, you can use the workflow to manually kick off a test with an example JSON post, and then start inegrating more steps into a workflow that requires sending a SMTP email. + +## Development mode + +If you want to run the services in development mode, you can use the following command to watch for file changes, if you choose to copy this to build your workflow off of: + +```bash +poetry run dev +``` diff --git a/email_smtp_sender/pyproject.toml b/email_smtp_sender/pyproject.toml new file mode 100644 index 00000000..2c058514 --- /dev/null +++ b/email_smtp_sender/pyproject.toml @@ -0,0 +1,24 @@ +[tool.poetry] +name = "smtp_send_email" +version = "0.0.1" +description = "Example workflow and function for sending an email using SMTP" +authors = [ + "CyberAstronaut <42648291+CyberAstronaut101@users.noreply.github.com>", +] +readme = "README.md" +packages = [{include = "src", format = ["sdist"]}] + +[tool.poetry.dependencies] +python = ">=3.10,<4.0" +watchfiles = "^1.0.0" +pydantic = "^2.10.5" +python-dotenv = "1.0.1" +restack-ai = "^0.0.52" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +dev = "src.services:watch_services" +services = "src.services:run_services" diff --git a/email_smtp_sender/src/__init__.py b/email_smtp_sender/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/email_smtp_sender/src/client.py b/email_smtp_sender/src/client.py new file mode 100644 index 00000000..b6db7391 --- /dev/null +++ b/email_smtp_sender/src/client.py @@ -0,0 +1,30 @@ +import os +from restack_ai import Restack +from restack_ai.restack import CloudConnectionOptions +from dotenv import load_dotenv + +from src.functions.smtp_send_email import load_smtp_config +# Load environment variables from a .env file +load_dotenv() + +# Call and validate environment variables for critical functions - prevents app from running if we don't have the necessary environment variables +# Possible standard practice for all functions that require environment variables? +# Most examples have long blocks of checking for environment variables, so this could be a good way to consolidate that to a standard function we +# can short circuit and kill the app if we know we will have a failure state. + +## Verify ENV VARS present for SMTP Send Email function +load_smtp_config() + +engine_id = os.getenv("RESTACK_ENGINE_ID") +address = os.getenv("RESTACK_ENGINE_ADDRESS") +api_key = os.getenv("RESTACK_ENGINE_API_KEY") + +connection_options = CloudConnectionOptions( + engine_id=engine_id, + address=address, + api_key=api_key +) +client = Restack(connection_options) + + +# \ No newline at end of file diff --git a/email_smtp_sender/src/functions/__init__.py b/email_smtp_sender/src/functions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/email_smtp_sender/src/functions/smtp_send_email.py b/email_smtp_sender/src/functions/smtp_send_email.py new file mode 100644 index 00000000..05fee4e2 --- /dev/null +++ b/email_smtp_sender/src/functions/smtp_send_email.py @@ -0,0 +1,74 @@ +import os +from restack_ai.function import function, FunctionFailure, log +from dataclasses import dataclass +from dotenv import load_dotenv + +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart + +import json + +load_dotenv() + +@dataclass +class SendEmailInput: + to_email: str + subject: str + body: str + +@function.defn() +async def smtp_send_email(input: SendEmailInput): + + config = load_smtp_config() + + # Verify input.to_email is a valid email address - quick n dirty + if not "@" in input.to_email: + raise FunctionFailure("SMTPSendEmail: input.to_email not valid email", non_retryable=True) + + # Create message + message = MIMEMultipart() + message["From"] = config.get("SMTP_FROM_EMAIL") + message["To"] = input.to_email + message["Subject"] = input.subject + + # Add body + message.attach(MIMEText(input.body, "plain")) + + try: + # Create SMTP session + with smtplib.SMTP(config.get("SMTP_SERVER"), config.get("SMTP_PORT")) as server: + server.starttls() + server.login(config.get("SMTP_USERNAME"), config.get("SMTP_PASSWORD")) + + # Send email + print(f"Sending email to {input.to_email}") + server.send_message(message) + print("Email sent successfully") + + return f"Email sent successfully to {input.to_email}" + + except Exception as e: + log.error("Failed to send email", error=e) + + errorMessage = json.dumps({"error": f"Failed to send email {e}"}) + raise FunctionFailure(errorMessage, non_retryable=False) + + +def load_smtp_config(): + """Validates that we have all essential environment variables set; raises an exception if not.""" + + required_vars = { + "SMTP_SERVER": os.getenv("SMTP_SERVER"), + "SMTP_PORT": os.getenv("SMTP_PORT"), + "SMTP_USERNAME": os.getenv("SMTP_USERNAME"), + "SMTP_PASSWORD": os.getenv("SMTP_PASSWORD"), + "SMTP_FROM_EMAIL": os.getenv("SMTP_FROM_EMAIL"), + } + + missing = [var for var, value in required_vars.items() if not value] + + if missing: + raise FunctionFailure(f"Missing required environment variables: {missing}", non_retryable=True) + + return required_vars \ No newline at end of file diff --git a/email_smtp_sender/src/services.py b/email_smtp_sender/src/services.py new file mode 100644 index 00000000..809b1102 --- /dev/null +++ b/email_smtp_sender/src/services.py @@ -0,0 +1,33 @@ +import os +import asyncio +from src.client import client +from watchfiles import run_process +# import webbrowser + +## Workflow and function imports +from src.workflows.send_email import SendEmailWorkflow +from src.functions.smtp_send_email import smtp_send_email + +async def main(): + await asyncio.gather( + client.start_service( + workflows=[SendEmailWorkflow], + functions=[smtp_send_email] + ) + ) + +def run_services(): + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("Service interrupted by user. Exiting gracefully.") + +def watch_services(): + watch_path = os.getcwd() + print(f"Watching {watch_path} and its subdirectories for changes...") + # Opens default browser to Dev UI + # webbrowser.open("http://localhost:5233") + run_process(watch_path, recursive=True, target=run_services) + +if __name__ == "__main__": + run_services() diff --git a/email_smtp_sender/src/workflows/__init__.py b/email_smtp_sender/src/workflows/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/email_smtp_sender/src/workflows/send_email.py b/email_smtp_sender/src/workflows/send_email.py new file mode 100644 index 00000000..94c5679f --- /dev/null +++ b/email_smtp_sender/src/workflows/send_email.py @@ -0,0 +1,29 @@ +from restack_ai.workflow import workflow, import_functions, log, RetryPolicy +from datetime import timedelta +from pydantic import BaseModel, Field + +with import_functions(): + from src.functions.smtp_send_email import smtp_send_email, SendEmailInput + +class WorkflowInputParams(BaseModel): + body: str = Field(default="SMTP Email Body Content") + subject: str = Field(default="SMTP Email Subject") + to_email: str = Field(default="SMTP Email Recipient Address") + +@workflow.defn() +class SendEmailWorkflow: + @workflow.run + async def run(self, input: WorkflowInputParams): + + emailState = await workflow.step( + smtp_send_email, + SendEmailInput( + body=input.body, + subject=input.subject, + to_email=input.to_email, + ), + start_to_close_timeout=timedelta(seconds=15), + retry_policy=RetryPolicy(maximum_attempts=1) + ) + + return emailState From d23dea7f9d99f06b7284cb16055172788fe5a0d9 Mon Sep 17 00:00:00 2001 From: CyberAstronaut <42648291+CyberAstronaut101@users.noreply.github.com> Date: Tue, 14 Jan 2025 03:29:38 +0000 Subject: [PATCH 09/10] clean up old dir --- smtp_send_email/.env.example | 14 --- smtp_send_email/.gitignore | 3 - smtp_send_email/README.md | 91 ------------------- smtp_send_email/pyproject.toml | 24 ----- smtp_send_email/src/__init__.py | 0 smtp_send_email/src/client.py | 30 ------ smtp_send_email/src/functions/__init__.py | 0 .../src/functions/smtp_send_email.py | 74 --------------- smtp_send_email/src/services.py | 33 ------- smtp_send_email/src/workflows/__init__.py | 0 smtp_send_email/src/workflows/send_email.py | 29 ------ 11 files changed, 298 deletions(-) delete mode 100644 smtp_send_email/.env.example delete mode 100644 smtp_send_email/.gitignore delete mode 100644 smtp_send_email/README.md delete mode 100644 smtp_send_email/pyproject.toml delete mode 100644 smtp_send_email/src/__init__.py delete mode 100644 smtp_send_email/src/client.py delete mode 100644 smtp_send_email/src/functions/__init__.py delete mode 100644 smtp_send_email/src/functions/smtp_send_email.py delete mode 100644 smtp_send_email/src/services.py delete mode 100644 smtp_send_email/src/workflows/__init__.py delete mode 100644 smtp_send_email/src/workflows/send_email.py diff --git a/smtp_send_email/.env.example b/smtp_send_email/.env.example deleted file mode 100644 index b5aff5a0..00000000 --- a/smtp_send_email/.env.example +++ /dev/null @@ -1,14 +0,0 @@ -# SMTP Environment Variables -SMTP_SERVER = "smtp.mailgun.org" -SMTP_PORT = 587 -SMTP_USERNAME = "postmaster@domain.xyz" # Usually starts with 'postmaster@' -SMTP_PASSWORD = "PASSWD" -SENDER_EMAIL = "restack@mg.domain.xyz" - -# Restack Cloud (Optional) - -# RESTACK_ENGINE_ID= -# RESTACK_ENGINE_API_KEY= -# RESTACK_ENGINE_API_ADDRESS= -# RESTACK_ENGINE_ADDRESS= -# RESTACK_CLOUD_TOKEN= diff --git a/smtp_send_email/.gitignore b/smtp_send_email/.gitignore deleted file mode 100644 index 78f7ac68..00000000 --- a/smtp_send_email/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.DS_Store -.env -poetry.lock diff --git a/smtp_send_email/README.md b/smtp_send_email/README.md deleted file mode 100644 index 3fc90b8a..00000000 --- a/smtp_send_email/README.md +++ /dev/null @@ -1,91 +0,0 @@ -# Restack AI - SMTP Send Email Example - - -## Why SMTP in 2025? - - - -### The SMTP Advantage - -*"But why not use [insert latest buzzword solution here]?"* - -Listen, I get it. You're probably thinking "SMTP? In 2025? What is this, a museum?" But hear me out: - -Want to send emails from `workflow1@yourdomain.com`... `workflow100@yourdomain.com`? All you need is: -1. A domain (your digital real estate) -2. Basic DNS setup -3. A working SMTP server - -## Prerequisites - -- Python 3.10 or higher -- Poetry (for dependency management) -- Docker (for running the Restack services) -- SMTP Credentials - -## Usage - -Run Restack local engine with Docker: - -```bash -docker run -d --pull always --name restack -p 5233:5233 -p 6233:6233 -p 7233:7233 ghcr.io/restackio/restack:main -``` - -Open the web UI to see the workflows: http://localhost:5233 - ---- - -Clone this repository: - -```bash -git clone https://github.com/restackio/examples-python -cd examples-python/smtp_send_email/ -``` - ---- - -Reference `.env.example` to create a `.env` file with your SMTP credentials: - -```bash -cp .env.example .env -``` - -``` -SMTP_SERVER = "smtp.mailgun.org" -SMTP_PORT = 587 -SMTP_USERNAME = "postmaster@domain.xyz" # Usually starts with 'postmaster@' -SMTP_PASSWORD = "PASSWD" -SENDER_EMAIL = "restack@mg.domain.xyz" -``` - -Update the `.env` file with the required ENVVARs - ---- - -Install dependencies using Poetry: - - ```bash - poetry env use 3.12 - poetry shell - poetry install - poetry env info # Optional: copy the interpreter path to use in your IDE (e.g. Cursor, VSCode, etc.) - ``` - - -Run the [services](https://docs.restack.io/libraries/python/services): - -```bash -poetry run services -``` - -This will start the Restack service with the defined [workflows](https://docs.restack.io/libraries/python/workflows) and [functions](https://docs.restack.io/libraries/python/functions). - -In the Dev UI, you can use the workflow to manually kick off a test with an example JSON post, and then start inegrating more steps into a workflow that requires sending a SMTP email. - -## Development mode - -If you want to run the services in development mode, you can use the following command to watch for file changes, if you choose to copy this to build your workflow off of: - -```bash -poetry run dev -``` diff --git a/smtp_send_email/pyproject.toml b/smtp_send_email/pyproject.toml deleted file mode 100644 index 2c058514..00000000 --- a/smtp_send_email/pyproject.toml +++ /dev/null @@ -1,24 +0,0 @@ -[tool.poetry] -name = "smtp_send_email" -version = "0.0.1" -description = "Example workflow and function for sending an email using SMTP" -authors = [ - "CyberAstronaut <42648291+CyberAstronaut101@users.noreply.github.com>", -] -readme = "README.md" -packages = [{include = "src", format = ["sdist"]}] - -[tool.poetry.dependencies] -python = ">=3.10,<4.0" -watchfiles = "^1.0.0" -pydantic = "^2.10.5" -python-dotenv = "1.0.1" -restack-ai = "^0.0.52" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" - -[tool.poetry.scripts] -dev = "src.services:watch_services" -services = "src.services:run_services" diff --git a/smtp_send_email/src/__init__.py b/smtp_send_email/src/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/smtp_send_email/src/client.py b/smtp_send_email/src/client.py deleted file mode 100644 index b6db7391..00000000 --- a/smtp_send_email/src/client.py +++ /dev/null @@ -1,30 +0,0 @@ -import os -from restack_ai import Restack -from restack_ai.restack import CloudConnectionOptions -from dotenv import load_dotenv - -from src.functions.smtp_send_email import load_smtp_config -# Load environment variables from a .env file -load_dotenv() - -# Call and validate environment variables for critical functions - prevents app from running if we don't have the necessary environment variables -# Possible standard practice for all functions that require environment variables? -# Most examples have long blocks of checking for environment variables, so this could be a good way to consolidate that to a standard function we -# can short circuit and kill the app if we know we will have a failure state. - -## Verify ENV VARS present for SMTP Send Email function -load_smtp_config() - -engine_id = os.getenv("RESTACK_ENGINE_ID") -address = os.getenv("RESTACK_ENGINE_ADDRESS") -api_key = os.getenv("RESTACK_ENGINE_API_KEY") - -connection_options = CloudConnectionOptions( - engine_id=engine_id, - address=address, - api_key=api_key -) -client = Restack(connection_options) - - -# \ No newline at end of file diff --git a/smtp_send_email/src/functions/__init__.py b/smtp_send_email/src/functions/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/smtp_send_email/src/functions/smtp_send_email.py b/smtp_send_email/src/functions/smtp_send_email.py deleted file mode 100644 index 05fee4e2..00000000 --- a/smtp_send_email/src/functions/smtp_send_email.py +++ /dev/null @@ -1,74 +0,0 @@ -import os -from restack_ai.function import function, FunctionFailure, log -from dataclasses import dataclass -from dotenv import load_dotenv - -import smtplib -from email.mime.text import MIMEText -from email.mime.multipart import MIMEMultipart - -import json - -load_dotenv() - -@dataclass -class SendEmailInput: - to_email: str - subject: str - body: str - -@function.defn() -async def smtp_send_email(input: SendEmailInput): - - config = load_smtp_config() - - # Verify input.to_email is a valid email address - quick n dirty - if not "@" in input.to_email: - raise FunctionFailure("SMTPSendEmail: input.to_email not valid email", non_retryable=True) - - # Create message - message = MIMEMultipart() - message["From"] = config.get("SMTP_FROM_EMAIL") - message["To"] = input.to_email - message["Subject"] = input.subject - - # Add body - message.attach(MIMEText(input.body, "plain")) - - try: - # Create SMTP session - with smtplib.SMTP(config.get("SMTP_SERVER"), config.get("SMTP_PORT")) as server: - server.starttls() - server.login(config.get("SMTP_USERNAME"), config.get("SMTP_PASSWORD")) - - # Send email - print(f"Sending email to {input.to_email}") - server.send_message(message) - print("Email sent successfully") - - return f"Email sent successfully to {input.to_email}" - - except Exception as e: - log.error("Failed to send email", error=e) - - errorMessage = json.dumps({"error": f"Failed to send email {e}"}) - raise FunctionFailure(errorMessage, non_retryable=False) - - -def load_smtp_config(): - """Validates that we have all essential environment variables set; raises an exception if not.""" - - required_vars = { - "SMTP_SERVER": os.getenv("SMTP_SERVER"), - "SMTP_PORT": os.getenv("SMTP_PORT"), - "SMTP_USERNAME": os.getenv("SMTP_USERNAME"), - "SMTP_PASSWORD": os.getenv("SMTP_PASSWORD"), - "SMTP_FROM_EMAIL": os.getenv("SMTP_FROM_EMAIL"), - } - - missing = [var for var, value in required_vars.items() if not value] - - if missing: - raise FunctionFailure(f"Missing required environment variables: {missing}", non_retryable=True) - - return required_vars \ No newline at end of file diff --git a/smtp_send_email/src/services.py b/smtp_send_email/src/services.py deleted file mode 100644 index 809b1102..00000000 --- a/smtp_send_email/src/services.py +++ /dev/null @@ -1,33 +0,0 @@ -import os -import asyncio -from src.client import client -from watchfiles import run_process -# import webbrowser - -## Workflow and function imports -from src.workflows.send_email import SendEmailWorkflow -from src.functions.smtp_send_email import smtp_send_email - -async def main(): - await asyncio.gather( - client.start_service( - workflows=[SendEmailWorkflow], - functions=[smtp_send_email] - ) - ) - -def run_services(): - try: - asyncio.run(main()) - except KeyboardInterrupt: - print("Service interrupted by user. Exiting gracefully.") - -def watch_services(): - watch_path = os.getcwd() - print(f"Watching {watch_path} and its subdirectories for changes...") - # Opens default browser to Dev UI - # webbrowser.open("http://localhost:5233") - run_process(watch_path, recursive=True, target=run_services) - -if __name__ == "__main__": - run_services() diff --git a/smtp_send_email/src/workflows/__init__.py b/smtp_send_email/src/workflows/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/smtp_send_email/src/workflows/send_email.py b/smtp_send_email/src/workflows/send_email.py deleted file mode 100644 index 94c5679f..00000000 --- a/smtp_send_email/src/workflows/send_email.py +++ /dev/null @@ -1,29 +0,0 @@ -from restack_ai.workflow import workflow, import_functions, log, RetryPolicy -from datetime import timedelta -from pydantic import BaseModel, Field - -with import_functions(): - from src.functions.smtp_send_email import smtp_send_email, SendEmailInput - -class WorkflowInputParams(BaseModel): - body: str = Field(default="SMTP Email Body Content") - subject: str = Field(default="SMTP Email Subject") - to_email: str = Field(default="SMTP Email Recipient Address") - -@workflow.defn() -class SendEmailWorkflow: - @workflow.run - async def run(self, input: WorkflowInputParams): - - emailState = await workflow.step( - smtp_send_email, - SendEmailInput( - body=input.body, - subject=input.subject, - to_email=input.to_email, - ), - start_to_close_timeout=timedelta(seconds=15), - retry_policy=RetryPolicy(maximum_attempts=1) - ) - - return emailState From d6d493a1f16f813ad518f4c707444e0040f990c3 Mon Sep 17 00:00:00 2001 From: CyberAstronaut <42648291+CyberAstronaut101@users.noreply.github.com> Date: Tue, 14 Jan 2025 03:30:05 +0000 Subject: [PATCH 10/10] python examples project directory change, reflect in `README` --- readme.md => README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) rename readme.md => README.md (87%) diff --git a/readme.md b/README.md similarity index 87% rename from readme.md rename to README.md index d14dcb62..4a96f23e 100644 --- a/readme.md +++ b/README.md @@ -9,6 +9,8 @@ This repository contains various examples demonstrating how to use the Restack A ## Getting Started +To run the examples, in general the process looks like the below, but reference the example README.md for example specific instructions. + 1. Clone this repository: ```bash @@ -19,7 +21,7 @@ This repository contains various examples demonstrating how to use the Restack A 2. Navigate to the example you want to explore: ```bash - cd examples-python/examples/ + cd examples-python/ ``` 3. Install dependencies using Poetry: