",
190 | "john@example.com",
191 | "mike@example.com"
192 | ],
193 | "send_each_at": [
194 | 1409348513,
195 | 1409348514,
196 | 1409348515
197 | ]
198 | }
199 | ```
200 |
201 |
202 | # Section Tags
203 | Section tags are similar to substitution tags in how they’re built, but are specific to the message, not the recipient. You have to have a substitution tag value for each recipient, but you can have any number of section tags. Section tags can then contain Substitution tags for the recipient if needed. Section tags have to be contained within a Substitution tag, since SendGrid needs to know which data to populate for the recipient.
204 |
205 | The format of the SMTP API section tag has the form:
206 | ```python
207 | {
208 | "section": {
209 | ":sectionName1": "section 1 text",
210 | ":sectionName2": "section 2 text"
211 | }
212 | }
213 | ```
214 |
215 |
216 | # Substitution Tags
217 |
218 | **This endpoint allows you to easily generate dynamic content for each recipient on your list.**
219 |
220 | When you send to a list of recipients over SMTP API you can specify substitution tags specific to each recipient. For example, a first name that will then be inserted into an opening greeting like the following, where each recipient sees -firstName- replaced with their first name.
221 |
222 | `"Dear -firstName-"`
223 |
224 | These tags can also be used in more complex scenarios. For example, you could use a -customerID- to build a custom URL that is specific to that user.
225 |
226 | A customer specific ID can replace -customerID- in the URL within your email
227 | `Claim your offer!`
228 |
229 | ## Substitution Tag Example
230 |
231 | Email HTML content:
232 | ```
233 |
234 |
235 |
236 | Hello -name-,
237 | Thank you for your interest in our products. I have set up an appointment
238 | to call you at -time- EST to discuss your needs in more detail. If you would
239 | like to reschedule this call please visit the following link:
240 | reschedule
241 |
242 | Regards,
243 |
244 | -salesContact-
245 | -contactPhoneNumber-
246 |
247 |
248 |
249 | ```
250 |
251 | An accompanying SMTP API JSON header might look something like this:
252 | ```
253 | {
254 | "to": [
255 | "john.doe@gmail.com",
256 | "jane.doe@hotmail.com"
257 | ],
258 | "sub": {
259 | "-name-": [
260 | "John",
261 | "Jane"
262 | ],
263 | "-customerID-": [
264 | "1234",
265 | "5678"
266 | ],
267 | "-salesContact-": [
268 | "Jared",
269 | "Ben"
270 | ],
271 | "-contactPhoneNumber-": [
272 | "555.555.5555",
273 | "777.777.7777"
274 | ],
275 | "-time-": [
276 | "3:00pm",
277 | "5:15pm"
278 | ]
279 | }
280 | }
281 | ```
282 |
283 | The resulting email for John would look like this:
284 | ```
285 |
286 |
287 |
288 | Hello John,
289 | Thank you for your interest in our products. I have set up an appointment
290 | to call you at 3:00pm EST to discuss your needs in more detail. If you would
291 | like to reschedule this call please visit the following link:
292 | reschedule
293 |
294 | Regards,
295 |
296 | Jared
297 | 555.555.5555
298 |
299 |
300 |
301 | ```
302 |
303 |
304 | # Suppression Groups
305 |
306 | ## Defining an Unsubscribe Group When Sending
307 |
308 | **This endpoint allows you to specify an unsubscribe group for an email depends on how you will be sending that email.**
309 |
310 | Precaution:
311 |
312 | * When sending an SMTP message, add the group’s ID to the X-SMTPAPI header.
313 | * When sending an email via the Web API v2, add the group’s ID in the `x-smtpapi` parameter.
314 | * When sending an email via the Web API v3, define the group’s ID in the `asm.group_id` parameter.
315 |
316 | You may only specify one group per send, and you should wait one minute after creating the group before sending with it.
317 |
318 | ```python
319 | {
320 | "asm_group_id": 1
321 | }
322 | ```
323 |
324 | Defining Unsubscribe Groups to display on the Manage Preferences page
325 | To specify which groups to display on the Manage Preferences page of an email, add the group IDs to the X-SMTPAPI header of an SMTP message, or in the x-smtpapi parameter of a mail.send API call. If the asm_groups_to_display header is omitted, your default groups will be shown on the Manage Preferences page instead.
326 |
327 | You can specify up to 25 groups to display.
328 | ```python
329 | {
330 | "asm_groups_to_display": [1, 2, 3]
331 | }
332 | ```
333 |
334 | ## Groups
335 | You can find your group IDs by looking at the Group ID column in the Unsubscribe Groups UI, or by calling the [GET method](https://sendgrid.com/docs/API_Reference/Web_API_v3/Suppression_Management/groups.html#-GET) of the group's resource.
336 |
337 |
338 | # Unique Arguments
339 |
340 | The SMTP API JSON string allows you to attach an unlimited number of unique arguments to your email up to 10,000 bytes. The arguments are used only for tracking. They can be retrieved through the Event API or the Email Activity page.
341 |
342 | These arguments can be added using a JSON string like this:
343 | ```
344 | {
345 | "unique_args": {
346 | "customerAccountNumber": "55555",
347 | "activationAttempt": "1",
348 | "New Argument 1": "New Value 1",
349 | "New Argument 2": "New Value 2",
350 | "New Argument 3": "New Value 3",
351 | "New Argument 4": "New Value 4"
352 | }
353 | }
354 | ```
355 |
356 | These arguments can then be seen in posts from the SendGrid Event Webhook. The contents of one of these POST requests would look something like this:
357 |
358 | ## Example Webhook Post Data
359 |
360 | ```
361 | {
362 | "sg_message_id": "145cea24eb8.1c420.57425.filter-132.3382.5368192A3.0",
363 | "New Argument 1": "New Value 1",
364 | "event": "processed",
365 | "New Argument 4": "New Value 4",
366 | "email": "user@example.com",
367 | "smtp-id": "<145cea24eb8.1c420.57425@localhost.localdomain>",
368 | "timestamp": 1399331116,
369 | "New Argument 2": "New Value 2",
370 | "New Argument 3": "New Value 3",
371 | "customerAccountNumber": "55555",
372 | "activationAttempt": "1"
373 | }
374 | ```
375 | Unique Arguments will also be shown in the Email Activity tab of your account.
376 |
377 | To apply different unique arguments to individual emails, you may use substitution tags. An example of this would look like:
378 | ```
379 | {
380 | "sub": {
381 | "-account_number-": [
382 | "314159",
383 | "271828"
384 | ]
385 | },
386 | "unique_args": {
387 | "customerAccountNumber": "-account_number-"
388 | }
389 | }
390 | ```
391 |
--------------------------------------------------------------------------------
/VERSION.txt:
--------------------------------------------------------------------------------
1 | 0.4.12
2 |
--------------------------------------------------------------------------------
/changes.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | """
3 | Small python script that, when run, will update the CHANGELOG with information
4 | about all merged pull requests since the previous release.
5 |
6 | This script must be run after tagging the latest version
7 | It checks the log of commits since the previous tag and parses it
8 | """
9 | import re
10 | import subprocess
11 | import sys
12 | from datetime import datetime
13 |
14 | # Regex patterns
15 | RELEASE_MD_PATTERN = re.compile(r'## \[(\d+\.\d+\.\d+)\]')
16 | MERGED_PR_PATTERN = re.compile(
17 | r'([0-9a-f]{7}) Merge pull request #(\d+) from (.+)/.+'
18 | )
19 | TAG_PATTERN = re.compile(
20 | r'refs/tags/v(\d+\.\d+\.\d+) (\w{3} \w{3} \d{1,2} \d{2}:\d{2}:\d{2} \d{4})'
21 | )
22 |
23 | # PR Type terms
24 | FIX_TERMS = ['fix', 'change', 'update']
25 |
26 |
27 | # Helper functions
28 | def generate_pr_link(pr_num):
29 | """
30 | Returns a markdown link to a PR in this repo given its number
31 | """
32 | return (
33 | '[PR #{0}](https://github.com/sendgrid/smtpapi-python/pulls/{0})'
34 | ).format(pr_num)
35 |
36 |
37 | def generate_user_link(user):
38 | """
39 | Returns a markdown link to a user
40 | """
41 | return '[@{0}](https://github.com/{0})'.format(user)
42 |
43 |
44 | # Get latest tag
45 | command = ['git', 'tag', '--format=%(refname) %(creatordate)']
46 | res = subprocess.run(command, capture_output=True, text=True)
47 | if res.returncode != 0:
48 | print('Error occurred when running git tag command:', str(res.stderr))
49 | sys.exit(1)
50 | # Get the last line and get the tag number
51 | latest_release_match = TAG_PATTERN.match(
52 | list(filter(None, res.stdout.split('\n')))[-1],
53 | )
54 | latest_release = latest_release_match[1]
55 | latest_release_date = datetime.strptime(
56 | latest_release_match[2], '%a %b %d %H:%M:%S %Y',
57 | )
58 | print('Generating CHANGELOG for', latest_release)
59 |
60 | # Read in the CHANGELOG file first
61 | with open('CHANGELOG.md') as f:
62 | # Read the text in as a list of lines
63 | old_text = f.readlines()
64 | # Get the latest release (top of the CHANGELOG)
65 | for line in old_text:
66 | match = RELEASE_MD_PATTERN.match(line)
67 | if match:
68 | prev_release = match[1]
69 | break
70 |
71 | if latest_release == prev_release:
72 | print(
73 | 'The latest git tag matches the last release in the CHANGELOG. '
74 | 'Please tag the repository before running this script.'
75 | )
76 | sys.exit(1)
77 |
78 | # Use git log to list all commits between that tag and HEAD
79 | command = 'git log --oneline v{}..@'.format(prev_release).split(' ')
80 | res = subprocess.run(command, capture_output=True, text=True)
81 | if res.returncode != 0:
82 | print('Error occurred when running git log command:', str(res.stderr))
83 | sys.exit(1)
84 |
85 | # Parse the output from the above command to find all commits for merged PRs
86 | merge_commits = []
87 | for line in res.stdout.split('\n'):
88 | match = MERGED_PR_PATTERN.match(line)
89 | if match:
90 | merge_commits.append(match)
91 |
92 | # Determine the type of PR from the commit message
93 | added, fixes = [], []
94 | for commit in merge_commits:
95 | # Get the hash of the commit and get the message of it
96 | commit_sha = commit[1]
97 | command = 'git show {} --format=format:%B'.format(commit_sha).split(' ')
98 | res = subprocess.run(command, capture_output=True, text=True)
99 | out = res.stdout.lower()
100 | is_added = True
101 |
102 | # When storing we need the PR title, number and user
103 | data = {
104 | # 3rd line of the commit message is the PR title
105 | 'title': out.split('\n')[2],
106 | 'number': commit[2],
107 | 'user': commit[3],
108 | }
109 |
110 | for term in FIX_TERMS:
111 | if term in out:
112 | fixes.append(data)
113 | is_added = False
114 | break
115 | if is_added:
116 | added.append(data)
117 |
118 | # Now we need to write out the CHANGELOG again
119 | with open('CHANGELOG.md', 'w') as f:
120 | # Write out the header lines first
121 | for i in range(0, 3):
122 | f.write(old_text[i])
123 |
124 | # Create and write out the new version information
125 | latest_release_date_string = latest_release_date.strftime('%Y-%m-%d')
126 | f.write('## [{}] - {} ##\n'.format(
127 | latest_release,
128 | latest_release_date_string,
129 | ))
130 | # Add the stuff that was added
131 | f.write('### Added\n')
132 | for commit in added:
133 | f.write('- {}: {}{} (via {})\n'.format(
134 | generate_pr_link(commit['number']),
135 | commit['title'],
136 | '.' if commit['title'][-1] != '.' else '',
137 | generate_user_link(commit['user'])
138 | ))
139 | f.write('\n')
140 | # Add the fixes
141 | f.write('### Fixes\n')
142 | for commit in fixes:
143 | f.write('- {}: {}{} (via {})\n'.format(
144 | generate_pr_link(commit['number']),
145 | commit['title'],
146 | '.' if commit['title'][-1] != '.' else '',
147 | generate_user_link(commit['user'])
148 | ))
149 | f.write('\n')
150 |
151 | # Add the old stuff
152 | for i in range(3, len(old_text)):
153 | f.write(old_text[i])
154 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | web:
4 | build: .
5 | volumes:
6 | - .:/root
7 |
--------------------------------------------------------------------------------
/examples/example.py:
--------------------------------------------------------------------------------
1 | # Python 2/3 compatible codebase
2 | from __future__ import absolute_import, division, print_function
3 | from smtpapi import SMTPAPIHeader
4 |
5 | import time
6 | from os import path, sys
7 |
8 | if __name__ == '__main__' and __package__ is None:
9 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
10 |
11 | header = SMTPAPIHeader()
12 |
13 | # [To](http://sendgrid.com/docs/API_Reference/SMTP_API/index.html)
14 | # header.add_to('test@example.com')
15 | header.set_tos(['test1@example.com', 'test2@example.com'])
16 |
17 | # [Substitutions]
18 | # (http://sendgrid.com/docs/API_Reference/SMTP_API/substitution_tags.html)
19 | # header.add_substitution('key', 'value')
20 | header.set_substitutions({'key': ['value1', 'value2']})
21 |
22 | # [Unique Arguments]
23 | # (http://sendgrid.com/docs/API_Reference/SMTP_API/unique_arguments.html)
24 | # header.add_unique_arg('key', 'value')
25 | header.set_unique_args({'key': 'value'})
26 |
27 | # [Categories](http://sendgrid.com/docs/API_Reference/SMTP_API/categories.html)
28 | # header.add_category('category')
29 | header.set_categories(['category1', 'category2'])
30 |
31 | # [Sections](http://sendgrid.com/docs/API_Reference/SMTP_API/section_tags.html)
32 | # header.add_section('key', 'section')
33 | header.set_sections({'key1': 'section1', 'key2': 'section2'})
34 |
35 | # [Filters]
36 | # (http://sendgrid.com/docs/API_Reference/SMTP_API/apps.html)
37 | header.add_filter('filter', 'setting', 'value')
38 |
39 | # [ASM Group ID]
40 | # (https://sendgrid.com/docs/User_Guide/advanced_suppression_manager.html)
41 | header.set_asm_group_id('value')
42 |
43 | # [IP Pools]
44 | # (https://sendgrid.com/docs/API_Reference/Web_API_v3/IP_Management/ip_pools.html)
45 | header.set_ip_pool("testPool")
46 |
47 | # [Scheduling Parameters]
48 | # (https://sendgrid.com/docs/API_Reference/SMTP_API/scheduling_parameters.html)
49 | # header.add_send_each_at(unix_timestamp) # must be a unix timestamp
50 | # header.set_send_each_at([]) # must be a unix timestamp
51 | header.set_send_at(int(time.time())) # must be a unix timestamp
52 |
53 | print(header.json_string())
54 |
--------------------------------------------------------------------------------
/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | python2.7 setup.py install
4 | python2.7 test/__init__.py
5 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import io
2 | import os
3 | from distutils.file_util import copy_file
4 | from setuptools import setup, find_packages
5 |
6 |
7 | dir_path = os.path.abspath(os.path.dirname(__file__))
8 | readme = io.open(os.path.join(dir_path, 'README.rst'), encoding='utf-8').read()
9 | version = io.open(
10 | os.path.join(dir_path, 'VERSION.txt'),
11 | encoding='utf-8',
12 | ).read().strip()
13 | copy_file(os.path.join(dir_path, 'VERSION.txt'),
14 | os.path.join(dir_path, 'smtpapi', 'VERSION.txt'),
15 | verbose=0)
16 | setup(
17 | name='smtpapi',
18 | version=version,
19 | author='Yamil Asusta, Kane Kim',
20 | author_email='yamil@sendgrid.com, kane.isturm@sendgrid.com',
21 | url='https://github.com/sendgrid/smtpapi-python/',
22 | packages=find_packages(exclude=["test"]),
23 | include_package_data=True,
24 | license='MIT License',
25 | description='Simple wrapper to use SendGrid SMTP API',
26 | long_description=readme,
27 | classifiers=[
28 | 'Programming Language :: Python :: 2.7',
29 | 'Programming Language :: Python :: 3.4',
30 | 'Programming Language :: Python :: 3.5',
31 | 'Programming Language :: Python :: 3.6',
32 | 'Programming Language :: Python :: 3.7',
33 | 'Programming Language :: Python :: 3.8',
34 | 'Programming Language :: Python :: 3.9',
35 | 'Programming Language :: Python :: 3.10',
36 | ],
37 | )
38 |
--------------------------------------------------------------------------------
/smtpapi/__init__.py:
--------------------------------------------------------------------------------
1 | import json
2 | import decimal
3 |
4 |
5 | class _CustomJSONEncoder(json.JSONEncoder):
6 |
7 | def default(self, o):
8 | if isinstance(o, decimal.Decimal):
9 | return float(o)
10 | # Provide a fallback to the default encoder if we haven't implemented
11 | # special support for the object's class
12 | return super(_CustomJSONEncoder, self).default(o)
13 |
14 |
15 | class SMTPAPIHeader(object):
16 |
17 | def __init__(self):
18 | self.data = {}
19 |
20 | def add_to(self, to):
21 | if 'to' not in self.data:
22 | self.data['to'] = []
23 | if type(to) is list:
24 | self.data['to'] += to
25 | else:
26 | self.data['to'].append(to)
27 |
28 | def set_tos(self, tos):
29 | self.data['to'] = tos
30 |
31 | def add_substitution(self, key, value):
32 | if 'sub' not in self.data:
33 | self.data['sub'] = {}
34 | if key not in self.data['sub']:
35 | self.data['sub'][key] = []
36 | self.data['sub'][key].append(value)
37 |
38 | def set_substitutions(self, subs):
39 | self.data['sub'] = subs
40 |
41 | def _add_key_value(self, index, key, value):
42 | if index not in self.data:
43 | self.data[index] = {}
44 | self.data[index][key] = value
45 |
46 | def _add_key(self, index, key):
47 | if index not in self.data:
48 | self.data[index] = []
49 | self.data[index].append(key)
50 |
51 | def add_unique_arg(self, key, value):
52 | self._add_key_value('unique_args', key, value)
53 |
54 | def set_unique_args(self, value):
55 | self.data['unique_args'] = value
56 |
57 | def add_category(self, category):
58 | self._add_key('category', category)
59 |
60 | def set_categories(self, category):
61 | self.data['category'] = category
62 |
63 | def add_section(self, key, section):
64 | self._add_key_value('section', key, section)
65 |
66 | def set_sections(self, value):
67 | self.data['section'] = value
68 |
69 | def add_send_each_at(self, time):
70 | self._add_key('send_each_at', time)
71 |
72 | def set_send_each_at(self, time):
73 | self.data['send_each_at'] = time
74 |
75 | def set_send_at(self, time):
76 | self.data['send_at'] = time
77 |
78 | def add_filter(self, app, setting, val):
79 | if 'filters' not in self.data:
80 | self.data['filters'] = {}
81 | if app not in self.data['filters']:
82 | self.data['filters'][app] = {}
83 | if 'settings' not in self.data['filters'][app]:
84 | self.data['filters'][app]['settings'] = {}
85 | self.data['filters'][app]['settings'][setting] = val
86 |
87 | def set_asm_group_id(self, value):
88 | if not bool(value):
89 | self.data['asm_group_id'] = {}
90 | else:
91 | self.data['asm_group_id'] = value
92 |
93 | def set_ip_pool(self, value):
94 | if bool(value):
95 | self.data['ip_pool'] = value
96 | else:
97 | self.data['ip_pool'] = {}
98 |
99 | def json_string(self):
100 | result = {}
101 | for key in self.data.keys():
102 | if self.data[key] != [] and self.data[key] != {}:
103 | result[key] = self.data[key]
104 | return json.dumps(result, cls=_CustomJSONEncoder)
105 |
--------------------------------------------------------------------------------
/static/img/github-fork.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sendgrid/smtpapi-python/e89ee24d62cfc57608369836b0f8fc9a1daddeb3/static/img/github-fork.png
--------------------------------------------------------------------------------
/static/img/github-sign-up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sendgrid/smtpapi-python/e89ee24d62cfc57608369836b0f8fc9a1daddeb3/static/img/github-sign-up.png
--------------------------------------------------------------------------------
/test/__init__.py:
--------------------------------------------------------------------------------
1 | import decimal
2 | import json
3 | import os
4 | import datetime
5 |
6 | from smtpapi import SMTPAPIHeader
7 |
8 | try:
9 | import unittest2 as unittest
10 | except ImportError:
11 | import unittest
12 |
13 |
14 | class TestSMTPAPI(unittest.TestCase):
15 |
16 | def setUp(self):
17 | self.validHeader = json.loads('''{"to":["test@email.com",
18 | "test2@email.com", "test3@email.com"],
19 | "sub":{"subKey":["subValue"],"decimalKey":[1.23456789]},
20 | "section":{"testSection":"sectionValue"},
21 | "category":["testCategory"],
22 | "unique_args":{"testUnique":"uniqueValue"},
23 | "asm_group_id":42,
24 | "send_each_at":[1409348513, 1409348514],
25 | "send_at": 1409348515,
26 | "ip_pool": "testPool",
27 | "filters":{"testFilter":{"settings":{"filter":"filterValue"}}}}''')
28 |
29 | self.dropsHeader = json.loads('''{
30 | "sub":{"subKey":["subValue"],"decimalKey":[1.23456789]},
31 | "unique_args":{"testUnique":"uniqueValue"},
32 | "filters":{"testFilter":{"settings":{"filter":"filterValue"}}}}''')
33 |
34 | def test_add(self):
35 | header = SMTPAPIHeader()
36 | header.add_to('test@email.com')
37 | header.add_to(['test2@email.com', 'test3@email.com'])
38 | header.add_substitution('subKey', 'subValue')
39 | header.add_substitution('decimalKey', decimal.Decimal("1.23456789"))
40 | header.add_section('testSection', 'sectionValue')
41 | header.add_category('testCategory')
42 | header.add_unique_arg('testUnique', 'uniqueValue')
43 | header.set_asm_group_id(42)
44 | header.add_send_each_at(1409348513)
45 | header.add_send_each_at(1409348514)
46 | header.set_send_at(1409348515)
47 | header.set_ip_pool('testPool')
48 | header.add_filter('testFilter', 'filter', 'filterValue')
49 | self.assertEqual(self.validHeader, json.loads(header.json_string()))
50 |
51 | def test_set(self):
52 | header = SMTPAPIHeader()
53 | header.set_tos([
54 | "test@email.com",
55 | "test2@email.com",
56 | "test3@email.com",
57 | ])
58 | header.set_substitutions({
59 | "subKey": ["subValue"],
60 | "decimalKey": [decimal.Decimal("1.23456789")]
61 | })
62 | header.set_sections(json.loads('{"testSection":"sectionValue"}'))
63 | header.set_categories(["testCategory"])
64 | header.set_unique_args(json.loads('{"testUnique":"uniqueValue"}'))
65 | header.set_asm_group_id(42)
66 | header.set_send_each_at([1409348513, 1409348514])
67 | header.set_send_at(1409348515)
68 | header.set_ip_pool('testPool')
69 | header.add_filter('testFilter', 'filter', 'filterValue')
70 | self.assertEqual(self.validHeader, json.loads(header.json_string()))
71 |
72 | def test_drop_empty(self):
73 | header = SMTPAPIHeader()
74 | header.set_tos([])
75 | header.set_substitutions({
76 | "subKey": ["subValue"],
77 | "decimalKey": [decimal.Decimal("1.23456789")]
78 | })
79 | header.set_sections(json.loads('{}'))
80 | header.set_categories([])
81 | header.set_unique_args(json.loads('{"testUnique":"uniqueValue"}'))
82 | header.set_asm_group_id(None)
83 | header.set_send_each_at([])
84 | header.set_ip_pool(None)
85 | header.add_filter('testFilter', 'filter', 'filterValue')
86 | self.assertEqual(self.dropsHeader, json.loads(header.json_string()))
87 |
88 | def test_license_year(self):
89 | LICENSE_FILE = 'LICENSE'
90 | copyright_line = ''
91 | with open(LICENSE_FILE, 'r') as f:
92 | for line in f:
93 | if line.startswith('Copyright'):
94 | copyright_line = line.strip()
95 | break
96 | self.assertEqual(
97 | 'Copyright (C) %s, Twilio SendGrid, Inc. '
98 | % datetime.datetime.now().year,
99 | copyright_line
100 | )
101 |
102 |
103 | class TestRepository(unittest.TestCase):
104 |
105 | def setUp(self):
106 |
107 | self.required_files = [
108 | './Dockerfile',
109 | './.env_sample',
110 | './PULL_REQUEST_TEMPLATE.md',
111 | './.gitignore',
112 | './CHANGELOG.md',
113 | './CODE_OF_CONDUCT.md',
114 | './CONTRIBUTING.md',
115 | './LICENSE',
116 | './README.rst',
117 | './TROUBLESHOOTING.md',
118 | './USAGE.md',
119 | './VERSION.txt',
120 | ]
121 |
122 | self.file_not_found_message = 'File "{0}" does not exist in repo!'
123 |
124 | def test_repository_files_exists(self):
125 |
126 | for file_path in self.required_files:
127 | if isinstance(file_path, list):
128 | # multiple file paths: assert that any one of the files exists
129 | self.assertTrue(
130 | any(os.path.exists(f) for f in file_path),
131 | msg=self.file_not_found_message.format(
132 | '" or "'.join(file_path)
133 | ),
134 | )
135 | else:
136 | self.assertTrue(
137 | os.path.exists(file_path),
138 | msg=self.file_not_found_message.format(file_path),
139 | )
140 |
141 |
142 | if __name__ == '__main__':
143 | unittest.main()
144 |
--------------------------------------------------------------------------------
/test/requirements.txt:
--------------------------------------------------------------------------------
1 | sendgrid
2 | coverage
3 | flake8
4 |
--------------------------------------------------------------------------------
/test/test_project.py:
--------------------------------------------------------------------------------
1 | import os
2 | import unittest
3 |
4 |
5 | class ProjectTests(unittest.TestCase):
6 |
7 | # ./Docker or docker/Docker
8 | def test_docker_dir(self):
9 | self.assertTrue(
10 | os.path.isfile("./Dockerfile")
11 | or os.path.isdir("./docker/Dockerfile")
12 | )
13 |
14 | # ./docker-compose.yml or ./docker/docker-compose.yml
15 | def test_docker_compose(self):
16 | self.assertTrue(
17 | os.path.isfile('./docker-compose.yml')
18 | or os.path.isfile('./docker/docker-compose.yml')
19 | )
20 |
21 | # ./.env_sample
22 | def test_env(self):
23 | self.assertTrue(os.path.isfile('./.env_sample'))
24 |
25 | # ./.gitignore
26 | def test_gitignore(self):
27 | self.assertTrue(os.path.isfile('./.gitignore'))
28 |
29 | # ./CHANGELOG.md
30 | def test_changelog(self):
31 | self.assertTrue(os.path.isfile('./CHANGELOG.md'))
32 |
33 | # ./CODE_OF_CONDUCT.md
34 | def test_code_of_conduct(self):
35 | self.assertTrue(os.path.isfile('./CODE_OF_CONDUCT.md'))
36 |
37 | # ./CONTRIBUTING.md
38 | def test_contributing(self):
39 | self.assertTrue(os.path.isfile('./CONTRIBUTING.md'))
40 |
41 | # ./LICENSE
42 | def test_license(self):
43 | self.assertTrue(os.path.isfile('./LICENSE'))
44 |
45 | # ./PULL_REQUEST_TEMPLATE.md
46 | def test_pr_template(self):
47 | self.assertTrue(
48 | os.path.isfile('./PULL_REQUEST_TEMPLATE.md')
49 | )
50 |
51 | # ./README.rst
52 | def test_readme(self):
53 | self.assertTrue(os.path.isfile('./README.rst'))
54 |
55 | # ./TROUBLESHOOTING.md
56 | def test_troubleshooting(self):
57 | self.assertTrue(os.path.isfile('./TROUBLESHOOTING.md'))
58 |
59 | # ./USAGE.md
60 | def test_usage(self):
61 | self.assertTrue(os.path.isfile('./USAGE.md'))
62 |
63 | # ./VERSION.txt
64 | def test_use_cases(self):
65 | self.assertTrue(os.path.isfile('./VERSION.txt'))
66 |
67 |
68 | if __name__ == '__main__':
69 | unittest.main()
70 |
--------------------------------------------------------------------------------
/twilio_sendgrid_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sendgrid/smtpapi-python/e89ee24d62cfc57608369836b0f8fc9a1daddeb3/twilio_sendgrid_logo.png
--------------------------------------------------------------------------------
/use_cases/README.md:
--------------------------------------------------------------------------------
1 | This directory provides examples for specific use cases. Please [open an issue](https://github.com/sendgrid/smtpapi-python/issues) or make a pull request for any use cases you would like to see here. Thank you!
2 |
3 | # Table of Contents
4 |
--------------------------------------------------------------------------------