Skip to content

Commit

Permalink
Reformat code with automated tools
Browse files Browse the repository at this point in the history
Apply standardized code style
  • Loading branch information
medmunds committed Feb 6, 2023
1 parent 40891fc commit b4e22c6
Show file tree
Hide file tree
Showing 94 changed files with 13,001 additions and 7,508 deletions.
18 changes: 9 additions & 9 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@ We welcome all contributions: issue reports, bug fixes, documentation improvemen
new features, ideas and suggestions, and anything else to help improve the package.

Before posting **questions** or **issues** in GitHub, please check out
[*Getting support*][support] and [*Troubleshooting*][troubleshooting]
[_Getting support_][support] and [_Troubleshooting_][troubleshooting]
in the Anymail docs. Also…

> …when you're reporting a problem or bug, it's *really helpful* to include:
> * which **ESP** you're using (Mailgun, SendGrid, etc.)
> * what **versions** of Anymail, Django, and Python you're running
> * any error messages and exception stack traces
> * the relevant portions of your code and settings
> * any [troubleshooting] you've tried
> …when you're reporting a problem or bug, it's _really helpful_ to include:
>
> - which **ESP** you're using (Mailgun, SendGrid, etc.)
> - what **versions** of Anymail, Django, and Python you're running
> - any error messages and exception stack traces
> - the relevant portions of your code and settings
> - any [troubleshooting] you've tried
For more info on **pull requests** and the **development** environment,
please see [*Contributing*][contributing] in the docs. For significant
please see [_Contributing_][contributing] in the docs. For significant
new features or breaking changes, it's always a good idea to
propose the idea in the [discussions] forum before writing
a lot of code.


[contributing]: https://anymail.readthedocs.io/en/stable/contributing/
[discussions]: https://github.com/anymail/django-anymail/discussions
[support]: https://anymail.readthedocs.io/en/stable/help/#support
Expand Down
12 changes: 6 additions & 6 deletions .github/ISSUE_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
Reporting an error? It's helpful to know:

* Anymail version
* ESP (Mailgun, SendGrid, etc.)
* Your ANYMAIL settings (change secrets to "redacted")
* Versions of Django, requests, python
* Exact error message and/or stack trace
* Any other relevant code and settings (e.g., for problems
- Anymail version
- ESP (Mailgun, SendGrid, etc.)
- Your ANYMAIL settings (change secrets to "redacted")
- Versions of Django, requests, python
- Exact error message and/or stack trace
- Any other relevant code and settings (e.g., for problems
sending, your code that sends the message)
5 changes: 2 additions & 3 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ name: integration-test
on:
pull_request:
push:
branches: [ "main", "v[0-9]*" ]
tags: [ "v[0-9]*" ]
branches: ["main", "v[0-9]*"]
tags: ["v[0-9]*"]
workflow_dispatch:
schedule:
# Weekly test (on branch main) every Thursday at 12:15 UTC.
# (Used to monitor compatibility with ESP API changes.)
- cron: "15 12 * * 4"


jobs:
skip_duplicate_runs:
# Avoid running the live integration tests twice on the same code
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ name: release

on:
push:
tags: [ "v[0-9]*" ]
tags: ["v[0-9]*"]
workflow_dispatch:

jobs:
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ name: test
on:
pull_request:
push:
branches: [ "main", "v[0-9]*" ]
tags: [ "v[0-9]*" ]
branches: ["main", "v[0-9]*"]
tags: ["v[0-9]*"]
workflow_dispatch:
schedule:
# Weekly test (on branch main) every Thursday at 12:00 UTC.
# (Used to monitor compatibility with Django patches/dev and other dependencies.)
- cron: "0 12 * * 4"


jobs:
get-envlist:
runs-on: ubuntu-20.04
Expand Down
134 changes: 62 additions & 72 deletions ADDING_ESPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,23 @@ for you.
This document adds general background and covers some design
decisions that aren't necessarily obvious from the code.


## Getting started

* Don't want to do *all* of this? **That's OK!** A partial PR
- Don't want to do _all_ of this? **That's OK!** A partial PR
is better than no PR. And opening a work-in-progress PR
early is a really good idea.
* Don't want to do *any* of this? Use GitHub issues to request
- Don't want to do _any_ of this? Use GitHub issues to request
support for other ESPs in Anymail.
* It's often easiest to copy and modify the existing code
for an ESP with a similar API. There are some hints in each
- It's often easiest to copy and modify the existing code
for an ESP with a similar API. There are some hints in each
section below what might be "similar".


### Which ESPs?

Anymail is best suited to *transactional* ESP APIs. The Django core
mail package it builds on isn't a good match for most *bulk* mail APIs.
Anymail is best suited to _transactional_ ESP APIs. The Django core
mail package it builds on isn't a good match for most _bulk_ mail APIs.
(If you can't specify an individual recipient email address and at
least some of the message content, it's probably not a transactional API.)
least some of the message content, it's probably not a transactional API.)

Similarly, Anymail is best suited to ESPs that offer some value-added
features beyond simply sending email. If you'd get exactly the same
Expand All @@ -38,8 +36,7 @@ SMTP endpoint, there's really no need to add it to Anymail.
We strongly prefer ESPs where we'll be able to run live integration
tests regularly. That requires the ESP have a free tier (testing is
extremely low volume), a sandbox API, or that they offer developer
accounts for open source projects like Anymail.

accounts for open source projects like Anymail.

## EmailBackend and payload

Expand All @@ -48,77 +45,73 @@ your code should be able to focus on the ESP-specific parts.

You'll subclass a backend and a payload for your ESP implementation:

* Backend (subclass `AnymailBaseBackend` or `AnymailRequestsBackend`):
- Backend (subclass `AnymailBaseBackend` or `AnymailRequestsBackend`):
implements Django's email API, orchestrates the overall sending process
for multiple messages.
* Payload (subclass `BasePayload` or `RequestsPayload`)

- Payload (subclass `BasePayload` or `RequestsPayload`)
implements conversion of a single Django `EmailMessage` to parameters
for the ESP API.

Whether you start from the base or requests version depends on whether
Whether you start from the base or requests version depends on whether
you'll be using an ESP client library or calling their HTTP API directly.


### Client lib or HTTP API?

Which to pick? It's a bit of a judgement call:

* Often, ESP Python client libraries don't seem to be actively maintained.
- Often, ESP Python client libraries don't seem to be actively maintained.
Definitely avoid those.

* Some client libraries are just thin wrappers around Requests (or urllib).
- Some client libraries are just thin wrappers around Requests (or urllib).
There's little value in using those, and you'll lose some optimizations
built into `AnymailRequestsBackend`.

* Surprisingly often, client libraries (unintentionally) impose limitations
- Surprisingly often, client libraries (unintentionally) impose limitations
that are more restrictive than than (or conflict with) the underlying ESP API.
You should report those bugs against the library. (Or if they were already
reported a long time ago, see the first point above.)

* Some ESP APIs have really complex (or obscure) payload formats,
- Some ESP APIs have really complex (or obscure) payload formats,
or authorization schemes that are non-trivial to implement in Requests.
If the client library handles this for you, it's a better choice.
If the client library handles this for you, it's a better choice.

When in doubt, it's usually fine to use `AnymailRequestsBackend` and write
directly to the HTTP API.


### Requests backend (using HTTP API)

Good staring points for similar ESP APIs:
* JSON payload: Postmark
* Form-encoded payload: Mailgun

- JSON payload: Postmark
- Form-encoded payload: Mailgun

Different API endpoints for (e.g.,) template vs. regular send?
Implement `get_api_endpoint()` in your Payload.
Implement `get_api_endpoint()` in your Payload.

Need to encode JSON in the payload? Use `self.serialize_json()`
(it has some extra error handling).

Need to parse JSON in the API response? Use `self.deserialize_json_response()`
(same reason).


### Base backend (using client lib)

Good starting points: Test backend; SparkPost

Don't forget add an `'extras_require'` entry for your ESP in setup.py.
Don't forget add an `'extras_require'` entry for your ESP in setup.py.
Also update `'tests_require'`.

If the client lib supports the notion of a reusable API "connection"
(or session), you should override `open()` and `close()` to provide
API state caching. See the notes in the base implementation.
(The RequestsBackend implements this using Requests sessions.)


### Payloads

Look for the "Abstract implementation" comment in `base.BasePayload`.
Your payload should consider implementing everything below there.


#### Email addresses

All payload methods dealing with email addresses (recipients, from, etc.) are
Expand All @@ -129,10 +122,10 @@ passed `anymail.utils.EmailAddress` objects, so you don't have to parse them.

For recipients, you can implement whichever of these Payload methods
is most convenient for the ESP API:
* `set_to(emails)`, `set_cc(emails)`, and `set_bcc(emails)`
* `set_recipients(type, emails)`
* `add_recipient(type, email)`

- `set_to(emails)`, `set_cc(emails)`, and `set_bcc(emails)`
- `set_recipients(type, emails)`
- `add_recipient(type, email)`

#### Attachments

Expand All @@ -147,101 +140,98 @@ with angle brackets, and `att.cid` is without angle brackets.

Use `att.base64content` if your ESP wants base64-encoded data.


#### AnymailUnsupportedFeature and validating parameters

Should your payload use `self.unsupported_feature()`? The rule of thumb is:

* If it *cannot be accurately communicated* to the ESP API, that's unsupported.
- If it _cannot be accurately communicated_ to the ESP API, that's unsupported.
E.g., the user provided multiple `tags` but the ESP's "Tag" parameter only accepts
a (single) string value.

* Anymail avoids enforcing ESP policies (because these tend to change over time, and
we don't want to update our code). So if it *can* be accurately communicated to the
ESP API, that's *not* unsupported---even if the ESP docs say it's not allowed.
- Anymail avoids enforcing ESP policies (because these tend to change over time, and
we don't want to update our code). So if it _can_ be accurately communicated to the
ESP API, that's _not_ unsupported---even if the ESP docs say it's not allowed.
E.g., the user provided 10 `tags`, the ESP's "Tags" parameter accepts a list,
but is documented maximum 3 tags. Anymail should pass the list of 10 tags,
and let the ESP error if it chooses.

Similarly, Anymail doesn't enforce allowed attachment types, maximum attachment size,
maximum number of recipients, etc. That's the ESP's responsibility.
maximum number of recipients, etc. That's the ESP's responsibility.

One exception: if the ESP mis-handles certain input (e.g., drops the message
but returns "success"; mangles email display names), and seems unlikely to fix the problem,
we'll typically add a warning or workaround to Anymail.
One exception: if the ESP mis-handles certain input (e.g., drops the message but
returns "success"; mangles email display names), and seems unlikely to fix the problem,
we'll typically add a warning or workaround to Anymail.
(As well as reporting the problem to the ESP.)


#### Batch send and splitting `to`

One of the more complicated Payload functions is handling multiple `to` addresses properly.
One of the more complicated Payload functions is handling multiple `to` addresses
properly.

If the user has set `merge_data`, a separate message should get sent to each `to` address,
and recipients should not see the full To list. If `merge_data` is not set, a single message
should be sent with all addresses in the To header.
If the user has set `merge_data`, a separate message should get sent to each `to`
address, and recipients should not see the full To list. If `merge_data` is not set,
a single message should be sent with all addresses in the To header.

Most backends handle this in the Payload's `serialize_data` method, by restructuring
the payload if `merge_data` is not None.


#### Tests

Every backend needs mock tests, that use a mocked API to verify the ESP is being called
correctly. It's often easiest to copy and modify the backend tests for an ESP with a similar
API.

Ideally, every backend should also have live integration tests, because sometimes the docs
don't quite match the real world. (And because ESPs have been known to change APIs without
notice.) Anymail's CI runs the live integration tests at least weekly.
correctly. It's often easiest to copy and modify the backend tests for an ESP with
a similar API.

Ideally, every backend should also have live integration tests, because sometimes the
docs don't quite match the real world. (And because ESPs have been known to change APIs
without notice.) Anymail's CI runs the live integration tests at least weekly.

## Webhooks

ESP webhook documentation is *almost always* vague on at least some aspects of the webhook
event data, and example payloads in their docs are often outdated (and/or manually constructed
and inaccurate).

Runscope (or similar) is an extremely useful tool for collecting actual webhook payloads.
ESP webhook documentation is _almost always_ vague on at least some aspects of the
webhook event data, and example payloads in their docs are often outdated (and/or
manually constructed and inaccurate).

Runscope (or similar) is an extremely useful tool for collecting actual webhook
payloads.

### Tracking webhooks

Good starting points:

* JSON event payload: SendGrid, Postmark
* Form data event payload: Mailgun
- JSON event payload: SendGrid, Postmark
- Form data event payload: Mailgun

(more to come)

### Inbound webhooks

Raw MIME vs. ESP-parsed fields? If you're given both, the raw MIME is usually easier to work with.
Raw MIME vs. ESP-parsed fields? If you're given both, the raw MIME is usually easier
to work with.

(more to come)


## Project goals

Anymail aims to:

* Normalize common transactional ESP functionality (to simplify
- Normalize common transactional ESP functionality (to simplify
switching between ESPs)

* But allow access to the full ESP feature set, through
`esp_extra` (so Anymail doesn't force users into
- But allow access to the full ESP feature set, through
`esp_extra` (so Anymail doesn't force users into
least-common-denominator functionality, or prevent use of
newly-released ESP features)

* Present a Pythonic, Djangotic API, and play well with Django
- Present a Pythonic, Djangotic API, and play well with Django
and other Django reusable apps

* Maintain compatibility with all currently supported Django versions---and
even unsupported minor versions in between (so Anymail isn't the package
- Maintain compatibility with all currently supported Django versions---and
even unsupported minor versions in between (so Anymail isn't the package
that forces you to upgrade Django---or that prevents you from upgrading
when you're ready)

Many of these goals incorporate lessons learned from Anymail's predecessor
Djrill project. And they mean that django-anymail is biased toward implementing
*relatively* thin wrappers over ESP transactional sending, tracking, and receiving APIs.
Many of these goals incorporate lessons learned from Anymail's predecessor
Djrill project. And they mean that django-anymail is biased toward implementing
_relatively_ thin wrappers over ESP transactional sending, tracking, and receiving APIs.
Anything that would add Django models to Anymail is probably out of scope.
(But could be a great companion package.)
6 changes: 3 additions & 3 deletions anymail/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Expose package version at root of package
from ._version import __version__, VERSION # NOQA: F401
from django import VERSION as DJANGO_VERSION

from ._version import VERSION, __version__ # NOQA: F401

from django import VERSION as DJANGO_VERSION
if DJANGO_VERSION < (3, 2, 0):
default_app_config = 'anymail.apps.AnymailBaseConfig'
default_app_config = "anymail.apps.AnymailBaseConfig"
Loading

0 comments on commit b4e22c6

Please sign in to comment.