<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: SteveT</title>
    <description>The latest articles on Forem by SteveT (@tuck1s).</description>
    <link>https://forem.com/tuck1s</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F18723%2F65e30bb9-3c11-467a-9aff-ddc1513e13e3.png</url>
      <title>Forem: SteveT</title>
      <link>https://forem.com/tuck1s</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/tuck1s"/>
    <language>en</language>
    <item>
      <title>Calm your Suppression List Anxieties with Python</title>
      <dc:creator>SteveT</dc:creator>
      <pubDate>Fri, 22 Sep 2017 13:05:36 +0000</pubDate>
      <link>https://forem.com/sparkpost/calm-your-suppression-list-anxieties-with-python-d92</link>
      <guid>https://forem.com/sparkpost/calm-your-suppression-list-anxieties-with-python-d92</guid>
      <description>&lt;h3&gt;
  
  
  “It takes many good deeds to build a good reputation, and only one bad one to lose it.”
&lt;/h3&gt;

&lt;p&gt;– Benjamin Franklin&lt;/p&gt;

&lt;p&gt;From our handy &lt;a href="https://www.sparkpost.com/docs/getting-started/getting-started-sparkpost/#important-coming-from-other-email-services"&gt;Getting Started Guide&lt;/a&gt;, you know how important it is to bring your suppression list from your old provider with you. Ben Franklin was right – your &lt;a href="https://www.sparkpost.com/blog/email-suppression/"&gt;email reputation&lt;/a&gt; will catch a nasty cold if you send to stale, unsubscribed, bounced addresses. This affects whether your messages to your real subscribers are accepted, now and in the future, so it’s best to heed the doctor’s advice.&lt;/p&gt;

&lt;p&gt;In this article, we’ll set up an easy-to-use tool to manage your suppression lists. If you want some more background on the “what and “why of suppressions, &lt;a href="https://www.sparkpost.com/docs/user-guide/using-suppression-lists/"&gt;this article&lt;/a&gt; is a good starting point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scrubbing up
&lt;/h3&gt;

&lt;p&gt;The exported suppression lists we see coming from old providers are often dirty. Duplicate entries, invalid entries with more than one @ sign, invalid characters, telephone numbers instead of email addresses, you name it. We’ve seen files with weird characters in various obscure international alphabets. This might make you consider just amputating those lists, but we’ll explore techniques to preserve as much of them as we can.&lt;/p&gt;

&lt;p&gt;Lists can be large, reaching nearly a million entries. Working with small blocks manually is going to take forever. We are going to need a robot surgeon!&lt;/p&gt;

&lt;h3&gt;
  
  
  Plan for treatment
&lt;/h3&gt;

&lt;p&gt;Let’s set out our needs, and translate them into design goals.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make it easy to get started and simple to use.&lt;/li&gt;
&lt;li&gt;Make best efforts to understand your file format, even if it appears to contain weird characters.&lt;/li&gt;
&lt;li&gt;Check and upload any size of a list without manual work.&lt;/li&gt;
&lt;li&gt;Check the input files up front, with helpful warnings as we go.&lt;/li&gt;
&lt;li&gt;Checks should be thorough and fast. If there are faults, we want to know exactly where they are in the file, and what’s wrong. Specifically, we need to:

&lt;ul&gt;
&lt;li&gt;Ensure email addresses are well-formed (i.e. follow the RFCs)&lt;/li&gt;
&lt;li&gt;Check the other field values, such as transactional / non_transactional flags.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;A “check everything but don’t change anything mode, to make it easy to find and fix faulty input data.&lt;/li&gt;
&lt;li&gt;Allow retrieval of your whole suppression list back from SparkPost, or select time-bounded portions.

&lt;ul&gt;
&lt;li&gt;Have time-zone awareness, while accepting times in your locale. In particular, remember that start and end times could fall on either side of a daylight savings time change.&lt;/li&gt;
&lt;li&gt;Keep it simple. The API supports searching by domain, source, type, description etc – however, that can be done by filtering the retrieved file afterward. If you want these features, &lt;a href="https://github.com/tuck1s/sparkySuppress/issues/new"&gt;raise an issue&lt;/a&gt; on the Git repository and we’ll look at it.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Work across both master account and subaccounts.&lt;/li&gt;
&lt;li&gt;Make it easy to supply defaults for missing/optional file information.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That leads us on to making a tool with the following options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check&lt;/strong&gt; the format of your files (prior to import). Always a good idea to bring your suppressions with you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update&lt;/strong&gt; your suppression list in SparkPost (i.e. create if your list is currently empty).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieve&lt;/strong&gt; your suppression list from SparkPost, for example, if you want to get suppressions back into your upstream campaign management tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete&lt;/strong&gt; your suppression list from SparkPost, i.e. clean your suppression list out. Maybe you uploaded some entries by mistake. We hope that’s a rare use-case, but it’s there for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Time to operate
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/tuck1s/sparkySuppress"&gt;sparkySuppress&lt;/a&gt; is a tool written in Python to help you manage your suppression list. The &lt;a href="https://github.com/tuck1s/sparkySuppress"&gt;Github repo&lt;/a&gt; includes a comprehensive README file. There’s some help with getting Python 3 installed &lt;a href="https://www.sparkpost.com/blog/sparkpost-message-events-api/"&gt;here&lt;/a&gt; if you need it.&lt;/p&gt;

&lt;p&gt;You can configure sparkySuppress with the &lt;code&gt;sparkpost.ini&lt;/code&gt; file, which is used to set up things you change infrequently, such as your API key, timezone, batch sizes and so on. You can leave everything except API key set to default if you like.&lt;/p&gt;

&lt;p&gt;Email addresses from input files are checked as we go, using the excellent &lt;a href="https://github.com/JoshData/python-email-validator"&gt;email_validator&lt;/a&gt; library. We use this to give comprehensive reporting in case of faulty addresses, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Line 2 ! bad@email@address.com The email address is not valid. It must have exactly one @-sign.
Line 3 ! invalid.email@~{}gmail.com The domain name ~{}gmail.com contains invalid characters (Codepoint U+007E not allowed at position 1 in '~{}gmail.com').
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ! marks the entry as having an error. We’ll mark entries that have recoverable problems with a warning w like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Line 1 w need valid transactional &amp;amp; non_transactional flags: {'recipient': 'test.recip@gmail.com'}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  An excellent character
&lt;/h3&gt;

&lt;p&gt;Text files are not as simple as they appear! Unusual file character encoding can be an obstacle, particularly when you don’t have control over how the suppression list export was created in the first place.&lt;/p&gt;

&lt;p&gt;UTF-8 is the most modern and capable encoding, but some systems may not use it. Output files exported from some older versions of Excel will be in Latin-1 for example, rather than UTF-8.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;FileCharacterEncodings&lt;/code&gt; setting in the &lt;code&gt;sparkpost.ini&lt;/code&gt; provides an easy way to control how your input file will be processed. The tool reads your file using each encoding in turn, and if it finds anomalies, will try in the next encoding and so on. So if you have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FileCharacterEncodings=utf-8,utf-16,ascii,latin-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you will see the tool trying each encoding until it finds one that reads the whole file without error. You can select any of the standard encodings shown &lt;a href="https://docs.python.org/3.6/library/codecs.html#standard-encodings"&gt;here&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="err"&gt;./sparkySuppress.py&lt;/span&gt; &lt;span class="err"&gt;check&lt;/span&gt; &lt;span class="err"&gt;klist-1.csv&lt;/span&gt;
&lt;span class="err"&gt;Trying&lt;/span&gt; &lt;span class="err"&gt;file&lt;/span&gt; &lt;span class="err"&gt;klist-1.csv&lt;/span&gt; &lt;span class="err"&gt;with&lt;/span&gt; &lt;span class="err"&gt;encoding:&lt;/span&gt; &lt;span class="err"&gt;utf-8&lt;/span&gt;
        &lt;span class="err"&gt;Near&lt;/span&gt; &lt;span class="err"&gt;line&lt;/span&gt; &lt;span class="err"&gt;1125&lt;/span&gt; &lt;span class="err"&gt;'utf-8'&lt;/span&gt; &lt;span class="err"&gt;codec&lt;/span&gt; &lt;span class="err"&gt;can't&lt;/span&gt; &lt;span class="err"&gt;decode&lt;/span&gt; &lt;span class="err"&gt;byte&lt;/span&gt; &lt;span class="err"&gt;0x9a&lt;/span&gt; &lt;span class="err"&gt;in&lt;/span&gt; &lt;span class="err"&gt;position&lt;/span&gt; &lt;span class="err"&gt;7198:&lt;/span&gt; &lt;span class="err"&gt;invalid&lt;/span&gt; &lt;span class="err"&gt;start&lt;/span&gt; &lt;span class="err"&gt;byte&lt;/span&gt;
&lt;span class="err"&gt;Trying&lt;/span&gt; &lt;span class="err"&gt;file&lt;/span&gt; &lt;span class="err"&gt;klist-1.csv&lt;/span&gt; &lt;span class="err"&gt;with&lt;/span&gt; &lt;span class="err"&gt;encoding:&lt;/span&gt; &lt;span class="err"&gt;utf-16&lt;/span&gt;
        &lt;span class="err"&gt;Near&lt;/span&gt; &lt;span class="err"&gt;line&lt;/span&gt; &lt;span class="err"&gt;1&lt;/span&gt; &lt;span class="err"&gt;UTF-16&lt;/span&gt; &lt;span class="err"&gt;stream&lt;/span&gt; &lt;span class="err"&gt;does&lt;/span&gt; &lt;span class="err"&gt;not&lt;/span&gt; &lt;span class="err"&gt;start&lt;/span&gt; &lt;span class="err"&gt;with&lt;/span&gt; &lt;span class="err"&gt;BOM&lt;/span&gt;
&lt;span class="err"&gt;Trying&lt;/span&gt; &lt;span class="err"&gt;file&lt;/span&gt; &lt;span class="err"&gt;klist-1.csv&lt;/span&gt; &lt;span class="err"&gt;with&lt;/span&gt; &lt;span class="err"&gt;encoding:&lt;/span&gt; &lt;span class="err"&gt;ascii&lt;/span&gt;
        &lt;span class="err"&gt;Near&lt;/span&gt; &lt;span class="err"&gt;line&lt;/span&gt; &lt;span class="err"&gt;1125&lt;/span&gt; &lt;span class="err"&gt;'ascii'&lt;/span&gt; &lt;span class="err"&gt;codec&lt;/span&gt; &lt;span class="err"&gt;can't&lt;/span&gt; &lt;span class="err"&gt;decode&lt;/span&gt; &lt;span class="err"&gt;byte&lt;/span&gt; &lt;span class="err"&gt;0x9a&lt;/span&gt; &lt;span class="err"&gt;in&lt;/span&gt; &lt;span class="err"&gt;position&lt;/span&gt; &lt;span class="err"&gt;7198:&lt;/span&gt; &lt;span class="err"&gt;ordinal&lt;/span&gt; &lt;span class="err"&gt;not&lt;/span&gt; &lt;span class="err"&gt;in&lt;/span&gt; &lt;span class="err"&gt;range(128)&lt;/span&gt;
&lt;span class="err"&gt;Trying&lt;/span&gt; &lt;span class="err"&gt;file&lt;/span&gt; &lt;span class="err"&gt;klist-1.csv&lt;/span&gt; &lt;span class="err"&gt;with&lt;/span&gt; &lt;span class="err"&gt;encoding:&lt;/span&gt; &lt;span class="err"&gt;latin-1&lt;/span&gt;
        &lt;span class="err"&gt;File&lt;/span&gt; &lt;span class="err"&gt;reads&lt;/span&gt; &lt;span class="err"&gt;OK.&lt;/span&gt;

&lt;span class="err"&gt;Lines&lt;/span&gt; &lt;span class="err"&gt;in&lt;/span&gt; &lt;span class="err"&gt;file:&lt;/span&gt; &lt;span class="err"&gt;8496&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your first encoding in the list is used when you’re retrieving entries back from SparkPost into a file.&lt;/p&gt;

&lt;h3&gt;
  
  
  A good performance
&lt;/h3&gt;

&lt;p&gt;Delete is a bit special – it uses multi-threading because deletes have to be done one per call. Update and retrieve work fast when single-threaded, as each call handles a batch. You should experience good performance with the default batch size and thread settings, but you can tweak them if needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practicing your medicine
&lt;/h3&gt;

&lt;p&gt;In case you don’t have data from your old provider yet, here’s a &lt;a href="https://www.sparkpost.com/blog/recipient-suppression-lists-python/"&gt;tool for creating suppression lists&lt;/a&gt; that you can use to create a dummy file to practice on.&lt;/p&gt;

&lt;p&gt;That’s about it! You are now a skilled suppression list surgeon. You’ll soon have your campaigns in excellent shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  And finally…
&lt;/h3&gt;

&lt;p&gt;If you are exploring this tool and want to give the author feedback, you’re welcome to visit our &lt;a href="http://slack.sparkpost.com"&gt;Community Slack channel&lt;/a&gt;– there’s a channel just for Python, #python. Alternatively, open a Github project &lt;a href="https://github.com/tuck1s/sparkySuppress/issues/new"&gt;issue&lt;/a&gt; or &lt;a href="https://github.com/tuck1s/sparkySuppress/compare"&gt;pull-request&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you don’t like Python (whut?) there are some lower-level command-line SparkPost projects that provide a thin “wrapper over the API and can be used to manipulate suppression lists. Check out &lt;a href="https://github.com/SparkPost/node-sparkpost-cli"&gt;Node.js&lt;/a&gt; and &lt;a href="https://github.com/SparkPost/sparkpost-cli"&gt;Go&lt;/a&gt; and if you want to know more about the API and UI for suppression lists, &lt;a href="https://www.sparkpost.com/blog/how-to-view-and-validate-your-suppression-lists/"&gt;here’s a good place to start&lt;/a&gt;. There’s also a &lt;a href="https://www.sparkpost.com/docs/tech-resources/download-suppression-list/"&gt;node.js tool&lt;/a&gt; to retrieve your list back again from SparkPost for checking.&lt;/p&gt;

&lt;p&gt;If you prefer point-and-click, the SparkPost user interface has a built-in &lt;a href="https://app.sparkpost.com/lists/suppressions"&gt;Lists/Suppressions&lt;/a&gt; upload feature. This gives you a nice example template and is ideal when you have perfectly formatted files that aren’t too large, with a maximum of 10,000 recipients per file.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.sparkpost.com/blog/suppression-list-python/"&gt;Calm your Suppression List Anxieties with Python&lt;/a&gt; appeared first on &lt;a href="https://www.sparkpost.com"&gt;SparkPost&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>email</category>
      <category>cli</category>
    </item>
    <item>
      <title>Event Data Made Easy with SparkPost Message Events</title>
      <dc:creator>SteveT</dc:creator>
      <pubDate>Fri, 07 Jul 2017 13:01:45 +0000</pubDate>
      <link>https://forem.com/sparkpost/event-data-made-easy-with-sparkpost-message-events</link>
      <guid>https://forem.com/sparkpost/event-data-made-easy-with-sparkpost-message-events</guid>
      <description>

&lt;p&gt;We love it when developers use SparkPost &lt;a href="https://www.sparkpost.com/blog/webhooks-beyond-the-basics/"&gt;webhooks&lt;/a&gt; to build awesome responsive services. Webhooks are great when you need real-time feedback on what your customers are doing with their messages. They work on a “push model – you create a &lt;a href="https://en.wikipedia.org/wiki/Microservices"&gt;microservice&lt;/a&gt; to handle the event stream.&lt;/p&gt;

&lt;p&gt;Did you know that SparkPost also supports a “pull model Message Events &lt;a href="https://developers.sparkpost.com/api/message-events.html"&gt;API&lt;/a&gt; that enables you to download your event data for up to ten days afterwards? This can be particularly useful in situations such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You’re finding it difficult to create and maintain a production-ready microservice. For example, your corporate IT policy might make it difficult for you to have open ports permanently listening;&lt;/li&gt;
&lt;li&gt;You’re familiar with batch type operations and running periodic workloads, so you don’t need real-time message events;&lt;/li&gt;
&lt;li&gt;You’re a convinced webhooks fan, but you’re investigating issues with your almost-working webhooks receiver microservice, and want a reference copy of those events to compare.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this sounds like your situation, you’re in the right place! Now let’s walk through setting up a really simple tool to get those events.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design goals
&lt;/h3&gt;

&lt;p&gt;Let’s start by setting out the requirements for this project, then translate them into design goals for the tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want it easy to customize without programming.&lt;/li&gt;
&lt;li&gt;SparkPost events are a rich source of data, but some event-types and event properties might not be relevant to you. Being selective gives smaller output file sizes, which is a good thing, right?&lt;/li&gt;
&lt;li&gt;Speaking of output files, you want event data in the commonly-used &lt;code&gt;csv&lt;/code&gt; file format. While programmers love JSON, CSV is easier for non-technical users (and results in smaller files).&lt;/li&gt;
&lt;li&gt;You want to set up your SparkPost account credentials and other basic information once and once only, without having to redo them each time it’s used. Having to remember that stuff is boring.&lt;/li&gt;
&lt;li&gt;You need flexibility on the event date/time ranges of interest.&lt;/li&gt;
&lt;li&gt;You want to set up your local time-zone once, and then work in that zone, not converting values manually to UTC time. Of course, if you really want to work in UTC, because your other server logs are all UTC, then “make it so.”&lt;/li&gt;
&lt;li&gt;Provide some meaningful comfort reporting on your screen. Extracting millions of events could take some time to run. I want to know it’s working.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Events, dear programmer, events …
&lt;/h3&gt;

&lt;p&gt;Firstly, you’ll need Python 3 and &lt;code&gt;git&lt;/code&gt; installed and working on your system. For Linux, a simple procedure can be found in our previous &lt;a href="https://www.sparkpost.com/blog/sending-scheduled-mailings-simply/"&gt;blog post&lt;/a&gt;. It’s really this easy:&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;su -
yum update &lt;span class="nt"&gt;-y&lt;/span&gt;
yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; python35
yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; wget
wget https://bootstrap.pypa.io/get-pip.py
python3 get-pip.py 
yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; git
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;For other platforms, &lt;a href="https://www.python.org/downloads/"&gt;this&lt;/a&gt; is a good starting point to get the latest Python download; there are many good tutorials out there on how to install.&lt;/p&gt;

&lt;p&gt;Then get the &lt;code&gt;sparkyEvents&lt;/code&gt; &lt;a href="https://github.com/tuck1s/sparkyEvents"&gt;code from Github using&lt;/a&gt;:&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git clone https://github.com/tuck1s/sparkyEvents.git
Initialized empty Git repository &lt;span class="k"&gt;in&lt;/span&gt; /home/stuck/sparkyEvents/.git/
remote: Counting objects: 32, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
remote: Compressing objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;22/22&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
remote: Total 32 &lt;span class="o"&gt;(&lt;/span&gt;delta 7&lt;span class="o"&gt;)&lt;/span&gt;, reused 28 &lt;span class="o"&gt;(&lt;/span&gt;delta 5&lt;span class="o"&gt;)&lt;/span&gt;, pack-reused 0
Unpacking objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;32/32&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;sparkyEvents
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  We’re the &lt;a href="https://www.youtube.com/watch?v=zIV4poUZAQo"&gt;knights&lt;/a&gt; who say “.ini”
&lt;/h3&gt;

&lt;p&gt;Set up a &lt;code&gt;sparkpost.ini&lt;/code&gt; file as per the example in the Github README file &lt;a href="https://github.com/tuck1s/sparkyEvents/blob/master/README.md"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Replace &lt;code&gt;&amp;lt;YOUR API KEY&amp;gt;&lt;/code&gt; with &lt;del&gt;a shrubbery&lt;/del&gt; your specific, private API key.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Host&lt;/code&gt; is only needed for SparkPost Enterprise service usage; you can omit for &lt;a href="https://www.sparkpost.com/"&gt;sparkpost.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Events&lt;/code&gt; is a list, as per &lt;a href="https://developers.sparkpost.com/api/message-events.html#message-events-message-events-get"&gt;SparkPost Event Types&lt;/a&gt;; omit the line, or assign it blank, to select all event types.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Properties&lt;/code&gt; can be any of the &lt;a href="https://www.sparkpost.com/docs/tech-resources/webhook-event-reference/"&gt;SparkPost Event Properties&lt;/a&gt;. Definitions can split over lines using indentation, as per &lt;a href="https://docs.python.org/3/library/configparser.html#supported-ini-file-structure"&gt;Python .ini file structure&lt;/a&gt;, which is handy as there are nearly sixty different properties. You can select just those properties you want, rather than everything; this keeps the output file to just the information you want.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Timezone&lt;/code&gt; can be configured to suit your locale. It’s used by SparkPost to interpret the event time range &lt;code&gt;from_time&lt;/code&gt; and &lt;code&gt;to_time&lt;/code&gt; that you give in command-line parameters. If you leave this blank, SparkPost will default to using UTC.&lt;/p&gt;

&lt;p&gt;If you run the tool without any command-line parameters, it prints usage:&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./sparkyEvents.py 

NAME
   ./sparkyEvents.py
   Simple command-line tool to retrieve SparkPost message events into a .CSV file.

SYNOPSIS
  ./sparkyEvents.py outfile.csv from_time to_time

MANDATORY PARAMETERS
    outfile.csv output filename, must be writeable. Records included are specified in the .ini file.
    from_time
    to_time Format YYYY-MM-DDTHH:MM
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;code&gt;from_time&lt;/code&gt; and &lt;code&gt;to_time&lt;/code&gt; are inclusive, so for example if you want a full day of events, use time T00:00 to T23:59.&lt;/p&gt;

&lt;p&gt;Here’s a typical run of the tool, extracting just over 18 million events. This run took a little over two hours to complete.&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./sparkyEvents.py outfile.csv 2017-06-04T00:00 2017-06-04T23:59

SparkPost events from 2017-06-04T00:00 to 2017-06-04T23:59 America/New_York to outfile.csv
Events: &amp;lt;all&amp;gt;
Properties: ['timestamp', 'type', 'event_id', 'friendly_from', 'mailfrom', 'raw_rcpt_to', 'message_id', 'template_id', 'campaign_id', 'subaccount_id', 'subject', 'bounce_class', 'raw_reason', 'rcpt_meta', 'rcpt_tags']
Total events to fetch: 18537125
Page 1: got 10000 events in 5.958 seconds
Page 2: got 10000 events in 5.682 seconds
Page 3: got 10000 events in 5.438 seconds
Page 4: got 10000 events in 6.347 seconds
:
:
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;That’s it! You’re ready to use the tool now. Want to take a peek inside the code? Keep reading!&lt;/p&gt;

&lt;h3&gt;
  
  
  Inside the code
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Getting events via the SparkPost API&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The SparkPost Python library doesn’t yet have built-in support for the &lt;code&gt;message-events&lt;/code&gt; endpoint. In practice the Python &lt;code&gt;requests&lt;/code&gt; library is all we need. It provides inbuilt abstractions for handling JSON data, response status codes etc and is generally a thing of beauty.&lt;/p&gt;

&lt;p&gt;One thing we need to take care of here is that the message-events endpoint is rate-limited. If we make too many requests, SparkPost replies with a &lt;code&gt;429&lt;/code&gt; response code. We play nicely using the following function, which sleeps for a set time, then retries:&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;getMessageEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;'/api/v1/message-events'&lt;/span&gt;
       &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;'Authorization'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'Accept'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

       &lt;span class="n"&gt;moreToDo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
       &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;moreToDo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
           &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

           &lt;span class="c1"&gt;# Handle possible 'too many requests' error inside this module
&lt;/span&gt;           &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
               &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
           &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
               &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="s"&gt;'errors'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s"&gt;'message'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'Too many requests'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                   &lt;span class="n"&gt;snooze&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;
                   &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'.. pausing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snooze&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'seconds for rate-limiting'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                   &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snooze&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                   &lt;span class="k"&gt;continue&lt;/span&gt; &lt;span class="c1"&gt;# try again
&lt;/span&gt;           &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
               &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Error:'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
               &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

   &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ConnectionError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'error code'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Practically, when using event batches of 10000 I didn’t experience any rate-limiting responses even on a fast client. I had to deliberately set smaller batch sizes during testing, so you may not see rate-limiting occur for you in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Selecting the Event Properties&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SparkPost’s events have nearly sixty possible properties. Users may not want all of them, so let’s select those via the sparkpost.ini file. As with other Python projects, the excellent ConfigParser library does most of the work here. It supports a nice multi-line feature:&lt;/p&gt;

&lt;p&gt;“Values can also span multiple lines, as long as they are indented deeper than the first line of the value.”&lt;/p&gt;

&lt;p&gt;We can read the properties (applying a sensible default if it’s absent), remove any newline or carriage-return characters, and convert to a Python list in just three lines:&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# If the fields are not specified, default to a basic few
&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Properties'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'timestamp,type'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Strip newline and CR
&lt;/span&gt;&lt;span class="n"&gt;fList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;','&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Writing to file&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Python &lt;a href="https://docs.python.org/3.6/library/csv.html"&gt;csv library&lt;/a&gt; enables us to create the output file, complete with the required header row field names, based on the &lt;code&gt;fList&lt;/code&gt; we’ve just read:&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;fh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DictWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outfile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fieldnames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;restval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extrasaction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'ignore'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writeheader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Using the &lt;code&gt;DictWriter&lt;/code&gt; class, data is automatically matched to the field names in the output file, and written in the expected order on each line. &lt;code&gt;restval="&lt;/code&gt; ensures we emit blanks for absent data, since not all events have every property. &lt;br&gt;
&lt;code&gt;extrasaction=’ignore’&lt;/code&gt; ensures that we skip extra data we don’t want.&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'results'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
   &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Write out results as CSV rows in the output file
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;That’s pretty much everything of note. The tool is less than 150 lines of actual code.&lt;/p&gt;

&lt;h3&gt;
  
  
  You’re the Master of Events!
&lt;/h3&gt;

&lt;p&gt;So that’s it! You can now download squillions of events from SparkPost, and can customize the output files you’re getting. You’re now the master of events!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was originally published on &lt;a href="https://www.sparkpost.com/blog/sparkpost-message-events-api/"&gt;sparkpost.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;


</description>
      <category>metrics</category>
      <category>api</category>
      <category>messageevents</category>
      <category>python</category>
    </item>
    <item>
      <title>Charm your SparkPost Recipient and Suppression Lists with Python</title>
      <dc:creator>SteveT</dc:creator>
      <pubDate>Thu, 08 Jun 2017 02:14:57 +0000</pubDate>
      <link>https://forem.com/sparkpost/charm-your-sparkpost-recipient-and-suppression-lists-with-python</link>
      <guid>https://forem.com/sparkpost/charm-your-sparkpost-recipient-and-suppression-lists-with-python</guid>
      <description>&lt;p&gt;When developing code that sends email to your customers, it’s smart to try things out on a test list first. Using our &lt;a href="https://www.sparkpost.com/docs/faq/using-sink-server/"&gt;sink-domain service&lt;/a&gt; helps you avoid negative impact to &lt;a href="https://www.sparkpost.com/blog/email-reputation-matters/"&gt;your reputation&lt;/a&gt; during tests. Next you’ll want to check that your code is working at scale on a realistic sized &lt;a href="https://www.sparkpost.com/docs/user-guide/uploading-recipient-list/"&gt;recipient list&lt;/a&gt; and flush out any code performance issues… but how?&lt;/p&gt;

&lt;p&gt;You could use Microsoft Excel to put together .csv recipient-list files for you, but there are &lt;a href="https://www.quora.com/How-many-rows-and-columns-in-one-excel-sheet"&gt;practical limitations&lt;/a&gt;, and it’s slow. You’ll be using fields like “substitution_data that require &lt;a href="https://en.wikipedia.org/wiki/JSON"&gt;JSON encoding&lt;/a&gt;, and Excel doesn’t help you with those. Performance-wise, anything more than a few hundred rows in Excel is going to get cumbersome.&lt;/p&gt;

&lt;p&gt;What we need is a tool that will generate realistic looking test lists; preferably of any size, up to hundreds of thousands of recipients, with safe sink-domain addresses. Excel would be really slow at doing that – we can do much better with some gentle programming.&lt;/p&gt;

&lt;p&gt;The second requirement, perhaps less obvious, is testing your uploads to SparkPost’s built-in &lt;a href="https://www.sparkpost.com/docs/user-guide/using-suppression-lists/"&gt;suppression-list functionality&lt;/a&gt;. It’s good practice to upload the suppressed addresses from your previous provider before mailing – see, for example our &lt;a href="https://www.sparkpost.com/blog/?s=migration"&gt;Migration Guides&lt;/a&gt;. You might need to rehearse a migration without using your real suppression addresses. Perhaps you don’t have easy access to them right now, because your old provider is playing awkward and doesn’t have a nice API. Luckily, with very little extra code, we can also make this tool generate “practice suppression lists.&lt;/p&gt;

&lt;h2&gt;
  
  
  You’re on my list
&lt;/h2&gt;

&lt;p&gt;CSV files have a “header in line 1 of the file, giving the names of each field. Handy hint: you can get an example file, including the header line, directly from SparkPost, using the “Download a Recipient List CSV template button &lt;a href="https://app.sparkpost.com/lists/recipients?_ga=2.38487782.456726847.1496187918-424867781.1485888065"&gt;right here&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--c-ADRJvm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://media.sparkpost.com/uploads/2017/05/Recip-list-1024x524.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--c-ADRJvm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://media.sparkpost.com/uploads/2017/05/Recip-list-1024x524.png" alt="Download a SparkPost Recipient List CSV Template"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The SparkPost recipient-list .csv format looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;email,name,return_path,metadata,substitution_data,tags
recipient@example.com,Example Recipient,reply@example.com,"{""foo"": ""bar""}","{""member"": ""Platinum"", ""region"": ""US""}","[""test"", ""example""]"
recipient2@example.com,Jake,reply@example.com,"{""foo"": ""bar""}","{""member"": ""Platinum"", ""region"": ""US""}","[""test"", ""example""]"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;metadata&lt;/code&gt;, &lt;code&gt;substitution_data&lt;/code&gt;, and &lt;code&gt;tags&lt;/code&gt; fields can carry pretty much anything you want.&lt;/p&gt;

&lt;p&gt;SparkPost's suppression list .csv format is equally fun, and looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;recipient,transactional,non_transactional,description,subaccount_id
anon11779856@demo.sink.sparkpostmail.com,true,true,Example data import,0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Let’s have an argument
&lt;/h2&gt;

&lt;p&gt;Some command-line &lt;a href="https://www.youtube.com/watch?v=Lvcnx6-0GhA"&gt;arguments&lt;/a&gt; would be nice, so we can change the lists we’re generating. Here’s the arguments we’ll accept, which translate nicely into design goals for this project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A flag to say whether we’re generating a recipient list or a suppression list&lt;/li&gt;
&lt;li&gt;How many records we want (make it optional – say 10 as a default)&lt;/li&gt;
&lt;li&gt;A recipient domain to generate records for (optional – default as something safe, such as demo.sink.sparkpostmail.com).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Downloading and using the tool
&lt;/h2&gt;

&lt;p&gt;Firstly, you’ll need &lt;code&gt;python&lt;/code&gt;, &lt;code&gt;pip&lt;/code&gt;, and &lt;code&gt;git&lt;/code&gt; installed. If you don’t already have them, there are some simple instructions to in my &lt;a href="https://www.sparkpost.com/blog/sending-scheduled-mailings-simply/"&gt;previous blogpost&lt;/a&gt;. Then we use &lt;code&gt;git clone&lt;/code&gt; to download the project. The external package &lt;code&gt;names&lt;/code&gt; is needed, we can install that using &lt;code&gt;pip3&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git clone https://github.com/tuck1s/gen-SparkPost-Lists-python.git
Initialized empty Git repository &lt;span class="k"&gt;in&lt;/span&gt; /home/stuck/gen-SparkPost-Lists-python/.git/
remote: Counting objects: 32, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
remote: Total 32 &lt;span class="o"&gt;(&lt;/span&gt;delta 0&lt;span class="o"&gt;)&lt;/span&gt;, reused 0 &lt;span class="o"&gt;(&lt;/span&gt;delta 0&lt;span class="o"&gt;)&lt;/span&gt;, pack-reused 32
Unpacking objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;32/32&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;pip3 &lt;span class="nb"&gt;install &lt;/span&gt;names
Collecting names
Installing collected packages: names
Successfully installed names-0.3.0

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;gen-SparkPost-Lists-python/
&lt;span class="nv"&gt;$ &lt;/span&gt;./gen-sparkpost-lists.py recip 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that final command, you should see the list output to the screen.  If you want to direct it into a file, you just use &lt;code&gt;&amp;gt;&lt;/code&gt;, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;./gen-sparkpost-lists.py recip 10 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; mylist.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s all there is to it!  If you run the tool with no arguments, it gives some guidance on usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./gen-sparkpost-lists.py 

NAME
   ./gen-sparkpost-lists.py
   Generate a random, SparkPost-compatible Recipient- or Suppression-List for .CSV import.

SYNOPSIS
  ./gen-sparkpost-lists.py recip|supp|help [count [domain]]

OPTIONAL PARAMETERS
    count = number of records to generate (default 10)
    domain = recipient domain to generate records for (default demo.sink.sparkpostmail.com)
[stuck@ip-172-31-20-126 gen-SparkPost-Lists-python]$ 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Inside the code – special snowflakes
&lt;/h2&gt;

&lt;p&gt;Here’s the kind of data we want to generate for our test recipient-lists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;email,name,return_path,metadata,substitution_data,tags
anon13061346@demo.sink.sparkpostmail.com,Teddy Lanier,bounce@demo.sink.sparkpostmail.com,"{""custID"": 3156295}","{""memberType"": ""bronze"", ""state"": ""KY""}","[""gwen"", ""bacon"", ""hass"", ""fuerte""]"
anon94133309@demo.sink.sparkpostmail.com,Heriberto Pennell,bounce@demo.sink.sparkpostmail.com,"{""custID"": 78804336}","{""memberType"": ""platinum"", ""state"": ""MT""}","[""bacon""]"
anon14982287@demo.sink.sparkpostmail.com,Terry Smialek,bounce@demo.sink.sparkpostmail.com,"{""custID"": 16745544}","{""memberType"": ""platinum"", ""state"": ""WA""}","[""bacon""]"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The metadata, substitution data and tags are from our example company, Avocado Industries.  Let’s pick a line of that apart, and hide the double-quotes &lt;code&gt;””&lt;/code&gt; so we can see it more clearly:&lt;/p&gt;

&lt;p&gt;Metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"custID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3156295&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Substitution_data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"memberType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bronze"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"KY"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tags (these are &lt;a href="https://en.wikipedia.org/wiki/Avocado#A_cultivars"&gt;types of avocado&lt;/a&gt;, by the way!)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"gwen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="s2"&gt;"bacon"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="s2"&gt;"hass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="s2"&gt;"fuerte"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We want each recipient email address to be unique, so that when imported into SparkPost, the list is exactly the asked-for length. Sounds easy – we can just use a random number generator to produce an ID like the ones shown above. The catch is that &lt;a href="https://en.wikipedia.org/wiki/Simple_random_sample"&gt;random&lt;/a&gt; functions can give the same ID during a run, and on a long run that is quite likely to happen. We need to prevent that, eliminating duplicate addresses as we go.&lt;/p&gt;

&lt;p&gt;Python provides a nice &lt;a href="https://docs.python.org/3.6/library/stdtypes.html?highlight=set#set"&gt;set() datatype&lt;/a&gt; we can use that’s relatively efficient:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;uniqFlags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;dataRow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;randomRecip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;numDigits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uniqFlags&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ve created a global set object, &lt;code&gt;uniqFlags&lt;/code&gt; which will acts as a scratchpad for random numbers we’ve already used – and pass it into the function &lt;code&gt;randomRecip&lt;/code&gt; in the usual way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Need to treat ensureUnique only with mutating list methods such as 'add', so the updated value is returned to the calling function
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;randomRecip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensureUnique&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="n"&gt;taken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
   &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;taken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="n"&gt;localpartnum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;randrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;taken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;localpartnum&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ensureUnique&lt;/span&gt;                    &lt;span class="c1"&gt;# If already had this number, then pick another one
&lt;/span&gt;   &lt;span class="n"&gt;ensureUnique&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;localpartnum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;'anon'&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;localpartnum&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;zfill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;'@'&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;    &lt;span class="c1"&gt;# Pad the number out to a fixed length of digits
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python allows changes made to &lt;code&gt;ensureUnique&lt;/code&gt; inside the function using the &lt;code&gt;.add()&lt;/code&gt; method to show up in the global data – in other words, the parameter is called by reference.&lt;/p&gt;

&lt;p&gt;For the other fields, picking random values from a small set of options is easy. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;randomMemberType&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
   &lt;span class="n"&gt;tiers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'bronze'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'silver'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'gold'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'platinum'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tiers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can pick randomized US postal states in exactly the same way. The custID field is just a naive random number (so it might repeat). I’ve left that as an exercise for the reader to change, if you wish (hint: use another set).&lt;/p&gt;

&lt;p&gt;For the tags field – we would like to assign somewhere between none and all of the possible Avocado varieties to each person; and for good measure we’ll randomize the order of those tags too. Here’s how we do that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Compose a random number of tags, in random shuffled order, from a preset short list.
# List of varieties is taken from: http://www.californiaavocado.com/how-tos/avocado-varieties.aspx
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;randomTags&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
   &lt;span class="n"&gt;avocadoVarieties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'bacon'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'fuerte'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'gwen'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'hass'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'lamb hass'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'pinkerton'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'reed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'zutano'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
   &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;randrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;avocadoVarieties&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
   &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;avocadoVarieties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
   &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What’s in a name?
&lt;/h2&gt;

&lt;p&gt;SparkPost recipient-list format supports a text name field, as well as an email address. It would be nice to have realistic-looking data for that. Fortunately, someone’s already &lt;a href="https://github.com/treyhunner/names"&gt;built a package&lt;/a&gt; that uses the 1990 US Census data, that turns out to be easy to leverage. You’ll recall we installed the &lt;code&gt;names&lt;/code&gt; package earlier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Prepare a cache of actual, random names - this enables long lists to be built faster
&lt;/span&gt;&lt;span class="n"&gt;nameList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="n"&gt;nameList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;'first'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_first_name&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;'last'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_last_name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;names&lt;/code&gt; library calls take a little while to run, which could really slow down our list creation. Rather than calling the function for every row, the above code builds a nameList of first and last names, that we can choose from later. For our purposes, it’s OK to have text names that might repeat (i.e. more than one Jane Doe) – only the email addresses need be strictly unique.&lt;/p&gt;

&lt;p&gt;The choice of 100 in the above code is fairly arbitrary – it will give us “enough randomness when picking a random first-name and separately picking a random last-name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full speed ahead
&lt;/h2&gt;

&lt;p&gt;A quick local test shows the tool can create a 100,000 entry recipient list – about 20MB – in seven seconds, so you shouldn’t have to wait long even for large outputs.&lt;/p&gt;

&lt;p&gt;The output of the tool is just a text stream, so you can redirect it into a file using &amp;gt;, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;./gen-sparkpost-lists.py recip 100000 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;mylist.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also pipe it into other tools. &lt;a href="https://csvkit.readthedocs.io/en/1.0.2/"&gt;CSVkit&lt;/a&gt; is great for this – you can choose which columns to filter on (with csvcut), display (with csvlook) etc.  For example, you could easily create a file with just email, name, and substitution_data, and view it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;./gen-sparkpost-lists.py recip 10 | csvcut &lt;span class="nt"&gt;-c1&lt;/span&gt;,2,5 | csvlook
|-----------------------------------------------------------------------------------------------------------|
|  email                                    | name             | substitution_data                          |
|-----------------------------------------------------------------------------------------------------------|
|  anon78856278@demo.sink.sparkpostmail.com | Linda Erdmann    | &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"memberType"&lt;/span&gt;: &lt;span class="s2"&gt;"gold"&lt;/span&gt;, &lt;span class="s2"&gt;"state"&lt;/span&gt;: &lt;span class="s2"&gt;"MN"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;      |
|  anon27569456@demo.sink.sparkpostmail.com | James Glenn      | &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"memberType"&lt;/span&gt;: &lt;span class="s2"&gt;"platinum"&lt;/span&gt;, &lt;span class="s2"&gt;"state"&lt;/span&gt;: &lt;span class="s2"&gt;"PA"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;  |
|  anon82026154@demo.sink.sparkpostmail.com | Mark Morris      | &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"memberType"&lt;/span&gt;: &lt;span class="s2"&gt;"bronze"&lt;/span&gt;, &lt;span class="s2"&gt;"state"&lt;/span&gt;: &lt;span class="s2"&gt;"NC"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;    |
|  anon99410317@demo.sink.sparkpostmail.com | Daniel Baldwin   | &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"memberType"&lt;/span&gt;: &lt;span class="s2"&gt;"platinum"&lt;/span&gt;, &lt;span class="s2"&gt;"state"&lt;/span&gt;: &lt;span class="s2"&gt;"TX"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;  |
|  anon40941199@demo.sink.sparkpostmail.com | Cammie Cornell   | &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"memberType"&lt;/span&gt;: &lt;span class="s2"&gt;"platinum"&lt;/span&gt;, &lt;span class="s2"&gt;"state"&lt;/span&gt;: &lt;span class="s2"&gt;"TX"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;  |
|  anon81569289@demo.sink.sparkpostmail.com | Mary Pearce      | &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"memberType"&lt;/span&gt;: &lt;span class="s2"&gt;"bronze"&lt;/span&gt;, &lt;span class="s2"&gt;"state"&lt;/span&gt;: &lt;span class="s2"&gt;"NC"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;    |
|  anon87708262@demo.sink.sparkpostmail.com | Angella Souphom  | &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"memberType"&lt;/span&gt;: &lt;span class="s2"&gt;"bronze"&lt;/span&gt;, &lt;span class="s2"&gt;"state"&lt;/span&gt;: &lt;span class="s2"&gt;"NV"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;    |
|  anon74282988@demo.sink.sparkpostmail.com | Antonio Erdmann  | &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"memberType"&lt;/span&gt;: &lt;span class="s2"&gt;"platinum"&lt;/span&gt;, &lt;span class="s2"&gt;"state"&lt;/span&gt;: &lt;span class="s2"&gt;"MD"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;  |
|  anon48883171@demo.sink.sparkpostmail.com | Randolph Maranto | &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"memberType"&lt;/span&gt;: &lt;span class="s2"&gt;"bronze"&lt;/span&gt;, &lt;span class="s2"&gt;"state"&lt;/span&gt;: &lt;span class="s2"&gt;"MA"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;    |
|  anon17719693@demo.sink.sparkpostmail.com | Jack Hudson      | &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"memberType"&lt;/span&gt;: &lt;span class="s2"&gt;"silver"&lt;/span&gt;, &lt;span class="s2"&gt;"state"&lt;/span&gt;: &lt;span class="s2"&gt;"CA"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;    |
|-----------------------------------------------------------------------------------------------------------|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  And finally …
&lt;/h2&gt;

&lt;p&gt;Download the &lt;a href="https://github.com/tuck1s/gen-SparkPost-Lists-python"&gt;code&lt;/a&gt; and make your own test recipient and suppression lists. Leave a comment below to let me know how you’ve used it and what other hacks you’d like to see.&lt;/p&gt;

&lt;p&gt;This post was originally posted on &lt;a href="https://www.sparkpost.com/blog/recipient-suppression-lists-python/"&gt;SparkPost&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>email</category>
      <category>python</category>
    </item>
    <item>
      <title>Sending Scheduled Mailings Simply with SparkPost</title>
      <dc:creator>SteveT</dc:creator>
      <pubDate>Wed, 31 May 2017 21:48:36 +0000</pubDate>
      <link>https://forem.com/sparkpost/sending-scheduled-mailings-simply-with-sparkpost</link>
      <guid>https://forem.com/sparkpost/sending-scheduled-mailings-simply-with-sparkpost</guid>
      <description>&lt;p&gt;Do you need to send batches of emails, synchronised to go at a set time? Are you unsure whether to develop your own campaign management tools, or buy off-the-shelf? Have you been through our &lt;a href="https://www.sparkpost.com/docs/getting-started/sparkpost-new-user-guide/"&gt;Getting Started Guide&lt;/a&gt;, and are inspired to send your first campaign, but are feeling a bit nervous about writing your own code?&lt;/p&gt;

&lt;p&gt;A customer of ours recently had this same need - sending out email batches of a few million emails each morning. They looked at some great fully-featured campaign management tools from &lt;a href="https://www.sparkpost.com/partners/"&gt;SparkPost partners&lt;/a&gt; such as &lt;a href="https://iterable.com/"&gt;Iterable&lt;/a&gt;, &lt;a href="https://www.ongage.com/"&gt;Ongage&lt;/a&gt;, and &lt;a href="http://cordial.io/"&gt;Cordial&lt;/a&gt; that cover this need, and lots more besides.  When you have many different campaign types, complex â€˜customer journey’ campaigns, and integrated WYSIWYG editors - they're a good option.&lt;/p&gt;

&lt;p&gt;If, however, you're looking for something simple, there is another way - and you're in the right place!  &lt;a href="https://github.com/SparkPost/python-sparkpost"&gt;SparkPost’s Python library&lt;/a&gt; and our &lt;a href="https://www.sparkpost.com/blog/why-not-schedule-the-emails-you-send/"&gt;built-in scheduled sending feature&lt;/a&gt; make it easy to put something together.&lt;/p&gt;

&lt;p&gt;We’ll put ourselves in the shoes of our friendly fictional company, Avocado Industries, and follow them through setting up a campaign. This article takes you through various features of SparkPost’s Python client library, and links to the final code &lt;a href="https://github.com/tuck1s/sparkySched"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what do I need?
&lt;/h2&gt;

&lt;p&gt;You’re sending out a newsletter to your subscribers.  You’ve &lt;a href="https://www.sparkpost.com/docs/getting-started/creating-template/"&gt;created a nice looking template&lt;/a&gt; and uploaded it to SparkPost.  You have your recipient list at hand, or can export it easily enough from your database.  You want SparkPost to mail-merge the recipient personalization details in, and get your awesome send out.&lt;/p&gt;

&lt;p&gt;These needs translate into the following design goals for the code we’re going to write:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Specify everything about your send using parameters and a small text file.  You don’t want to change the code for each campaign.&lt;/li&gt;
&lt;li&gt;Leverage the SparkPost stored template and personalization features, without doing any programming yourself.&lt;/li&gt;
&lt;li&gt;Use local flat files for the recipient lists.  The same format used by SparkPost’s &lt;a href="https://support.sparkpost.com/customer/portal/articles/2351320-uploading-and-storing-a-recipient-list-as-a-csv-file?_ga=2.108761730.1899408583.1496187922-424867781.1485888065"&gt;stored recipient list&lt;/a&gt; uploads is a good fit.&lt;/li&gt;
&lt;li&gt;Gather recipients from your list into API-call batches for efficiency - with no upper limits to the overall size of your send.&lt;/li&gt;
&lt;li&gt;Support timezones for the scheduled send, and also support â€˜just send it now’.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data guacamole
&lt;/h2&gt;

&lt;p&gt;The SparkPost recipient-list format looks like this, with all the fields populated.  Here we see Jerome’s details for the Avocado Industries loyalty scheme.  We can see he’s a gold-card member, lives in Washington State, and likes several &lt;a href="https://en.wikipedia.org/wiki/Avocado#A_cultivars"&gt;different avocado varieties&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;email,name,return_path,metadata,substitution_data,tags
jerome.russell@example.com,Jerome Russell,bounce@avocado-industries.com,"{""custID"": 60525717}","{""memberType"": ""gold"", ""state"": ""WA""}","[""hass"", ""pinkerton"", ""gwen"", ""lamb hass"", ""fuerte"", ""bacon""]"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything apart from the email address is optional, so it would be nice to have the tool also accept just a plain old list of email addresses.  It would also be nice if the tool is happy if we omit the header line.  That’s easily done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taco me to the start
&lt;/h2&gt;

&lt;p&gt;The tool is written for &lt;code&gt;python3&lt;/code&gt;.  SparkPost relies on the &lt;code&gt;pip&lt;/code&gt; installer, and we’ll need &lt;code&gt;git&lt;/code&gt; to obtain the tool.  You can check if you already have these tools, using the following commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python3 &lt;span class="nt"&gt;-V&lt;/span&gt;
Python 3.5.1
&lt;span class="nv"&gt;$ &lt;/span&gt;pip3 &lt;span class="nt"&gt;-V&lt;/span&gt;
pip 9.0.1 from /usr/local/lib/python3.5/site-packages &lt;span class="o"&gt;(&lt;/span&gt;python 3.5&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;$git&lt;/span&gt; &lt;span class="nt"&gt;--version&lt;/span&gt;
git version 2.7.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you already have them, continue to “Add SparkPost Python Library sauce” below.  Otherwise here is a simple install sequence for Amazon EC2 Linux:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;su -
yum update &lt;span class="nt"&gt;-y&lt;/span&gt;
yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; python35
yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; wget
wget https://bootstrap.pypa.io/get-pip.py
python3 get-pip.py 
yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are using another platform, check out installation instructions for your platform &lt;a href="https://www.python.org/"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add SparkPost Python Library sauce
&lt;/h2&gt;

&lt;p&gt;We use &lt;code&gt;pip3&lt;/code&gt; to install, as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/local/bin/pip3 &lt;span class="nb"&gt;install &lt;/span&gt;sparkpost
Collecting sparkpost
  Using cached sparkpost-1.3.5-py2.py3-none-any.whl
Collecting requests&amp;gt;&lt;span class="o"&gt;=&lt;/span&gt;2.5.1 &lt;span class="o"&gt;(&lt;/span&gt;from sparkpost&lt;span class="o"&gt;)&lt;/span&gt;
  Using cached requests-2.13.0-py2.py3-none-any.whl
Installing collected packages: requests, sparkpost
Successfully installed requests-2.13.0 sparkpost-1.3.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get the &lt;code&gt;sparkySched&lt;/code&gt; code from Github using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git clone https://github.com/tuck1s/sparkySched.git
Cloning into &lt;span class="s1"&gt;'sparkySched'&lt;/span&gt;...
remote: Counting objects: 55, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
remote: Compressing objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;3/3&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
remote: Total 55 &lt;span class="o"&gt;(&lt;/span&gt;delta 0&lt;span class="o"&gt;)&lt;/span&gt;, reused 0 &lt;span class="o"&gt;(&lt;/span&gt;delta 0&lt;span class="o"&gt;)&lt;/span&gt;, pack-reused 52
Unpacking objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;55/55&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Checking connectivity... &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;sparkySched
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Gotta be .ini to win it
&lt;/h2&gt;

&lt;p&gt;We now set up some attributes such as your API key, campaign, and certain substitution data in a text file, as they will be the same each time you send.  An example is provided in the project called &lt;code&gt;sparkpost.ini.example&lt;/code&gt;.  Rename this to &lt;code&gt;sparkpost.ini&lt;/code&gt;, and replace &lt;em&gt;&lt;/em&gt; with a key you’ve created in your own SparkPost account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[SparkPost]&lt;/span&gt;
&lt;span class="py"&gt;Authorization&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;YOUR API KEY&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;#Campaign setup
&lt;/span&gt;&lt;span class="py"&gt;Campaign&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;avocado-saladcopter&lt;/span&gt;
&lt;span class="py"&gt;GlobalSub&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;{"subject": "Fresh avocado delivered to your door in 30 minutes by our flying saladcopter"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  And, Send!
&lt;/h2&gt;

&lt;p&gt;There’s a sample file of 1000 safe test recipients included in the project that we can send to.  Change the template name below from &lt;code&gt;avocado-goodness&lt;/code&gt; to one you have in your account, and set the sending time to suit you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;./sparkySched.py recips_1k_sub_n_tags.csv avocado-goodness 2017-05-08T19:10:00+01:00
Opened connection to https://api.sparkpost.com
Injecting to SparkPost:
To  1000 recips: template &lt;span class="s2"&gt;"avocado-goodness"&lt;/span&gt; start_time 2017-05-08T19:10:00+01:00: OK - &lt;span class="k"&gt;in &lt;/span&gt;1.62 seconds
&lt;span class="err"&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all is well, you should see the “OK” line, and your mailing is sent.  That’s all you need to do.  Happy sending!&lt;/p&gt;

&lt;h2&gt;
  
  
  Code salsa
&lt;/h2&gt;

&lt;p&gt;In this section, we take a deeper look inside the code.  You can skip this if you just want to use the tool instead of changing it.  Here’s how we call the SparkPost API to send messages, using the &lt;a href="https://github.com/SparkPost/python-sparkpost"&gt;SparkPost Python library&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Inject the messages into SparkPost for a batch of recipients, using the specified transmission parameters
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sendToRecips&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recipBatch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sendObj&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'To'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipBatch&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="n"&gt;rjust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="s"&gt;'recips: template "'&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;sendObj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'template'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;'" start_time '&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;sendObj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'start_time'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;': '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="c1"&gt;# Compose in additional API-call parameters
&lt;/span&gt;   &lt;span class="n"&gt;sendObj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
       &lt;span class="s"&gt;'recipients'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;recipBatch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="s"&gt;'track_opens'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="s"&gt;'track_clicks'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="s"&gt;'use_draft_template'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
   &lt;span class="p"&gt;})&lt;/span&gt;
   &lt;span class="n"&gt;startT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
   &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transmissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;sendObj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;endT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'total_accepted_recipients'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipBatch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
           &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
           &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'OK - in'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endT&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;startT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;'seconds'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;SparkPostAPIException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'error code'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After some helpful on-screen output about what we’re trying to send, the function composes the recipients with the other passed-in ingredients and mixes in some sensible defaults, using &lt;code&gt;sendObj.update()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The SparkPost library call is wrapped in a &lt;code&gt;try/except&lt;/code&gt; clause, as it can return errors at the application level (such as having an incorrect API key), or at the transport level (such as your Internet connection being down).  This is generally good practice with any code that’s communicating with a remote service, and follows the examples &lt;a href="https://github.com/SparkPost/python-sparkpost/blob/master/examples/transmissions/handle_exception.py"&gt;packaged with our library&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We use &lt;code&gt;startT&lt;/code&gt;, &lt;code&gt;endT&lt;/code&gt;, and the &lt;code&gt;time()&lt;/code&gt; function to measure how long the API call actually takes.  While not strictly necessary, it’s interesting to see how performance varies with batch size, routing distance from client to server, etc.&lt;/p&gt;

&lt;p&gt;We will now craft the code to read parameters from the &lt;code&gt;.ini&lt;/code&gt; file and use them in the API sends.  Let’s read and check the mandatory parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Get parameters from .ini file
&lt;/span&gt;&lt;span class="n"&gt;configFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'sparkpost.ini'&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;configparser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConfigParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configFile&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'SparkPost'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Authorization'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# API key is mandatory
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Error: missing Authorization line in '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;configFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;baseUri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'https://'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Host'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'api.sparkpost.com'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# optional, default to public service
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Python library &lt;code&gt;configParser&lt;/code&gt; does the heavy lifting.  You’ve got to have an API key, so we exit if unset.  baseUri defaults to the &lt;a href="https://developers.sparkpost.com/api/?_ga=2.100782849.1899408583.1496187922-424867781.1485888065"&gt;sparkpost.com API endpoint&lt;/a&gt; if it’s unset.  The other parameters from the .ini file are read in the same way, and are described in the project README file.&lt;/p&gt;

&lt;p&gt;There are other ways to set things up in Python, such as using environment variables. My preference is for .ini files, because the file is right there, staring at you.  It’s easy to store, communicate, change and check, right there in your project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chickens go in, pies come out ...
&lt;/h2&gt;

&lt;p&gt;Let’s look at how to read that .csv format recipient list.  Python provides a nice library, &lt;code&gt;csv&lt;/code&gt;.  All the reading of double-quoted stuff that .csv files need to carry JSON objects like &lt;code&gt;"{""custID"": 60525717}"&lt;/code&gt; is taken care of for us.&lt;/p&gt;

&lt;p&gt;We could use &lt;code&gt;csv&lt;/code&gt;  to read the whole recipient-list into a Python array object - but that’s not a great idea, if we have squillions of addresses in our list.  The client will be perfectly fast enough for our purposes, and we’ll use less client memory, if we read in just enough to give us a nice sized batch to cook each time around.&lt;/p&gt;

&lt;p&gt;We’ll also handle line 1 of the file specially, to meet our â€˜go easy on the optional file header’ requirement.  Recall that a well-formed header should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;email,name,return_path,metadata,substitution_data,tags
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it’s got the single word â€˜email’ somewhere on line 1, let’s assume it really is a header, and we’ll take our field layouts from that line.  The tool will be happy if you omit optional fields, or have them in a different order on the line.  The only one you absolutely need is the email field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;recipBatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fh_recipList&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;line_num&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                         &lt;span class="c1"&gt;# Check if header row present
&lt;/span&gt;       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s"&gt;'email'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                        &lt;span class="c1"&gt;# we've got an email header-row field - continue
&lt;/span&gt;           &lt;span class="n"&gt;hdr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
           &lt;span class="k"&gt;continue&lt;/span&gt;
       &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="s"&gt;'@'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="c1"&gt;# Also accept headerless format with just email addresses
&lt;/span&gt;           &lt;span class="n"&gt;hdr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                     &lt;span class="c1"&gt;# line 1 contains data - so continue processing
&lt;/span&gt;       &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
           &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Invalid .csv file header - must contain "email" field'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then check if it’s really a headerless file with just a bunch of email addresses in it, by checking for a single entry with an @ sign.&lt;/p&gt;

&lt;p&gt;In the main loop &lt;code&gt;for i,h in enumerate(hdr)&lt;/code&gt; we use some nice Python language features to conform the data to the JSON object that SparkPost is expecting.  The &lt;code&gt;name&lt;/code&gt; field needs to be put inside the &lt;code&gt;address.name&lt;/code&gt; JSON attribute.  Return_path is added, if present.  Metadata, substitution_data and tags all come in to us as JSON-formatted strings, so we unpack them using &lt;code&gt;json.loads()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="c1"&gt;# Parse values from the line of the file into a row object
&lt;/span&gt;   &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
   &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;                                        &lt;span class="c1"&gt;# Only parse non-empty fields from this line
&lt;/span&gt;           &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
               &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'address'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;              &lt;span class="c1"&gt;# begin the address
&lt;/span&gt;           &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
               &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'address'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;      &lt;span class="c1"&gt;# add into the existing address structure
&lt;/span&gt;           &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'return_path'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
               &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                           &lt;span class="c1"&gt;# simple string field
&lt;/span&gt;           &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'metadata'&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'substitution_data'&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'tags'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
               &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;               &lt;span class="c1"&gt;# parse these fields as JSON text into dict objects
&lt;/span&gt;           &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
               &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Unexpected .csv file field name found: '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
               &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="n"&gt;recipBatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipBatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;batchSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="n"&gt;sendToRecips&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recipBatch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;txOpts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;recipBatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;                                 &lt;span class="c1"&gt;# Empty out, ready for next batch
# Handle the final batch remaining, if any
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipBatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;sendToRecips&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recipBatch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;txOpts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All that’s left to do, is to chew through the list, sending each time we gather in a full sized batch.  We send any final batch at the end, and we’re done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Command-line garnish - a pinch of &lt;del&gt;thyme&lt;/del&gt; time
&lt;/h2&gt;

&lt;p&gt;The last part we need is some command-line argument parsing.  The recipient-list, the template-ID, and the sending date/time are the things you might want to vary each time the tool is run.  Python munges your arguments using &lt;code&gt;argv[]&lt;/code&gt; in much the same way as other languages.&lt;/p&gt;

&lt;p&gt;There are all kinds of nonsense possible with input date and time - such as February 30th, 24:01 and so on.  Mix in timezone offsets, so the user can schedule in their local time, and no-one would seriously want to write their own time parsing code!  SparkPost’s API will of course be the final arbiter on what’s good, and what’s not - but it’s better to do some initial taste-tests before we try to send.&lt;/p&gt;

&lt;p&gt;Python’s &lt;code&gt;strptime()&lt;/code&gt; function does mostly what we want.  The format string can be made like SparkPost format, except Python has no &lt;code&gt;:&lt;/code&gt; separator in the &lt;code&gt;%z&lt;/code&gt; timezone. Python’s elegant negative indexing into strings (working backwards from the end of the string) makes it easy to write a small checking function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Validate SparkPost start_time format, slightly different to Python datetime (which has no : in timezone offset format specifier)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;isExpectedDateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="n"&gt;format_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'%Y-%m-%dT%H:%M:%S%z'&lt;/span&gt;
   &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="n"&gt;colon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;colon&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
           &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
       &lt;span class="n"&gt;colonless_timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
       &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;colonless_timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;format_string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
   &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Plat du jour
&lt;/h2&gt;

&lt;p&gt;If you don’t want to schedule a future start_time, you can just give today’s date and time. Times in the past are sent immediately.&lt;/p&gt;

&lt;p&gt;Depending on where your code is running (I happen to be using an AWS virtual machine), you should see each batch get sent in a few seconds. Even though it’s single-threaded, you can schedule a million emails in around ten minutes.  The actual send will proceed (at the scheduled start_time) as fast as it can.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./sparkySched.py recips_100k_sub_n_tags.csv avocado-goodness 2017-04-11T23:55:00+01:00
Opened connection to https://demo.sparkpostelite.com
Injecting to SparkPost:
To 10000 recips: template "avocado-goodness" start_time 2017-04-11T23:55:00+01:00: OK - in 4.97 seconds
To 10000 recips: template "avocado-goodness" start_time 2017-04-11T23:55:00+01:00: OK - in 4.92 seconds
To 10000 recips: template "avocado-goodness" start_time 2017-04-11T23:55:00+01:00: OK - in 4.783 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that’s pretty much it.  The full code, which is just over 100 actual lines, is &lt;a href="https://github.com/tuck1s/sparkySched"&gt;here&lt;/a&gt; with easy installation instructions. &lt;/p&gt;

&lt;h2&gt;
  
  
  A small digestif ...
&lt;/h2&gt;

&lt;p&gt;What’s the best way to test this out for real?  A tool to generate dummy recipient-list with sinkhole addresses could be handy. Keep an eye out for a follow-up blogpost..  I’ve included a couple of ready-made recipes recipient files in the &lt;a href="https://github.com/tuck1s/sparkySched"&gt;github project&lt;/a&gt; to get you started.&lt;/p&gt;

&lt;p&gt;Is this your first dining experience with Python and SparkPost?  Did I add too much seasoning?  Should the author be pun-ished for these bad jokes? Let us know!&lt;/p&gt;

&lt;p&gt;P.S. Want to talk more Python with us? Join us in our &lt;a href="http://slack.sparkpost.com/?_ga=2.209442194.1899408583.1496187922-424867781.1485888065"&gt;Community Slack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post was originally posted on the &lt;a href="https://www.sparkpost.com/blog/sending-scheduled-mailings-simply/"&gt;SparkPost blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>email</category>
      <category>python</category>
    </item>
  </channel>
</rss>
