Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/3799 two tiered api #2352

Open
wants to merge 26 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion doajtest/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,14 @@ def create_app_patch(cls):
'ENABLE_EMAIL': False,
"FAKER_SEED": 1,
"EVENT_SEND_FUNCTION": "portality.events.shortcircuit.send_event",
'CMS_BUILD_ASSETS_ON_STARTUP': False
'CMS_BUILD_ASSETS_ON_STARTUP': False,

# disable send plausible request
'PLAUSIBLE_URL': '',
'PLAUSIBLE_API_URL': '',

'RATE_LIMITS_PER_MIN_DEFAULT': 1000000,
'RATE_LIMITS_PER_MIN_T2 ': 1000000,
}

@classmethod
Expand Down
80 changes: 80 additions & 0 deletions doajtest/unit/api_tests/test_api_rate_limited.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import json

from doajtest import fixtures
from doajtest.helpers import DoajTestCase, patch_config
from portality.app import setup_dev_log
from portality.bll import DOAJ
from portality.core import app
from portality.models import Account

api_rate_serv = DOAJ.apiRateService()


def send_test_req(client, api_key):
url = '/api/search/applications/issn%3A0000-0000?api_key=' + api_key
return client.get(url)


def assert_is_too_many_requests(resp):
data = json.loads(resp.data)
assert data['status'] == 'too_many_requests'
assert resp.status_code == 429


def assert_not_too_many_requests(resp):
data = json.loads(resp.data)
assert data['query']
assert resp.status_code != 429


class TestApiRateLimited(DoajTestCase):

@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.originals = patch_config(
app,
{
'RATE_LIMITS_PER_MIN_DEFAULT': 10,
'RATE_LIMITS_PER_MIN_T2 ': 1000,
})
setup_dev_log()

def setUp(self):
super().setUp()
self.t2_user = Account(**fixtures.accounts.PUBLISHER_SOURCE)
self.t2_user.generate_api_key()
self.t2_user.add_role('api_rate_t2')

self.normal_user = Account(**fixtures.accounts.PUBLISHER_B_SOURCE)
self.normal_user.generate_api_key()

Account.save_all([self.normal_user, self.t2_user])
Account.refresh()

def send_multi_req_more_than_default_limit(self, api_key=None):
with self.app_test.test_client() as t_client:
for _ in range(app.config['RATE_LIMITS_PER_MIN_DEFAULT'] + 5):
resp = send_test_req(t_client, api_key=api_key)
return resp

def send_one_req(self, api_key=None):
with self.app_test.test_client() as t_client:
resp = send_test_req(t_client, api_key=api_key)
return resp

def test_normal__normal_user(self):
resp = self.send_one_req(api_key=self.normal_user.api_key)
assert_not_too_many_requests(resp)

def test_normal__t2_user(self):
resp = self.send_one_req(api_key=self.t2_user.api_key)
assert_not_too_many_requests(resp)

def test_multi_req__too_many_requests__normal_user(self):
resp = self.send_multi_req_more_than_default_limit(api_key=self.normal_user.api_key)
assert_is_too_many_requests(resp)

def test_multi_req__normal__t2_user(self):
resp = self.send_multi_req_more_than_default_limit(api_key=self.t2_user.api_key)
assert_not_too_many_requests(resp)
21 changes: 11 additions & 10 deletions docs/dictionary.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
| Short | Description |
|---------|------------------------------|
| bgjob | background job |
| noti | notification |
| noqa | NO-QA (NO Quality Assurance) |
| inst | instance |
| fmt | format |
| exparam | extra parameter |
| maned | Managing Editor |
| gsheet | Google Sheet |
| Short | Description |
|----------|------------------------------|
| bgjob | background job |
| noti | notification |
| noqa | NO-QA (NO Quality Assurance) |
| inst | instance |
| fmt | format |
| exparam | extra parameter |
| maned | Managing Editor |
| gsheet | Google Sheet |
| serv | service |
47 changes: 37 additions & 10 deletions portality/api/common.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#~~API:Feature~~
import json, uuid
from portality.core import app
from flask import request
# ~~API:Feature~~
import json
import uuid
from copy import deepcopy

from flask import request
from link_header import LinkHeader, Link

from portality.core import app

LINK_HEADERS = ['next', 'prev', 'last']
TOTAL_RESULTS_COUNT = ['total']

Expand All @@ -16,17 +19,17 @@ class Api(object):
# ~~->Swagger:Feature~~
# ~~->API:Documentation~~
SWAG_TEMPLATE = {
"description" : "",
"description": "",
"responses": {},
"parameters": [],
"tags": []
}
R200 = {"schema": {}, "description": "A successful request/response"}
R201 = {"schema": {"properties": CREATED_TEMPLATE}, "description": "Resource created successfully, response "
"contains the new resource ID and location."}
R201_BULK = {"schema": {"items": {"properties" : CREATED_TEMPLATE, "type" : "object"}, "type" : "array"},
"description": "Resources created successfully, response contains the new resource IDs "
"and locations."}
"contains the new resource ID and location."}
R201_BULK = {"schema": {"items": {"properties": CREATED_TEMPLATE, "type": "object"}, "type": "array"},
"description": "Resources created successfully, response contains the new resource IDs "
"and locations."}
R204 = {"description": "OK (Request succeeded), No Content"}
R400 = {"schema": {"properties": ERROR_TEMPLATE}, "description": "Bad Request. Your request body was missing a "
"required field, or the data in one of the "
Expand Down Expand Up @@ -125,6 +128,13 @@ class Api409Error(Exception):
pass


class Api429Error(Exception):
"""
Too many requests
"""
pass


class Api500Error(Exception):
pass

Expand Down Expand Up @@ -201,7 +211,7 @@ def generate_link_headers(metadata):
links.append(Link(v, rel=k)) # e.g. Link("https://example.com/foo", rel="next")

return str(LinkHeader(links)) # RFC compliant headers e.g.
# <https://example.com/foo>; rel=next, <https://example.com/bar>; rel=last
# <https://example.com/foo>; rel=next, <https://example.com/bar>; rel=last


def respond(data, status, metadata=None):
Expand All @@ -226,6 +236,16 @@ def respond(data, status, metadata=None):
return app.response_class(data, status, headers, mimetype='application/json')


def resp_err(error, log_msg, status_code, status_msg):
err_ref_id = uuid.uuid1()
err_msg = str(error) + " (ref: {y})".format(y=err_ref_id)
app.logger.info(log_msg + f' -- {err_msg}')
t = deepcopy(ERROR_TEMPLATE)
t['status'] = status_msg
t['error'] = err_msg
return respond(json.dumps(t), status_code)


@app.errorhandler(Api400Error)
def bad_request(error):
magic = uuid.uuid1()
Expand Down Expand Up @@ -266,6 +286,13 @@ def forbidden(error):
return respond(json.dumps(t), 403)


@app.errorhandler(Api429Error)
def too_many_requests(error):
return resp_err(error,
f"Sending 429 Too Many Requests from client",
429, 'too_many_requests')


@app.errorhandler(Api500Error)
def bad_request(error):
magic = uuid.uuid1()
Expand Down
Loading
Loading