Skip to content

Commit

Permalink
feat: Include Purge URL endpoint
Browse files Browse the repository at this point in the history
It is most common way to purge content from Azion CDN.
This purge method expects a list of URLs to be purged and
returns a multi status response, a dictionary of what went fine and
wrong in the response body.
  • Loading branch information
mauricioabreu committed Jun 9, 2018
1 parent ed6dabd commit bf462d2
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 0 deletions.
17 changes: 17 additions & 0 deletions azion/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from azion.models import (
Configuration, Token, as_boolean,
decode_json, filter_none, instance_from_data, many_of)
from azion.responses import handle_multi_status


class AuthToken(requests.auth.AuthBase):
Expand Down Expand Up @@ -260,3 +261,19 @@ def replace_configuration(self, configuration_id, name=None,
response = self.session.put(url, json=filter_none(data))
json = decode_json(response, 200)
return instance_from_data(Configuration, json)

def purge_url(self, urls, method='delete'):
"""Purge content of the given URLs inside
the `urls` list.
:param list urls:
List of URLs to be purged.
:param str method:
How the content will be purged.
Default to 'delete'.
"""
url = self.session.build_url('purge', 'url')
response = self.session.post(
url, json={'urls': urls, 'method': method})
data = decode_json(response, 207)
return handle_multi_status(data, 'urls')
54 changes: 54 additions & 0 deletions azion/responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
class MultiStatus(dict):
"""A container acting like a dict to
save responses with multiple status. Since multi-status proposal
recommends to keep the status as a identifier, we are using a dict
to group responses by status."""

def succeed(self):
"""Filter succeed purges.
:return: succeed respones.
:rtype: dict
"""
return {201: self[201]}

def failed(self):
"""Filter failed purges.
:return: failed responses.
:rtype: dict
"""
return {status: response for status, response in
self.items() if status >= 400}


def handle_multi_status(response, field):
"""Handle multi-status response.
A multi-status response conveys information about multiple resources
in situations where multiple status codes might be appropriate.
:param dict response:
A data structure containing a key `status` referencing the
HTTP status code.
:param str field:
Which field is related to the error, essential value to know
what went right and wrong.
"""
responses = MultiStatus()
for item in response:
status = parse_status_code(item['status'])
responses.update({
status: {
'details': item['details'],
field: item[field]
}
})
return responses


def parse_status_code(verbose_status):
"""Parse a HTTP status code like `HTTP/1.1 201 CREATED`
and return only 201 instead."""
pieces = verbose_status.split(' ')
return int(pieces[1])
97 changes: 97 additions & 0 deletions tests/integration/cassettes/Purge_url.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
{
"http_interactions": [
{
"recorded_at": "2018-06-09T17:47:25",
"request": {
"body": {
"encoding": "utf-8",
"string": "{\"urls\": [\"www.maugzoide.com/foo.jgp\", \"www.maugzoide.com/bar.jgp\", \"www.notauthorize.com/mistaken.jgp\"], \"method\": \"delete\"}"
},
"headers": {
"Accept": [
"application/json; version=1"
],
"Accept-Charset": [
"utf-8"
],
"Accept-Encoding": [
"gzip, deflate"
],
"Authorization": [
"token <AUTH_TOKEN>"
],
"Connection": [
"keep-alive"
],
"Content-Length": [
"125"
],
"Content-Type": [
"application/json"
],
"User-Agent": [
"python-requests/2.18.4"
]
},
"method": "POST",
"uri": "https://api.azion.net/purge/url"
},
"response": {
"body": {
"encoding": null,
"string": "[{\"status\":\"HTTP/1.1 403 FORBIDDEN\",\"urls\":[\"www.notauthorize.com/mistaken.jgp\"],\"details\":\"Unauthorized domain for your account\"},{\"status\":\"HTTP/1.1 201 CREATED\",\"urls\":[\"www.maugzoide.com/bar.jgp\",\"www.maugzoide.com/foo.jgp\"],\"details\":\"Purge request successfully created\"}]"
},
"headers": {
"Allow": [
"POST, OPTIONS"
],
"Connection": [
"keep-alive"
],
"Content-Language": [
"en"
],
"Content-Length": [
"277"
],
"Content-Type": [
"application/json"
],
"Date": [
"Sat, 09 Jun 2018 17:47:25 GMT"
],
"Server": [
"azion webserver"
],
"Vary": [
"Accept-Language, Cookie"
],
"X-Frame-Options": [
"SAMEORIGIN"
],
"X-RateLimit-Limit": [
"200"
],
"X-RateLimit-Remaining": [
"199"
],
"X-RateLimit-Reset": [
"2018-06-09T17:48:25.395136"
],
"x-content-type-options": [
"nosniff"
],
"x-xss-protection": [
"1; mode=block"
]
},
"status": {
"code": 207,
"message": "MULTI-STATUS"
},
"url": "https://api.azion.net/purge/url"
}
}
],
"recorded_with": "betamax/0.8.1"
}
24 changes: 24 additions & 0 deletions tests/integration/test_client.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import itertools
import os

from azion.client import Azion
Expand Down Expand Up @@ -81,3 +82,26 @@ def test_delete_configuration(self):

with recorder.use_cassette('Configuration_delete'):
assert client.delete_configuration(1528252734)


class TestPurge(object):

def test_purge_url(self):
client = Azion(token)
recorder = betamax.Betamax(client.session)

authorized_urls = [
'www.maugzoide.com/foo.jgp', 'www.maugzoide.com/bar.jgp']
forbidden_urls = ['www.notauthorize.com/mistaken.jgp']
urls = authorized_urls + forbidden_urls

with recorder.use_cassette('Purge_url'):
purge = client.purge_url(urls)

succeed_urls = itertools.chain(
*[response['urls'] for response in purge.succeed().values()])
assert sorted(authorized_urls) == sorted(list(succeed_urls))

failed_urls = itertools.chain(
*[response['urls'] for response in purge.failed().values()])
assert sorted(forbidden_urls) == sorted(list(failed_urls))
29 changes: 29 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,32 @@ def test_replace_configuration(self):
'active': False
}
)

@mock.patch('azion.client.handle_multi_status')
def test_purge_url(self, mock_handler):
mocked_session = create_mocked_session()
client = Azion(session=mocked_session)
mock_handler.return_value = [
{
"status": "HTTP/1.1 201 CREATED",
"urls": [
"http://www.domain.com/",
"http://www.domain.com/test.js"
],
"details": "Purge request successfully created"
}
]

# URLs to be purged
urls = [
'www.domain.com/',
'www.domain.com/test.js'
]
assert client.purge_url(urls, 'delete')
mocked_session.post.assert_called_once_with(
'https://api.azion.net/purge/url',
json={
'urls': urls,
'method': 'delete'
}
)
28 changes: 28 additions & 0 deletions tests/test_responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from azion.responses import handle_multi_status


def test_handle_multi_status():
# Response taken directly from API documentation
# https://www.azion.com.br/developers/api-v1/real-time-purge/
response = [
{
"status": "HTTP/1.1 201 CREATED",
"urls": [
"http://www.domain.com/",
"http://www.domain.com/test.js"
],
"details": "Purge request successfully created"
},
{
"status": "HTTP/1.1 403 FORBIDDEN",
"urls": ["http://static.mistaken-domain.com/image1.jpg"],
"details": "Unauthorized domain for your account"
}
]

responses = handle_multi_status(response, 'urls')
assert 201 in responses
assert responses[201]['details'] == 'Purge request successfully created'
assert responses[201]['urls'] == ['http://www.domain.com/', 'http://www.domain.com/test.js']
assert responses.succeed()
assert responses.failed()

0 comments on commit bf462d2

Please sign in to comment.