From a50aabb09132398618b7ae0abfb6adbe4e0c44e7 Mon Sep 17 00:00:00 2001 From: nayanm-fbn <153913924+nayanm-fbn@users.noreply.github.com> Date: Tue, 10 Sep 2024 08:43:39 +0100 Subject: [PATCH 1/3] Dev 14806 update side (#797) * DEV-14806: Capitalise sides for orders-management data * DEV-14806: Capitalise side in compliance data * DEV-14806: Undo extra commas * DEV-14806: Undo number change --- .../use-cases/compliance/data/iag_allocations.csv | 2 +- .../orders-management/data/amazon_allocations.csv | 4 ++-- .../orders-management/data/live_orders.csv | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/use-cases/compliance/data/iag_allocations.csv b/examples/use-cases/compliance/data/iag_allocations.csv index 843e5383..315bb700 100644 --- a/examples/use-cases/compliance/data/iag_allocations.csv +++ b/examples/use-cases/compliance/data/iag_allocations.csv @@ -1,2 +1,2 @@ instrument_name,client_internal,isin,figi,quantity,price,currency,allocation_id,originating_order,state,side,type -IAG group plc,EQ_1244,ES0177542018,BBG00J9WBYB9,500,1.48,USD,ALLOC001-for-ORD005,ORD005,partial allocation,buy,market \ No newline at end of file +IAG group plc,EQ_1244,ES0177542018,BBG00J9WBYB9,500,1.48,USD,ALLOC001-for-ORD005,ORD005,partial allocation,Buy,market \ No newline at end of file diff --git a/examples/use-cases/orders-management/data/amazon_allocations.csv b/examples/use-cases/orders-management/data/amazon_allocations.csv index 09ac856b..a27d20c4 100644 --- a/examples/use-cases/orders-management/data/amazon_allocations.csv +++ b/examples/use-cases/orders-management/data/amazon_allocations.csv @@ -1,2 +1,2 @@ -portfolio,instrument_name,client_internal,isin,figi,quantity,price,currency,allocation_id,originating_order,state,side,type -us_long,Amazon_Nasdaq_AMZN,imd_34634534,US0231351067,BBG000BVPXP1,777,102.7,USD,ALLOC001-for-ORD004,ORD004,partial allocation,buy,market +portfolio,instrument_name,client_internal,isin,figi,quantity,price,currency,allocation_id,originating_order,state,side,type +us_long,Amazon_Nasdaq_AMZN,imd_34634534,US0231351067,BBG000BVPXP1,777,102.7,USD,ALLOC001-for-ORD004,ORD004,partial allocation,Buy,market \ No newline at end of file diff --git a/examples/use-cases/orders-management/data/live_orders.csv b/examples/use-cases/orders-management/data/live_orders.csv index 12fbe597..a0adde33 100644 --- a/examples/use-cases/orders-management/data/live_orders.csv +++ b/examples/use-cases/orders-management/data/live_orders.csv @@ -1,7 +1,7 @@ -portfolio,instrument_name,client_internal,isin,figi,quantity,price,currency,order_id,side,type,state,limit_price,limit_currency,stop_price,stop_currency -us_long,Amazon_Nasdaq_AMZN,imd_34634534,US0231351067,BBG000BVPXP1,1000,100,USD,ORD001,sell,limit,new,100.50,USD -us_long,Amazon_Nasdaq_AMZN,imd_34634534,US0231351067,BBG000BVPXP1,1000,95,USD,ORD002,buy,limit,new,96.25,USD -us_long,Amazon_Nasdaq_AMZN,imd_34634534,US0231351067,BBG000BVPXP1,3000,,USD,ORD003,buy,stop,new,,,94,USD -us_long,Amazon_Nasdaq_AMZN,imd_34634534,US0231351067,BBG000BVPXP1,1000,,USD,ORD004,buy,market,new,, -us_long,Anglian_Water_40LV,imd_13579246,XS0089553282,,500,,USD,ORD005,buy,market,new,, -us_long,ICE_Silver_5000oz_Dec21,imd_12457801,,,5000,,USD,ORD006,buy,market,new,, +portfolio,instrument_name,client_internal,isin,figi,quantity,price,currency,order_id,side,type,state,limit_price,limit_currency,stop_price,stop_currency +us_long,Amazon_Nasdaq_AMZN,imd_34634534,US0231351067,BBG000BVPXP1,1000,100,USD,ORD001,Sell,limit,new,100.50,USD +us_long,Amazon_Nasdaq_AMZN,imd_34634534,US0231351067,BBG000BVPXP1,1000,95,USD,ORD002,Buy,limit,new,96.25,USD +us_long,Amazon_Nasdaq_AMZN,imd_34634534,US0231351067,BBG000BVPXP1,3000,,USD,ORD003,Buy,stop,new,,,94,USD +us_long,Amazon_Nasdaq_AMZN,imd_34634534,US0231351067,BBG000BVPXP1,1000,,USD,ORD004,Buy,market,new,, +us_long,Anglian_Water_40LV,imd_13579246,XS0089553282,,500,,USD,ORD005,Buy,market,new,, +us_long,ICE_Silver_5000oz_Dec21,imd_12457801,,,5000,,USD,ORD006,Buy,market,new,, \ No newline at end of file From 8875cd08b88a19e19c385c84a9b17e8d3f739619 Mon Sep 17 00:00:00 2001 From: Katy Rochford Date: Wed, 11 Sep 2024 13:02:46 +0100 Subject: [PATCH 2/3] FL-1034: Add sample notebook using the v2 notifications sdk to demonstrate how to produce an email notification containing more information than the original event body --- .../HelperFiles/CreateManualEvent.sql | 1 + .../HelperFiles/EmailTemplate.html | 247 +++++++ .../HelperFiles/ExtraTableRows.html | 61 ++ .../HelperFiles/requirements.txt | 65 ++ .../Order Events Email Notifications.ipynb | 623 ++++++++++++++++++ .../AddPropertyToCatalogue.sql | 9 + .../CreateManualEventWriter-View.sql | 68 ++ 7 files changed, 1074 insertions(+) create mode 100644 examples/use-cases/notifications/HelperFiles/CreateManualEvent.sql create mode 100644 examples/use-cases/notifications/HelperFiles/EmailTemplate.html create mode 100644 examples/use-cases/notifications/HelperFiles/ExtraTableRows.html create mode 100644 examples/use-cases/notifications/HelperFiles/requirements.txt create mode 100644 examples/use-cases/notifications/Order Events Email Notifications.ipynb create mode 100644 examples/use-cases/notifications/StandaloneSQLFiles/AddPropertyToCatalogue.sql create mode 100644 examples/use-cases/notifications/StandaloneSQLFiles/CreateManualEventWriter-View.sql diff --git a/examples/use-cases/notifications/HelperFiles/CreateManualEvent.sql b/examples/use-cases/notifications/HelperFiles/CreateManualEvent.sql new file mode 100644 index 00000000..90faa1f1 --- /dev/null +++ b/examples/use-cases/notifications/HelperFiles/CreateManualEvent.sql @@ -0,0 +1 @@ +select * from Views.ManualEvent.Writer where OrderCode == '{{Body.code}}' and OrderScope == '{{Body.scope}}'; \ No newline at end of file diff --git a/examples/use-cases/notifications/HelperFiles/EmailTemplate.html b/examples/use-cases/notifications/HelperFiles/EmailTemplate.html new file mode 100644 index 00000000..45a7cffd --- /dev/null +++ b/examples/use-cases/notifications/HelperFiles/EmailTemplate.html @@ -0,0 +1,247 @@ + + + + + + + New message + + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + + + + {{Extra_Rows}} + + + +
+ + + + +
+ + + + + + + +

{{email_title}}

{{email_description}}

+ + + + + +
+ + + + + + + + + + + + + +

{{subtitle1}}

{{description1}}

+ + + + +
+ + + + + + + + + + + + + +

{{subtitle2}}

{{description2}}

+ + + + +
+ + + + + + + + + + + + + +

{{subtitle3}}

{{description3}}

+ + + + +
+ + + + +
Review now ➟
+ + + + +
+
+ + \ No newline at end of file diff --git a/examples/use-cases/notifications/HelperFiles/ExtraTableRows.html b/examples/use-cases/notifications/HelperFiles/ExtraTableRows.html new file mode 100644 index 00000000..e8190f60 --- /dev/null +++ b/examples/use-cases/notifications/HelperFiles/ExtraTableRows.html @@ -0,0 +1,61 @@ + + + + + + + +
+ + + + + + + + + + + + + +

{{subtitle1}}

{{description1}}

+ + + + +
+ + + + + + + + + + + + + +

{{subtitle2}}

{{description2}}

+ + + + +
+ + + + + + + + + + + + + +

{{subtitle3}}

{{description3}}

+ \ No newline at end of file diff --git a/examples/use-cases/notifications/HelperFiles/requirements.txt b/examples/use-cases/notifications/HelperFiles/requirements.txt new file mode 100644 index 00000000..42dc480c --- /dev/null +++ b/examples/use-cases/notifications/HelperFiles/requirements.txt @@ -0,0 +1,65 @@ +aenum==3.1.15 +aiohttp==3.9.5 +aiosignal==1.3.1 +annotated-types==0.7.0 +asttokens==2.4.1 +async-timeout==4.0.3 +attrs==23.2.0 +certifi==2024.2.2 +charset-normalizer==3.3.2 +coloredlogs==15.0.1 +comm==0.2.2 +debugpy==1.8.1 +decorator==5.1.1 +detect-delimiter==0.1.1 +et-xmlfile==1.1.0 +exceptiongroup==1.2.1 +executing==2.0.1 +finbourne-sdk-utilities==0.0.10 +flatten-json==0.1.14 +frozenlist==1.4.1 +humanfriendly==10.0 +idna==3.7 +ipykernel==6.29.4 +ipython==8.24.0 +jedi==0.19.1 +jupyter_client==8.6.2 +jupyter_core==5.7.2 +lusid-jam==0.1.2 +lusid-notifications-sdk==2.1.35 +lusid-sdk==2.1.142 +lusid-sdk-preview==1.1.162 +lusidtools==1.0.14 +matplotlib-inline==0.1.7 +multidict==6.0.5 +nest-asyncio==1.6.0 +numpy==1.26.4 +openpyxl==3.1.3 +packaging==24.0 +pandas==2.2.2 +parso==0.8.4 +pexpect==4.9.0 +platformdirs==4.2.2 +prompt_toolkit==3.0.45 +psutil==5.9.8 +ptyprocess==0.7.0 +pure-eval==0.2.2 +pydantic==2.7.2 +pydantic_core==2.18.3 +Pygments==2.18.0 +python-dateutil==2.9.0.post0 +pytz==2024.1 +PyYAML==6.0.1 +pyzmq==26.0.3 +requests==2.32.3 +six==1.16.0 +stack-data==0.6.3 +tornado==6.4 +tqdm==4.66.4 +traitlets==5.14.3 +typing_extensions==4.12.0 +tzdata==2024.1 +urllib3==1.26.18 +wcwidth==0.2.13 +xlrd==1.2.0 +yarl==1.9.4 diff --git a/examples/use-cases/notifications/Order Events Email Notifications.ipynb b/examples/use-cases/notifications/Order Events Email Notifications.ipynb new file mode 100644 index 00000000..88e09eaa --- /dev/null +++ b/examples/use-cases/notifications/Order Events Email Notifications.ipynb @@ -0,0 +1,623 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from lusidtools.jupyter_tools import toggle_code\n", + "\n", + "\"\"\"Send email notifications for order events\n", + "\n", + "Attributes\n", + "----------\n", + "subscriptions\n", + "events\n", + "notifications\n", + "\"\"\"\n", + "\n", + "toggle_code(\"Toggle Docstring\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Introduction\n", + "This notebook was written to be used with the python SDK V2, so please ensure you have installed lusid-notifications-sdk and not lusid-notifications-sdk-preview, otherwise the authentication methods used in this notebook will not work.\n", + "\n", + "### Description\n", + "This notebook demonstrates how to setup an **email notification** with an html body which includes **more information** than you initially receive from the **event body** of interest, as follows:\n", + "- Subscribe to the event of interest (i.e. OrderCreated)\n", + "- Create a webhook notification which will trigger a luminesce query\n", + " - The luminesce query will get more information about your event of interest\n", + " - The luminesce query will then trigger a manual event containing all of this data\n", + "- Create a subscription to the manual event\n", + "- Create an email notification triggered by the manual event subscription\n", + "\n", + "### Set-up\n", + "This notebook uses a view, which accesses properties, set these up as follows:\n", + "- Create the properties (you can do this in many ways, see https://support.lusid.com/knowledgebase/article/KA-01856/en-us)\n", + "- Add the properties to the luminesce catalogue by running \"AddPropertyToCatalogue.sql\" in the query editor\n", + "- Create the view by running \"CreateManualEventWriter-View.sql\" from the query editor\n", + "- You will need to edit and re-run these files if you would like to add more properties\n", + "\n", + "You will need to make changes to the \"Variables Setup\" section if you wish to change:\n", + "- The event triggering the email to be sent\n", + "- The data to be included in the email body\n", + "- The number of rows to be included in the html table in the email body\n", + "If you would like to access information that is not already included in the manual event you will also need to edit the \"CreateManualEventWriter-View.sql\" and \"CreateManualEvent.sql\" files to include the necessary columns, and then delete and remake the view. Information on views (including how to delete one) can be found here: https://support.lusid.com/knowledgebase/article/KA-01767/" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This is to allow us to use the same package versions every time we run this notebook\n", + "# The '!' tells the python interpreter to run this line as a shell command\n", + "! pip install -r \"HelperFiles/requirements.txt\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import generic non-LUSID packages\n", + "import os\n", + "import pandas as pd\n", + "import pprint\n", + "import json\n", + "from IPython.core.display import HTML\n", + "\n", + "# Import key modules from the LUSID package\n", + "import lusid_notifications.models as models\n", + "import lusid_notifications\n", + "\n", + "# Import key functions from helper packages\n", + "from lusid_notifications import (\n", + " SyncApiClientFactory,\n", + " EnvironmentVariablesConfigurationLoader,\n", + " SecretsFileConfigurationLoader,\n", + " ArgsConfigurationLoader\n", + ")\n", + "from lusidjam import RefreshingToken\n", + "\n", + "# Set DataFrame display formats\n", + "pd.set_option(\"display.max_columns\", None)\n", + "pd.set_option(\"display.max_rows\", None)\n", + "pd.options.display.float_format = \"${:,.2f}\".format\n", + "display(HTML(\"\"))\n", + "\n", + "# Authenticate user and create API client\n", + "secrets_path = os.getenv('FBN_SECRETS_PATH')\n", + "app_name = os.getenv('FBN_APP_NAME')\n", + "# lusid_domain = \"fbn-xxx\"\n", + "# api_url = f\"https://{lusid_domain}.lusid.com/notification\"\n", + "api_url = os.getenv('FBN_NOTIFICATIONS_API_URL')\n", + "\n", + "config_loaders = [\n", + "\tEnvironmentVariablesConfigurationLoader(),\n", + "\tSecretsFileConfigurationLoader(api_secrets_file=secrets_path),\n", + " ArgsConfigurationLoader(access_token = RefreshingToken(), api_url=api_url, app_name = app_name)\n", + "]\n", + "api_client_factory = SyncApiClientFactory(config_loaders=config_loaders)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Setup apis to be used in this notebook\n", + "subscriptions_api = api_client_factory.build(lusid_notifications.SubscriptionsApi)\n", + "notifications_api = api_client_factory.build(lusid_notifications.NotificationsApi)\n", + "event_types_api = api_client_factory.build(lusid_notifications.EventTypesApi)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Variables Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Set the scope for your subscription and notification\n", + "scope = \"FinbourneUniversity\"\n", + "\n", + "# Set the event code, subscription code and description for the subscription\n", + "event_code = \"Manual\"\n", + "subscription_code = \"ManualEventSubscription\"\n", + "subscription_filter = \"body.Subject eq 'AdditionalInfoEvent'\"\n", + "event_code_webhook = \"OrderCreated\"\n", + "subscription_code_webhook = \"OrderCreatedSubscription\"\n", + "subscription_desc = \"An order has been created in Lusid.\"\n", + "\n", + "# Set the ID, display name, and description for the notifications\n", + "notification_id = \"OrderCreatedNotification\"\n", + "notification_id_webhook = \"OrderCreatedNotification_Webhook\"\n", + "notification_description = \"Order Creation Email Notification\"\n", + "notification_description_webhook = \"Order Creation Webhook Notification\"\n", + "\n", + "# Provide a subject, body, and addressees for your email\n", + "email_subject = \"Order has been created in Lusid.\"\n", + "email_addresses = [\"john.smith@finbourne.com\",\"jane.doe@finbourne.com\"] # Include 1+ emails in this list\n", + "\n", + "# Paths to load the html email templates from\n", + "html_file_path = \"HelperFiles/EmailTemplate.html\"\n", + "html_file_path_2 = \"HelperFiles/ExtraTableRows.html\"\n", + "\n", + "# Provide titles and descriptions to be included in the contents of your email\n", + "# An image of the rendered html tempate for one row of data can be found below\n", + "# You may choose the number of rows you would like to see in your email, but you must make sure\n", + "# that the number of subtitles and descriptions you provide matches the \"num_rows_html_body\" variable\n", + "\n", + "num_rows_html_body = 4\n", + "email_title = \"New Order!\"\n", + "email_description = \"A new order has been created.\"\n", + "\n", + "subtitle1 = \"Submitted By\"\n", + "description1 = \"{{Body.jsonMessage.FirstName}} {{Body.jsonMessage.LastName}}\"\n", + "subtitle2 = \"Timestamp\"\n", + "description2 = \"{{header.timestamp}}\"\n", + "subtitle3 = \"Order ID\"\n", + "description3 = \"{{Body.jsonMessage.OrderCode}}\"\n", + "subtitle1_row_2 = \"Order Scope\"\n", + "description1_row_2 = \"{{Body.jsonMessage.OrderScope}}\"\n", + "subtitle2_row_2 = \"Portfolio Name\"\n", + "description2_row_2 = \"{{Body.jsonMessage.PortfolioCode}}\"\n", + "subtitle3_row_2 = \"Instrument Name\"\n", + "description3_row_2 = \"{{Body.jsonMessage.ClientInternal}}\"\n", + "subtitle1_row_3 = \"Isin\"\n", + "description1_row_3 = \"{{Body.jsonMessage.Isin}}\"\n", + "subtitle2_row_3 = \"Order Quantity\"\n", + "description2_row_3 = \"{{Body.jsonMessage.Quantity}}\"\n", + "subtitle3_row_3 = \"Order Side\"\n", + "description3_row_3 = \"{{Body.jsonMessage.Side}}\"\n", + "subtitle1_row_4 = \"Order Type\"\n", + "description1_row_4 = \"{{Body.jsonMessage.Type}}\"\n", + "subtitle2_row_4 = \"Limit Price\"\n", + "description2_row_4 = \"{{Body.jsonMessage.LimitPrice}}\"\n", + "subtitle3_row_4 = \"Exposure Percentage\"\n", + "description3_row_4 = \"{{Body.jsonMessage.ExposurePercentage}}\"\n", + "\n", + "# A button labelled with \"Review Now\" will be included in the email (see below image)\n", + "# Set a URL that this button will direct the user to\n", + "button_url = f\"https://{lusid_domain}.lusid.com/app/dashboard/order-management/orders\"\n", + "\n", + "# Do not edit this dictionary, this is used to generate extra rows of html\n", + "html_variables_dict_extra_rows = {\n", + " \"subtitle1\": subtitle1,\n", + " \"subtitle2\": subtitle2,\n", + " \"subtitle3\": subtitle3,\n", + " \"description1\": description1,\n", + " \"description2\": description2,\n", + " \"description3\": description3,\n", + "}\n", + "\n", + "# Please add or remove rows to this dictionary depending on how many rows\n", + "# of data you would like to include in your email body\n", + "html_variables_dict = {\n", + " \"email_title\": email_title,\n", + " \"email_description\": email_description,\n", + " \"button_url\": button_url,\n", + " \"subtitle1_row_2\": subtitle1_row_2,\n", + " \"subtitle2_row_2\": subtitle2_row_2,\n", + " \"subtitle3_row_2\": subtitle3_row_2,\n", + " \"description1_row_2\": description1_row_2,\n", + " \"description2_row_2\": description2_row_2,\n", + " \"description3_row_2\": description3_row_2,\n", + " \"subtitle1_row_3\": subtitle1_row_3,\n", + " \"subtitle2_row_3\": subtitle2_row_3,\n", + " \"subtitle3_row_3\": subtitle3_row_3,\n", + " \"description1_row_3\": description1_row_3,\n", + " \"description2_row_3\": description2_row_3,\n", + " \"description3_row_3\": description3_row_3,\n", + " \"subtitle1_row_4\": subtitle1_row_4,\n", + " \"subtitle2_row_4\": subtitle2_row_4,\n", + " \"subtitle3_row_4\": subtitle3_row_4,\n", + " \"description1_row_4\": description1_row_4,\n", + " \"description2_row_4\": description2_row_4,\n", + " \"description3_row_4\": description3_row_4,\n", + "}\n", + "\n", + "html_variables_dict.update(html_variables_dict_extra_rows)" + ] + }, + { + "attachments": { + "image.png": { + "image/png": "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D9sJB77Nz2gop5nl6PK8wFtrtD1ac3/+OWfZXHGlgf033y2VoWZ5jw57WTZu/lVq16ohL7/5jgw3y3//w//KaaecaLdP1+l4E1i3b9fKXhzo0nXQ4Pzvf+0oe0N73WemyMQpn8i/zXuXLP9Bbrn+73bfPPWC+XuYz9n8a/Spl946AgCAQ4sGjK+//rp8/fXX8tVXX8knn3witWrVssGiVnpec801NvDUR9F7gabW1L722mvSpUsXOfnkk21ZIldccYXMnz8/ZtDPSvZkTI3XJk+eLOPHj7cxTE6efvppu846zJ07Vy6//HJ56aWXIsH1xo0b5R//+Iet8JwwYYKdT5/e+cwzz8hbb71ln4y5L84///zItmjN+j//+U/5+OOP5V//+pebI0wvCPQx/Dlth+5fvVjR/e4f9KJjn5grEAAAABwEmjRpEpo9e7abCjMBauiiiy5yU6HQ+vXrQy1btgy99957dnrs2LGhK6+8MmQCXTudyF133RW69dZb3VR2v/zyS6hTp06hH3/80U4vXbo01LBhw9CCBQtC7dq1C5kg15YrE6iGBg8e7KZCobPPPjv0/vvvu6kw3QYTiIfMhYKdNhcJoYsvvji0fft2O+0xQXno888/D5UtWza0evVqW/bcc8+FzAWDHfdbs2ZNqFSpUqGff/7ZTr/66qshc+Fhx/10/czFih1ft25dqHjx4iET6IdMwB5atWqVLVcDBw4M9enTx02FQm3btg298cYbbmr/mAujUIGkowAAAKDwaU2tppQsWbLEDlOnTpV33nlHHnroITeHSJUqVWTQoEEyevRoW1Ou471795ZKlSq5ORLTFJYffvghMpiANGEetZ+mllx99dU2xSM+BcbPBMaRdTaBu63Z1rQZrWXX2nCtqb700kuldOnS7h1hendfU1IqV64sX3zxhSvNO91fekdAB70joOk0WhOvy/Rr3bq1mIsMue6662TLli2uNDtNefG2Q4fly5fHpMrkB0E4AADAQUIbVn7wwQc2iNXh7bfftikq33//fUx+teZea/7yTTfdJLfddpudzo2X/uENw4cPl5UrV7pXE9MUjcsuu0yOP/546dWrV9JAfNq0aZF11kG3QwN8TT/RQFnzscuXj6b6+mkgrtvoNYDUaf+2erwyf1qublPfvn3toHnmAwcOtPtC8839NOX4hhtusBcVmpeeKBDX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" + } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Rendered HTML template:\n", + "\n", + "![image.png](attachment:image.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load html file\n", + "\n", + "This cell will load the html templates from file and populate title and description placeholders in the template with the values that you provided above. This will be used to format the body of the email." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def open_html_file(file_path):\n", + " with open(file_path, 'r') as file:\n", + " return file.read()\n", + "\n", + "html_email_body = open_html_file(html_file_path)\n", + "\n", + "# The below are required because using str.format() or an f string is problematic because there are so many {} used throughout the html which are not variables that we want to replace\n", + "def replace_variable_in_string_1(string,var_name,var_value):\n", + " return string.replace(\"{{\"+var_name+\"}}\",var_value)\n", + "\n", + "def replace_variable_in_string_2(string,var_name,var_value):\n", + " return string.replace(\"{{\"+var_name+\"}}\",\"{{\"+var_value+\"}}\")\n", + "\n", + "# Insert more rows of data into the html template according to \"num_rows_html_body\"\n", + "if num_rows_html_body > 1:\n", + " html_email_supplement = open_html_file(html_file_path_2)\n", + " html_email_supplement_final = \"\"\n", + " for i in range(num_rows_html_body - 1):\n", + " html_email_supplement_n = html_email_supplement\n", + " for name,value in html_variables_dict_extra_rows.items():\n", + " replace_with = name + \"_row_\" + str(i+2)\n", + " html_email_supplement_n = replace_variable_in_string_2(html_email_supplement_n,name,replace_with)\n", + " html_email_supplement_final = html_email_supplement_final + \"\\n\" + html_email_supplement_n\n", + " html_final_email_body = replace_variable_in_string_1(html_email_body,\"Extra_Rows\",html_email_supplement_final)\n", + "\n", + "for name,value in html_variables_dict.items():\n", + " html_final_email_body = replace_variable_in_string_1(html_final_email_body,name,value)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Notifications" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Subscribe to the event of interest\n", + "\n", + "This cell will create a subscription to the event type specified by your 'event_code_webhook' variable.\n", + "For this sample notebook if you do not change any of the variables above this will subscribe to the \"OrderCreated\" event which is automatically created in Lusid whenever an order is created.\n", + "\n", + "This subscription does not have a filter, so if you would like to only subscribe to a subset of the selected event_type (i.e. if you change event_type to \"OrderUpdated\" and only want notifications when the quantity is changed, but not for any other updates to the order, you will need to add a filter into the matching pattern.) Documentation on this can be found here: https://support.lusid.com/knowledgebase/article/KA-01819/" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "create_subscription_request_webhook = models.CreateSubscription(\n", + " id=models.ResourceId(\n", + " scope=scope,\n", + " code=subscription_code_webhook\n", + " ),\n", + " display_name=subscription_code_webhook,\n", + " description=subscription_desc,\n", + " status=\"Active\",\n", + " matching_pattern=models.MatchingPattern(\n", + " event_type=event_code_webhook\n", + " ),\n", + ")\n", + "\n", + "try:\n", + " subscription_api_response = subscriptions_api.create_subscription(create_subscription_request_webhook)\n", + " print(\"The response of SubscriptionsApi->:\\n\")\n", + " print(subscription_api_response)\n", + "except Exception as e:\n", + " print(f\"Exception when calling SubscriptionsApi->:\\n {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create a webhook notification which triggers a luminesce query\n", + "\n", + "This cell will create a webhook notification on top of the subscription created above, which will trigger a luminesce query whenever the specified event type occurs. The luminesce query will run the code contained in \"CreateManualEvent.sql\"\n", + "\n", + "For this sample notebook, if you do not change any of the variables above or edit the sql files, the luminesce query will trigger a manual event in Lusid, which contains more information about the order than the original \"OrderCreated\" event. The notification we created earlier in this notebook will then send an email based on a subscription to the manual event.\n", + "\n", + "The advantage of this method over simply triggering an email directly from the original \"OrderCreated\" event is that it allows us to access more information about the order than is supplied in the \"OrderCreated\" event body, which allows us to include more detailed information in the email.\n", + "\n", + "If you would like to change the information contained in the manual event you will need to edit the sql file, and subsequently edit the \"Variables Setup\" section to then get this information into your email notification." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def create_lumi_webhook_notification(\n", + " notifications_api,\n", + " scope: str,\n", + " code: str,\n", + " sql: str,\n", + " sub_code: str,\n", + "):\n", + "\n", + " notification_request = {\n", + " \"notificationId\": code,\n", + " \"displayName\": code,\n", + " \"description\": notification_description_webhook,\n", + " \"notificationType\": {\n", + " \"content\": sql,\n", + " \"type\": \"Webhook\",\n", + " \"httpMethod\": \"Put\",\n", + " \"url\": \"/honeycomb/api/sql/json\",\n", + " \"authenticationType\": \"Lusid\",\n", + " \"contentType\": \"PlainText\"\n", + " }}\n", + " \n", + " notification_definition = models.CreateNotificationRequest.from_dict(\n", + " notification_request\n", + " )\n", + "\n", + " # Call LUSID API and log response detail if exception occurs.\n", + " try:\n", + " \n", + " api_response = notifications_api.create_notification(\n", + " scope=scope,\n", + " code=sub_code,\n", + " create_notification_request=notification_definition,\n", + " )\n", + " print(\"Notification created successfully.\")\n", + " except ValueError as ve:\n", + " raise (ve)\n", + " except lusid_notifications.ApiException as e:\n", + " try:\n", + " error_body = json.loads(e.body)\n", + " if error_body['name'] == 'NotificationAlreadyExists':\n", + " print('Notification already exists')\n", + " else:\n", + " raise (e)\n", + " except Exception as next_e:\n", + " raise (next_e)\n", + " \n", + "def get_text_for_sql_query():\n", + " f = open(\n", + " \"HelperFiles/CreateManualEvent.sql\", \"r\")\n", + " res = f.read()\n", + " f.close()\n", + " return res\n", + "\n", + "sql = (get_text_for_sql_query()).replace('\"',\"'\").replace(\"\\n\",\" \")\n", + "\n", + "# create luminesce webhook notification\n", + "create_lumi_webhook_notification(\n", + " notifications_api,\n", + " scope=scope,\n", + " code=notification_id_webhook,\n", + " sql=sql,\n", + " sub_code = subscription_code_webhook\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Subscribe to the manual event\n", + "\n", + "This cell will create a subscription to the event type specified by your 'subscription_code' variable.\n", + "\n", + "For this sample notebook if you do not change any of the variables above this will subscribe to the manual event created by the webhook notification above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "create_subscription_request = models.CreateSubscription(\n", + " id=models.ResourceId(\n", + " scope=scope,\n", + " code=subscription_code\n", + " ),\n", + " display_name=subscription_code,\n", + " description=subscription_desc,\n", + " status=\"Active\",\n", + " matching_pattern=models.MatchingPattern(\n", + " event_type=event_code,\n", + " filter=subscription_filter\n", + " ),\n", + ")\n", + "\n", + "try:\n", + " subscription_api_response = subscriptions_api.create_subscription(create_subscription_request)\n", + " print(\"The response of SubscriptionsApi->:\\n\")\n", + " print(subscription_api_response)\n", + "except Exception as e:\n", + " print(f\"Exception when calling SubscriptionsApi->:\\n {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create an email notification\n", + "\n", + "This cell will create an email notification on top of the manual event subscription created above, which will send an email to all specified users when this event occurs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "notification_request = {\n", + " \"notificationId\": notification_id,\n", + " \"displayName\": notification_id,\n", + " \"description\": notification_description,\n", + " \"notificationType\": {\n", + " \"type\": \"Email\",\n", + " \"subject\": email_subject,\n", + " \"plainTextBody\": \"Email Body\",\n", + " \"htmlBody\": html_final_email_body,\n", + " \"emailAddressTo\": email_addresses,\n", + " \"emailAddressCc\": [],\n", + " \"emailAddressBcc\": []\n", + " }}\n", + "\n", + "create_notification_request = models.CreateNotificationRequest.from_dict(notification_request)\n", + "\n", + "# Send the request to lusid to create the email notification\n", + "try:\n", + " notification_api_response = notifications_api.create_notification(scope, subscription_code, create_notification_request)\n", + " print(\"The response of NotificationsApi->:\\n\")\n", + " print(notification_api_response)\n", + "except Exception as e:\n", + " print(f\"Exception when calling NotificationsApi->:\\n {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### (Optional) Delete the notifications & subscriptions\n", + "\n", + "This is useful if you have made any mistakes and need to start again, or if you are running tests.\n", + "\n", + "Delete the email notification." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " delete_notifications_response = notifications_api.delete_notification(scope,subscription_code,notification_id)\n", + "except Exception as e:\n", + " print(f\"Deletion of {notification_id} has failed: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Delete the webhook notification." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " delete_notifications_response = notifications_api.delete_notification(scope,subscription_code_webhook,notification_id_webhook)\n", + "except Exception as e:\n", + " print(f\"Deletion of {notification_id_webhook} has failed: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Delete the \"OrderCreated\" event subscription." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " delete_subscription_response = subscriptions_api.delete_subscription(scope,subscription_code)\n", + "except Exception as e:\n", + " print(f\"Deletion of {subscription_code} has failed: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Delete the \"Manual\" event subscription." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " delete_subscription_response = subscriptions_api.delete_subscription(scope,subscription_code_webhook)\n", + "except Exception as e:\n", + " print(f\"Deletion of {subscription_code_webhook} has failed: {e}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "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.12.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/use-cases/notifications/StandaloneSQLFiles/AddPropertyToCatalogue.sql b/examples/use-cases/notifications/StandaloneSQLFiles/AddPropertyToCatalogue.sql new file mode 100644 index 00000000..67d5709e --- /dev/null +++ b/examples/use-cases/notifications/StandaloneSQLFiles/AddPropertyToCatalogue.sql @@ -0,0 +1,9 @@ +@keysToCatalog = values +('Order/FinbourneUniversity/ExpPct', 'ExpPct', False, 'A property representing the Exposure Percentage of an order'); + +@config = select column1 as [Key], column2 as Name, column3 as IsMain, column4 as Description from @keysToCatalog; + +select * from Sys.Admin.Lusid.Provider.Configure +where Provider = 'Lusid.PortfolioOrder' +and Configuration = @config +; \ No newline at end of file diff --git a/examples/use-cases/notifications/StandaloneSQLFiles/CreateManualEventWriter-View.sql b/examples/use-cases/notifications/StandaloneSQLFiles/CreateManualEventWriter-View.sql new file mode 100644 index 00000000..ae25d17b --- /dev/null +++ b/examples/use-cases/notifications/StandaloneSQLFiles/CreateManualEventWriter-View.sql @@ -0,0 +1,68 @@ +@x = use Sys.Admin.SetupView +--provider=Views.ManualEvent.Writer +--parameters +OrderCode,Text,NotificationTestOrder1,true +OrderScope,Text,FinbourneUniversity,true +---- + +@users = select + EmailAddress, + UserId, + FirstName, + LastName +from Sys.User.Role; + +@raw_event_data = select + PortfolioScope, + PortfolioCode, + OrderScope, + OrderCode, + Quantity, + Side, + Type, + LimitPrice, + UserIdModified, + ClientInternal, + Isin, + ExpPct +from Lusid.PortfolioOrder +where OrderCode == #PARAMETERVALUE(OrderCode) and OrderScope == #PARAMETERVALUE(OrderScope) +; + +@event_data = select + * +from @raw_event_data r_e_d +inner join @users u on u.UserId = r_e_d.UserIdModified +; + +@@json_data = select + json_object( + 'FirstName', FirstName, + 'LastName', LastName, + 'EmailAddress', EmailAddress, + 'PortfolioScope', PortfolioScope, + 'PortfolioCode', PortfolioCode, + 'OrderScope', OrderScope, + 'OrderCode', OrderCode, + 'ClientInternal', ClientInternal, + 'Isin', Isin, + 'Quantity', Quantity, + 'Side', Side, + 'LimitPrice', LimitPrice, + 'Type', Type, + 'ExposurePercentage', ExpPct +) from @event_data; + +@manual_event_data = select #SELECT_AGG{ + { JsonMessage: @@json_data }, + { Message: 'A placeholder event has been created to allow for additional information' }, + { Subject: 'AdditionalInfoEvent' }, + { EventTime: datetime('now') }, + { EventType: 'Manual' }, + { Id: 'ManualEventForNotificationTriggering' }, + { WriteError: 'Creation of the Manual Event has failed'}, +}; + +select * from Notification.ManualEvent.Writer where toWrite = @manual_event_data; + +enduse; \ No newline at end of file From 24b909637d300b25429800b40f3bc8125187ed30 Mon Sep 17 00:00:00 2001 From: Katy Rochford Date: Tue, 17 Sep 2024 16:38:34 +0100 Subject: [PATCH 3/3] FL-1034: Add notebook for conditional email sending capabilities using lumi provider --- .../Conditional Email Notifications.ipynb | 621 ++++++++++++++++++ .../HelperFiles/EmailTemplate.html | 2 +- .../SendConditionalEmailNotification.sql | 1 + .../HelperFiles/requirements.txt | 64 +- .../Order Events Email Notifications.ipynb | 25 +- .../CreateConditionalEmailSend-View.sql | 68 ++ 6 files changed, 705 insertions(+), 76 deletions(-) create mode 100644 examples/use-cases/notifications/Conditional Email Notifications.ipynb create mode 100644 examples/use-cases/notifications/HelperFiles/SendConditionalEmailNotification.sql create mode 100644 examples/use-cases/notifications/StandaloneSQLFiles/CreateConditionalEmailSend-View.sql diff --git a/examples/use-cases/notifications/Conditional Email Notifications.ipynb b/examples/use-cases/notifications/Conditional Email Notifications.ipynb new file mode 100644 index 00000000..25b08ba9 --- /dev/null +++ b/examples/use-cases/notifications/Conditional Email Notifications.ipynb @@ -0,0 +1,621 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This is to allow us to use the same package versions every time we run this notebook\n", + "# The '!' tells the python interpreter to run this line as a shell command\n", + "! pip install -r \"HelperFiles/requirements.txt\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from lusidtools.jupyter_tools import toggle_code\n", + "\n", + "\"\"\"Send email notifications for order events dependent upon what portfolio the changed order belongs to\n", + "\n", + "Attributes\n", + "----------\n", + "subscriptions\n", + "events\n", + "notifications\n", + "\"\"\"\n", + "\n", + "toggle_code(\"Toggle Docstring\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Introduction\n", + "This notebook follows on from the Order Events Email Notifications notebook, but uses the luminesce Email.Send provider instead of an email notification to allow for conditional filtering within the notification itself. This means you could have emails being sent to different users depending upon the content of the subscription event (i.e. different users being emailed based upon which portfolio an OrderCreated event is sent from).\n", + "\n", + "### Description\n", + "- Subscribe to the event of interest (i.e. OrderCreated)\n", + "- Create a webhook notification which will trigger a luminesce query\n", + " - The luminesce query will send an email using the Email.Send provider\n", + " - The luminesce query will send the email to the user based upon a configuration mapping file\n", + " - In this specific example the email is sent to different users based upon which portfolio the order is part of" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import generic non-LUSID packages\n", + "import os\n", + "import pandas as pd\n", + "import pprint\n", + "import json\n", + "from IPython.core.display import HTML\n", + "\n", + "# Import key modules from the LUSID package\n", + "import lusid_notifications.models as models\n", + "import lusid_notifications\n", + "\n", + "# Import key functions from helper packages\n", + "from lusid_notifications import (\n", + " SyncApiClientFactory,\n", + " EnvironmentVariablesConfigurationLoader,\n", + " SecretsFileConfigurationLoader,\n", + " ArgsConfigurationLoader\n", + ")\n", + "from lusidjam import RefreshingToken\n", + "\n", + "# Set DataFrame display formats\n", + "pd.set_option(\"display.max_columns\", None)\n", + "pd.set_option(\"display.max_rows\", None)\n", + "pd.options.display.float_format = \"${:,.2f}\".format\n", + "display(HTML(\"\"))\n", + "\n", + "# Authenticate user and create API client\n", + "secrets_path = os.getenv('FBN_SECRETS_PATH')\n", + "app_name = os.getenv('FBN_APP_NAME')\n", + "# lusid_domain = \"fbn-xxx\"\n", + "# api_url = f\"https://{lusid_domain}.lusid.com/notification\"\n", + "api_url = os.getenv('FBN_NOTIFICATIONS_API_URL')\n", + "\n", + "config_loaders = [\n", + "\tEnvironmentVariablesConfigurationLoader(),\n", + "\tSecretsFileConfigurationLoader(api_secrets_file=secrets_path),\n", + " ArgsConfigurationLoader(access_token = RefreshingToken(), api_url=api_url, app_name = app_name)\n", + "]\n", + "api_client_factory = SyncApiClientFactory(config_loaders=config_loaders)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Setup apis to be used in this notebook\n", + "subscriptions_api = api_client_factory.build(lusid_notifications.SubscriptionsApi)\n", + "notifications_api = api_client_factory.build(lusid_notifications.NotificationsApi)\n", + "event_types_api = api_client_factory.build(lusid_notifications.EventTypesApi)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Variables Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Set the scope for your subscription and notification\n", + "scope = \"FinbourneUniversity\"\n", + "\n", + "# Set the event code, subscription code and description for the subscription\n", + "event_code_manual = \"Manual\"\n", + "subscription_code_manual = \"ManualEventSubscription\"\n", + "subscription_filter = \"body.Subject eq 'AdditionalInfoEvent'\"\n", + "event_code = \"OrderCreated\"\n", + "subscription_code = \"OrderCreatedSubscription\"\n", + "subscription_desc = \"An order has been created in Lusid.\"\n", + "\n", + "# Set the ID, display name, and description for the notifications\n", + "notification_id = \"OrderCreatedNotification\"\n", + "notification_id_email = \"OrderCreatedNotificationEmail\"\n", + "notification_description_email = \"Order Creation Trigger Email Send\"\n", + "notification_description = \"Order Creation Trigger Lumi Script\"\n", + "\n", + "# Provide a subject, body, and addressees for your email\n", + "email_subject = \"Order has been created in Lusid.\"\n", + "email_addresses = [\"john.smith@finbourne.com\",\"jane.doe@finbourne.com\"] # Include 1+ emails in this list\n", + "\n", + "# Paths to load the html email templates from\n", + "html_file_path = \"HelperFiles/EmailTemplate.html\"\n", + "html_file_path_2 = \"HelperFiles/ExtraTableRows.html\"\n", + "\n", + "# Provide titles and descriptions to be included in the contents of your email\n", + "# An image of the rendered html tempate for one row of data can be found below\n", + "# You may choose the number of rows you would like to see in your email, but you must make sure\n", + "# that the number of subtitles and descriptions you provide matches the \"num_rows_html_body\" variable\n", + "\n", + "num_rows_html_body = 4\n", + "email_title = \"New Order!\"\n", + "email_description = \"A new order has been created.\"\n", + "\n", + "subtitle1 = \"Submitted By\"\n", + "description1 = \"{{Body.jsonMessage.FirstName}} {{Body.jsonMessage.LastName}}\"\n", + "subtitle2 = \"Timestamp\"\n", + "description2 = \"{{header.timestamp}}\"\n", + "subtitle3 = \"Order ID\"\n", + "description3 = \"{{Body.jsonMessage.OrderCode}}\"\n", + "subtitle1_row_2 = \"Order Scope\"\n", + "description1_row_2 = \"{{Body.jsonMessage.OrderScope}}\"\n", + "subtitle2_row_2 = \"Portfolio Name\"\n", + "description2_row_2 = \"{{Body.jsonMessage.PortfolioCode}}\"\n", + "subtitle3_row_2 = \"Instrument Name\"\n", + "description3_row_2 = \"{{Body.jsonMessage.ClientInternal}}\"\n", + "subtitle1_row_3 = \"Isin\"\n", + "description1_row_3 = \"{{Body.jsonMessage.Isin}}\"\n", + "subtitle2_row_3 = \"Order Quantity\"\n", + "description2_row_3 = \"{{Body.jsonMessage.Quantity}}\"\n", + "subtitle3_row_3 = \"Order Side\"\n", + "description3_row_3 = \"{{Body.jsonMessage.Side}}\"\n", + "subtitle1_row_4 = \"Order Type\"\n", + "description1_row_4 = \"{{Body.jsonMessage.Type}}\"\n", + "subtitle2_row_4 = \"Limit Price\"\n", + "description2_row_4 = \"{{Body.jsonMessage.LimitPrice}}\"\n", + "subtitle3_row_4 = \"Exposure Percentage\"\n", + "description3_row_4 = \"{{Body.jsonMessage.ExposurePercentage}}\"\n", + "\n", + "# A button labelled with \"Review Now\" will be included in the email (see below image)\n", + "# Set a URL that this button will direct the user to\n", + "lusid_domain = \"my-domain\"\n", + "button_url = f\"https://{lusid_domain}.lusid.com/app/dashboard/order-management/orders\"\n", + "\n", + "# Do not edit this dictionary, this is used to generate extra rows of html\n", + "html_variables_dict_extra_rows = {\n", + " \"subtitle1\": subtitle1,\n", + " \"subtitle2\": subtitle2,\n", + " \"subtitle3\": subtitle3,\n", + " \"description1\": description1,\n", + " \"description2\": description2,\n", + " \"description3\": description3,\n", + "}\n", + "\n", + "# Please add or remove rows to this dictionary depending on how many rows\n", + "# of data you would like to include in your email body\n", + "html_variables_dict = {\n", + " \"email_title\": email_title,\n", + " \"email_description\": email_description,\n", + " \"button_url\": button_url,\n", + " \"subtitle1_row_2\": subtitle1_row_2,\n", + " \"subtitle2_row_2\": subtitle2_row_2,\n", + " \"subtitle3_row_2\": subtitle3_row_2,\n", + " \"description1_row_2\": description1_row_2,\n", + " \"description2_row_2\": description2_row_2,\n", + " \"description3_row_2\": description3_row_2,\n", + " \"subtitle1_row_3\": subtitle1_row_3,\n", + " \"subtitle2_row_3\": subtitle2_row_3,\n", + " \"subtitle3_row_3\": subtitle3_row_3,\n", + " \"description1_row_3\": description1_row_3,\n", + " \"description2_row_3\": description2_row_3,\n", + " \"description3_row_3\": description3_row_3,\n", + " \"subtitle1_row_4\": subtitle1_row_4,\n", + " \"subtitle2_row_4\": subtitle2_row_4,\n", + " \"subtitle3_row_4\": subtitle3_row_4,\n", + " \"description1_row_4\": description1_row_4,\n", + " \"description2_row_4\": description2_row_4,\n", + " \"description3_row_4\": description3_row_4,\n", + "}\n", + "\n", + "html_variables_dict.update(html_variables_dict_extra_rows)" + ] + }, + { + "attachments": { + "image.png": { + "image/png": "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D9sJB77Nz2gop5nl6PK8wFtrtD1ac3/+OWfZXHGlgf033y2VoWZ5jw57WTZu/lVq16ohL7/5jgw3y3//w//KaaecaLdP1+l4E1i3b9fKXhzo0nXQ4Pzvf+0oe0N73WemyMQpn8i/zXuXLP9Bbrn+73bfPPWC+XuYz9n8a/Spl946AgCAQ4sGjK+//rp8/fXX8tVXX8knn3witWrVssGiVnpec801NvDUR9F7gabW1L722mvSpUsXOfnkk21ZIldccYXMnz8/ZtDPSvZkTI3XJk+eLOPHj7cxTE6efvppu846zJ07Vy6//HJ56aWXIsH1xo0b5R//+Iet8JwwYYKdT5/e+cwzz8hbb71ln4y5L84///zItmjN+j//+U/5+OOP5V//+pebI0wvCPQx/Dlth+5fvVjR/e4f9KJjn5grEAAAABwEmjRpEpo9e7abCjMBauiiiy5yU6HQ+vXrQy1btgy99957dnrs2LGhK6+8MmQCXTudyF133RW69dZb3VR2v/zyS6hTp06hH3/80U4vXbo01LBhw9CCBQtC7dq1C5kg15YrE6iGBg8e7KZCobPPPjv0/vvvu6kw3QYTiIfMhYKdNhcJoYsvvji0fft2O+0xQXno888/D5UtWza0evVqW/bcc8+FzAWDHfdbs2ZNqFSpUqGff/7ZTr/66qshc+Fhx/10/czFih1ft25dqHjx4iET6IdMwB5atWqVLVcDBw4M9enTx02FQm3btg298cYbbmr/mAujUIGkowAAAKDwaU2tppQsWbLEDlOnTpV33nlHHnroITeHSJUqVWTQoEEyevRoW1Ou471795ZKlSq5ORLTFJYffvghMpiANGEetZ+mllx99dU2xSM+BcbPBMaRdTaBu63Z1rQZrWXX2nCtqb700kuldOnS7h1hendfU1IqV64sX3zxhSvNO91fekdAB70joOk0WhOvy/Rr3bq1mIsMue6662TLli2uNDtNefG2Q4fly5fHpMrkB0E4AADAQUIbVn7wwQc2iNXh7bfftikq33//fUx+teZea/7yTTfdJLfddpudzo2X/uENw4cPl5UrV7pXE9MUjcsuu0yOP/546dWrV9JAfNq0aZF11kG3QwN8TT/RQFnzscuXj6b6+mkgrtvoNYDUaf+2erwyf1qublPfvn3toHnmAwcOtPtC8839NOX4hhtusBcVmpeeKBDX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" + } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Rendered HTML template:\n", + "\n", + "![image.png](attachment:image.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load html file\n", + "\n", + "This cell will load the html templates from file and populate title and description placeholders in the template with the values that you provided above. This will be used to format the body of the email." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def open_html_file(file_path):\n", + " with open(file_path, 'r') as file:\n", + " return file.read()\n", + "\n", + "html_email_body = open_html_file(html_file_path)\n", + "\n", + "# The below are required because using str.format() or an f string is problematic because there are so many {} used throughout the html which are not variables that we want to replace\n", + "def replace_variable_in_string_1(string,var_name,var_value):\n", + " return string.replace(\"{{\"+var_name+\"}}\",var_value)\n", + "\n", + "def replace_variable_in_string_2(string,var_name,var_value):\n", + " return string.replace(\"{{\"+var_name+\"}}\",\"{{\"+var_value+\"}}\")\n", + "\n", + "# Insert more rows of data into the html template according to \"num_rows_html_body\"\n", + "if num_rows_html_body > 1:\n", + " html_email_supplement = open_html_file(html_file_path_2)\n", + " html_email_supplement_final = \"\"\n", + " for i in range(num_rows_html_body - 1):\n", + " html_email_supplement_n = html_email_supplement\n", + " for name,value in html_variables_dict_extra_rows.items():\n", + " replace_with = name + \"_row_\" + str(i+2)\n", + " html_email_supplement_n = replace_variable_in_string_2(html_email_supplement_n,name,replace_with)\n", + " html_email_supplement_final = html_email_supplement_final + \"\\n\" + html_email_supplement_n\n", + " html_final_email_body = replace_variable_in_string_1(html_email_body,\"Extra_Rows\",html_email_supplement_final)\n", + "\n", + "for name,value in html_variables_dict.items():\n", + " html_final_email_body = replace_variable_in_string_1(html_final_email_body,name,value)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(html_final_email_body)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Notifications" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Subscribe to the event of interest\n", + "\n", + "This cell will create a subscription to the event type specified by your 'event_code_webhook' variable.\n", + "For this sample notebook if you do not change any of the variables above this will subscribe to the \"OrderCreated\" event which is automatically created in Lusid whenever an order is created.\n", + "\n", + "This subscription does not have a filter, so if you would like to only subscribe to a subset of the selected event_type (i.e. if you change event_type to \"OrderUpdated\" and only want notifications when the quantity is changed, but not for any other updates to the order, you will need to add a filter into the matching pattern.) Documentation on this can be found here: https://support.lusid.com/knowledgebase/article/KA-01819/" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "create_subscription_request = models.CreateSubscription(\n", + " id=models.ResourceId(\n", + " scope=scope,\n", + " code=subscription_code\n", + " ),\n", + " display_name=subscription_code,\n", + " description=subscription_desc,\n", + " status=\"Active\",\n", + " matching_pattern=models.MatchingPattern(\n", + " event_type=event_code\n", + " ),\n", + ")\n", + "\n", + "try:\n", + " subscription_api_response = subscriptions_api.create_subscription(create_subscription_request)\n", + " print(\"The response of SubscriptionsApi->:\\n\")\n", + " print(subscription_api_response)\n", + "except Exception as e:\n", + " print(f\"Exception when calling SubscriptionsApi->:\\n {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create a webhook notification which triggers a luminesce query\n", + "\n", + "This cell will create a webhook notification on top of the subscription created above, which will trigger a luminesce query whenever the specified event type occurs. The luminesce query will run the code contained in \"CreateManualEvent.sql\"\n", + "\n", + "For this sample notebook, if you do not change any of the variables above or edit the sql files, the luminesce query will trigger a manual event in Lusid, which contains more information about the order than the original \"OrderCreated\" event. The notification we created earlier in this notebook will then send an email based on a subscription to the manual event.\n", + "\n", + "The advantage of this method over simply triggering an email directly from the original \"OrderCreated\" event is that it allows us to access more information about the order than is supplied in the \"OrderCreated\" event body, which allows us to include more detailed information in the email.\n", + "\n", + "If you would like to change the information contained in the manual event you will need to edit the sql file, and subsequently edit the \"Variables Setup\" section to then get this information into your email notification." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def create_lumi_webhook_notification(\n", + " notifications_api,\n", + " scope: str,\n", + " code: str,\n", + " desc: str,\n", + " sql: str,\n", + " sub_code: str,\n", + "):\n", + "\n", + " notification_request = {\n", + " \"notificationId\": code,\n", + " \"displayName\": code,\n", + " \"description\": desc,\n", + " \"notificationType\": {\n", + " \"content\": sql,\n", + " \"type\": \"Webhook\",\n", + " \"httpMethod\": \"Put\",\n", + " \"url\": \"/honeycomb/api/sql/json\",\n", + " \"authenticationType\": \"Lusid\",\n", + " \"contentType\": \"PlainText\"\n", + " }}\n", + " \n", + " notification_definition = models.CreateNotificationRequest.from_dict(\n", + " notification_request\n", + " )\n", + "\n", + " # Call LUSID API and log response detail if exception occurs.\n", + " try:\n", + " \n", + " api_response = notifications_api.create_notification(\n", + " scope=scope,\n", + " code=sub_code,\n", + " create_notification_request=notification_definition,\n", + " )\n", + " print(\"Notification created successfully.\")\n", + " except ValueError as ve:\n", + " raise (ve)\n", + " except lusid_notifications.ApiException as e:\n", + " try:\n", + " error_body = json.loads(e.body)\n", + " if error_body['name'] == 'NotificationAlreadyExists':\n", + " print('Notification already exists')\n", + " else:\n", + " raise (e)\n", + " except Exception as next_e:\n", + " raise (next_e)\n", + " \n", + "def get_text_for_sql_query(file):\n", + " f = open(\n", + " file, \"r\")\n", + " res = f.read()\n", + " f.close()\n", + " return res\n", + "\n", + "sql = (get_text_for_sql_query(\"helperFiles/CreateManualEvent.sql\")).replace('\"',\"'\").replace(\"\\n\",\" \")\n", + "\n", + "# create luminesce webhook notification\n", + "create_lumi_webhook_notification(\n", + " notifications_api,\n", + " scope=scope,\n", + " code=notification_id,\n", + " desc=notification_description,\n", + " sql=sql,\n", + " sub_code = subscription_code\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Subscribe to the manual event\n", + "\n", + "This cell will create a subscription to the event type specified by your 'subscription_code' variable.\n", + "\n", + "For this sample notebook if you do not change any of the variables above this will subscribe to the manual event created by the webhook notification above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "create_subscription_request = models.CreateSubscription(\n", + " id=models.ResourceId(\n", + " scope=scope,\n", + " code=subscription_code_manual\n", + " ),\n", + " display_name=subscription_code,\n", + " description=subscription_desc,\n", + " status=\"Active\",\n", + " matching_pattern=models.MatchingPattern(\n", + " event_type=event_code_manual,\n", + " filter=subscription_filter\n", + " ),\n", + ")\n", + "\n", + "try:\n", + " subscription_api_response = subscriptions_api.create_subscription(create_subscription_request)\n", + " print(\"The response of SubscriptionsApi->:\\n\")\n", + " print(subscription_api_response)\n", + "except Exception as e:\n", + " print(f\"Exception when calling SubscriptionsApi->:\\n {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create a second webhook notification which is triggered by the manual event\n", + "\n", + "This cell will create a webhook notification on top of the subscription created above, which will trigger a luminesce query whenever the specified event type occurs. The luminesce query will run the code contained in \"CreateManualEvent.sql\"\n", + "\n", + "For this sample notebook, if you do not change any of the variables above or edit the sql files, the luminesce query will trigger a manual event in Lusid, which contains more information about the order than the original \"OrderCreated\" event. The notification we created earlier in this notebook will then send an email based on a subscription to the manual event.\n", + "\n", + "The advantage of this method over simply triggering an email directly from the original \"OrderCreated\" event is that it allows us to access more information about the order than is supplied in the \"OrderCreated\" event body, which allows us to include more detailed information in the email.\n", + "\n", + "If you would like to change the information contained in the manual event you will need to edit the sql file, and subsequently edit the \"Variables Setup\" section to then get this information into your email notification." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "html_final_email_body2 = html_final_email_body.replace('\"','\"\"').replace(\"'\",\"''\")\n", + "# print(html_final_email_body2)\n", + "# html_final_email_body_2 = json.dumps(html_final_email_body)\n", + "# print(html_final_email_body_2)\n", + "\n", + "sql = (get_text_for_sql_query(\"helperFiles/SendConditionalEmailNotification.sql\"))\n", + "# .replace('\"',\"'\")\n", + "sql = sql.replace(\"{{html_body}}\",html_final_email_body2).replace(\"\\n\",\" \")\n", + "# .replace(\"{{email_subject}}\",email_subject)\n", + "print(sql)\n", + "\n", + "# create luminesce webhook notification\n", + "create_lumi_webhook_notification(\n", + " notifications_api,\n", + " scope=scope,\n", + " code=notification_id_email,\n", + " desc=notification_description_email,\n", + " sql=sql,\n", + " sub_code = subscription_code_manual\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### (Optional) Delete the notifications & subscriptions\n", + "\n", + "This is useful if you have made any mistakes and need to start again, or if you are running tests." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Delete the first webhook notification." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " delete_notifications_response = notifications_api.delete_notification(scope,subscription_code,notification_id)\n", + "except Exception as e:\n", + " print(f\"Deletion of {notification_id} has failed: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Delete the second webhook notification" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " delete_notifications_response = notifications_api.delete_notification(scope,subscription_code_manual,notification_id_email)\n", + "except Exception as e:\n", + " print(f\"Deletion of {notification_id_email} has failed: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Delete the \"OrderCreated\" event subscription." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " delete_subscription_response = subscriptions_api.delete_subscription(scope,subscription_code)\n", + "except Exception as e:\n", + " print(f\"Deletion of {subscription_code} has failed: {e}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " delete_subscription_response = subscriptions_api.delete_subscription(scope,subscription_code_manual)\n", + "except Exception as e:\n", + " print(f\"Deletion of {subscription_code_manual} has failed: {e}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/use-cases/notifications/HelperFiles/EmailTemplate.html b/examples/use-cases/notifications/HelperFiles/EmailTemplate.html index 45a7cffd..3481096d 100644 --- a/examples/use-cases/notifications/HelperFiles/EmailTemplate.html +++ b/examples/use-cases/notifications/HelperFiles/EmailTemplate.html @@ -211,7 +211,7 @@
Review now ➟
- Review now ➟ + Review now ➟ diff --git a/examples/use-cases/notifications/HelperFiles/SendConditionalEmailNotification.sql b/examples/use-cases/notifications/HelperFiles/SendConditionalEmailNotification.sql new file mode 100644 index 00000000..17ea7cd8 --- /dev/null +++ b/examples/use-cases/notifications/HelperFiles/SendConditionalEmailNotification.sql @@ -0,0 +1 @@ +select * from Views.ConditionalEmailNotification where OrderCode = '{{Body.jsonMessage.OrderCode}}' and OrderScope = '{{Body.jsonMessage.OrderScope}}' and EmailBody = '"{{html_body}}"'; \ No newline at end of file diff --git a/examples/use-cases/notifications/HelperFiles/requirements.txt b/examples/use-cases/notifications/HelperFiles/requirements.txt index 42dc480c..6c3b0a3b 100644 --- a/examples/use-cases/notifications/HelperFiles/requirements.txt +++ b/examples/use-cases/notifications/HelperFiles/requirements.txt @@ -1,65 +1,3 @@ -aenum==3.1.15 -aiohttp==3.9.5 -aiosignal==1.3.1 -annotated-types==0.7.0 -asttokens==2.4.1 -async-timeout==4.0.3 -attrs==23.2.0 -certifi==2024.2.2 -charset-normalizer==3.3.2 -coloredlogs==15.0.1 -comm==0.2.2 -debugpy==1.8.1 -decorator==5.1.1 -detect-delimiter==0.1.1 -et-xmlfile==1.1.0 -exceptiongroup==1.2.1 -executing==2.0.1 -finbourne-sdk-utilities==0.0.10 -flatten-json==0.1.14 -frozenlist==1.4.1 -humanfriendly==10.0 -idna==3.7 -ipykernel==6.29.4 -ipython==8.24.0 -jedi==0.19.1 -jupyter_client==8.6.2 -jupyter_core==5.7.2 lusid-jam==0.1.2 lusid-notifications-sdk==2.1.35 -lusid-sdk==2.1.142 -lusid-sdk-preview==1.1.162 -lusidtools==1.0.14 -matplotlib-inline==0.1.7 -multidict==6.0.5 -nest-asyncio==1.6.0 -numpy==1.26.4 -openpyxl==3.1.3 -packaging==24.0 -pandas==2.2.2 -parso==0.8.4 -pexpect==4.9.0 -platformdirs==4.2.2 -prompt_toolkit==3.0.45 -psutil==5.9.8 -ptyprocess==0.7.0 -pure-eval==0.2.2 -pydantic==2.7.2 -pydantic_core==2.18.3 -Pygments==2.18.0 -python-dateutil==2.9.0.post0 -pytz==2024.1 -PyYAML==6.0.1 -pyzmq==26.0.3 -requests==2.32.3 -six==1.16.0 -stack-data==0.6.3 -tornado==6.4 -tqdm==4.66.4 -traitlets==5.14.3 -typing_extensions==4.12.0 -tzdata==2024.1 -urllib3==1.26.18 -wcwidth==0.2.13 -xlrd==1.2.0 -yarl==1.9.4 +lusidtools==1.0.14 \ No newline at end of file diff --git a/examples/use-cases/notifications/Order Events Email Notifications.ipynb b/examples/use-cases/notifications/Order Events Email Notifications.ipynb index 88e09eaa..5d19c5bd 100644 --- a/examples/use-cases/notifications/Order Events Email Notifications.ipynb +++ b/examples/use-cases/notifications/Order Events Email Notifications.ipynb @@ -1,5 +1,16 @@ { "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This is to allow us to use the same package versions every time we run this notebook\n", + "# The '!' tells the python interpreter to run this line as a shell command\n", + "! pip install -r \"HelperFiles/requirements.txt\"" + ] + }, { "cell_type": "code", "execution_count": null, @@ -57,17 +68,6 @@ "## Setup" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# This is to allow us to use the same package versions every time we run this notebook\n", - "# The '!' tells the python interpreter to run this line as a shell command\n", - "! pip install -r \"HelperFiles/requirements.txt\"" - ] - }, { "cell_type": "code", "execution_count": null, @@ -201,6 +201,7 @@ "\n", "# A button labelled with \"Review Now\" will be included in the email (see below image)\n", "# Set a URL that this button will direct the user to\n", + "lusid_domain = \"fbn-xxx\"\n", "button_url = f\"https://{lusid_domain}.lusid.com/app/dashboard/order-management/orders\"\n", "\n", "# Do not edit this dictionary, this is used to generate extra rows of html\n", @@ -615,7 +616,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.4" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/examples/use-cases/notifications/StandaloneSQLFiles/CreateConditionalEmailSend-View.sql b/examples/use-cases/notifications/StandaloneSQLFiles/CreateConditionalEmailSend-View.sql new file mode 100644 index 00000000..169539e0 --- /dev/null +++ b/examples/use-cases/notifications/StandaloneSQLFiles/CreateConditionalEmailSend-View.sql @@ -0,0 +1,68 @@ +@x = +use Sys.Admin.SetupView +--provider=Views.ConditionalEmailNotification +--parameters +EmailBody,Text,This is an email,true +OrderCode,Text,NotificationTestOrder1,true +OrderScope,Text,FinbourneUniversity,true +---- + +@@sep = select '--' || '--' as x; + +@mapping_table = use Drive.Csv +--file=/TrainingFiles/MappingTable.csv +enduse; + +@users = select + EmailAddress, + UserId, + FirstName, + LastName +from Sys.User.Role; + +@raw_event_data = select + PortfolioScope, + PortfolioCode, + OrderScope, + OrderCode, + Quantity, + Side, + Type, + LimitPrice, + UserIdModified, + ClientInternal, + Isin, + ExpPct +from Lusid.PortfolioOrder +where OrderCode == #PARAMETERVALUE(OrderCode) and OrderScope == #PARAMETERVALUE(OrderScope) +; + +@event_data = select + * +from @raw_event_data r_e_d +inner join @users u on u.UserId = r_e_d.UserIdModified +; + +@@portfolio = select PortfolioCode from @raw_event_data; +@@email = select Email from @mapping_table where Portfolio == @@portfolio; +@@name = select Name from @mapping_table where Portfolio == @@portfolio; + +@@email_to = select '--addressTo="[' || @@name || '](' || @@email || ')"'; + +@@email_body = select REPLACE(REPLACE(#PARAMETERVALUE(EmailBody),'""','"'),"''","'"); + +@resp = +use Email.Send with @@sep, @@email_to, @@portfolio, @@email_body +{@@email_to} +--subject="This is a subject" +{@@sep} + +{@@email_body} + +enduse; + +select * from @resp where Ok = true; + +enduse; + +select * from @x \ No newline at end of file