diff --git a/CHANGELOG.md b/CHANGELOG.md index f92cf92..644f370 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ ## Current (in progress) - Add endpoint to stream a CSV response [#5](https://github.com/etalab/api-tabular/pull/5) +- Make URL in links absolute [#7](https://github.com/etalab/api-tabular/pull/7) diff --git a/api_tabular/app.py b/api_tabular/app.py index 7557729..f9f1a5d 100644 --- a/api_tabular/app.py +++ b/api_tabular/app.py @@ -10,7 +10,7 @@ get_resource_data, get_resource_data_streamed, ) -from api_tabular.utils import build_sql_query_string, build_link_with_page +from api_tabular.utils import build_sql_query_string, build_link_with_page, url_for from api_tabular.error import QueryException routes = web.RouteTableDef() @@ -22,7 +22,7 @@ ) -@routes.get(r"/api/resources/{rid}/") +@routes.get(r"/api/resources/{rid}/", name="meta") async def resource_meta(request): resource_id = request.match_info["rid"] resource = await get_resource( @@ -34,12 +34,12 @@ async def resource_meta(request): "url": resource["url"], "links": [ { - "href": f"/api/resources/{resource_id}/profile/", + "href": url_for(request, 'profile', rid=resource_id, _external=True), "type": "GET", "rel": "profile", }, { - "href": f"/api/resources/{resource_id}/data/", + "href": url_for(request, 'data', rid=resource_id, _external=True), "type": "GET", "rel": "data", }, @@ -48,7 +48,7 @@ async def resource_meta(request): ) -@routes.get(r"/api/resources/{rid}/profile/") +@routes.get(r"/api/resources/{rid}/profile/", name="profile") async def resource_profile(request): resource_id = request.match_info["rid"] resource = await get_resource( @@ -57,7 +57,7 @@ async def resource_profile(request): return web.json_response(resource) -@routes.get(r"/api/resources/{rid}/data/") +@routes.get(r"/api/resources/{rid}/data/", name="data") async def resource_data(request): resource_id = request.match_info["rid"] query_string = request.query_string.split("&") if request.query_string else [] @@ -85,12 +85,12 @@ async def resource_data(request): request.app["csession"], resource, sql_query ) - next = build_link_with_page(request.path, query_string, page + 1, page_size) - prev = build_link_with_page(request.path, query_string, page - 1, page_size) + next = build_link_with_page(request, query_string, page + 1, page_size) + prev = build_link_with_page(request, query_string, page - 1, page_size) body = { "data": response, "links": { - "profile": f"/api/resources/{resource_id}/profile/", + "profile": url_for(request, 'profile', rid=resource_id, _external=True), "next": next if page_size + offset < total else None, "prev": prev if page > 1 else None, }, @@ -99,7 +99,7 @@ async def resource_data(request): return web.json_response(body) -@routes.get(r"/api/resources/{rid}/data/csv/") +@routes.get(r"/api/resources/{rid}/data/csv/", name="csv") async def resource_data_csv(request): resource_id = request.match_info["rid"] query_string = request.query_string.split("&") if request.query_string else [] diff --git a/api_tabular/metrics.py b/api_tabular/metrics.py index d7a377a..638e904 100644 --- a/api_tabular/metrics.py +++ b/api_tabular/metrics.py @@ -67,8 +67,8 @@ async def metrics_data(request): response, total = await get_object_data(request.app["csession"], model, sql_query) - next = build_link_with_page(request.path, query_string, page + 1, page_size) - prev = build_link_with_page(request.path, query_string, page - 1, page_size) + next = build_link_with_page(request, query_string, page + 1, page_size) + prev = build_link_with_page(request, query_string, page - 1, page_size) body = { "data": response, "links": { diff --git a/api_tabular/utils.py b/api_tabular/utils.py index b54ddc5..e0a87b4 100644 --- a/api_tabular/utils.py +++ b/api_tabular/utils.py @@ -1,3 +1,6 @@ +from aiohttp.web_request import Request + + def build_sql_query_string( request_arg: list, page_size: int = None, offset: int = 0 ) -> str: @@ -33,8 +36,15 @@ def process_total(raw_total: str) -> int: return int(str_total) -def build_link_with_page(path, query_string, page, page_size): +def build_link_with_page(request: Request, query_string: str, page: int, page_size: int): q = [string for string in query_string if not string.startswith("page")] q.extend([f"page={page}", f"page_size={page_size}"]) rebuilt_q = "&".join(q) - return f"{path}?{rebuilt_q}" + return f"{request.scheme}://{request.host}{request.path}?{rebuilt_q}" + + +def url_for(request: Request, route: str, *args, **kwargs): + router = request.app.router + if kwargs.pop("_external", None): + return f"{request.scheme}://{request.host}{router[route].url_for(**kwargs)}" + return router[route].url_for(**kwargs) diff --git a/tests/test_api.py b/tests/test_api.py index 9989df5..0e92c07 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -22,12 +22,12 @@ async def test_api_resource_meta(client, rmock): "url": "https://example.com", "links": [ { - "href": f"/api/resources/{RESOURCE_ID}/profile/", + "href": f"http://127.0.0.1:{client.port}/api/resources/{RESOURCE_ID}/profile/", "type": "GET", "rel": "profile", }, { - "href": f"/api/resources/{RESOURCE_ID}/data/", + "href": f"http://127.0.0.1:{client.port}/api/resources/{RESOURCE_ID}/data/", "type": "GET", "rel": "data", }, @@ -66,7 +66,7 @@ async def test_api_resource_data(client, rmock): "links": { "next": None, "prev": None, - "profile": "/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/profile/", + "profile": f"http://127.0.0.1:{client.port}/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/profile/", }, "meta": {"page": 1, "page_size": 20, "total": 10}, } @@ -88,7 +88,7 @@ async def test_api_resource_data_with_args(client, rmock): "links": { "next": None, "prev": None, - "profile": "/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/profile/", + "profile": f"http://127.0.0.1:{client.port}/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/profile/", }, "meta": {"page": 1, "page_size": 20, "total": 10}, } @@ -110,7 +110,7 @@ async def test_api_resource_data_with_args_case(client, rmock): "links": { "next": None, "prev": None, - "profile": "/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/profile/", + "profile": f"http://127.0.0.1:{client.port}/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/profile/", }, "meta": {"page": 1, "page_size": 20, "total": 10}, } @@ -183,7 +183,7 @@ async def test_api_percent_encoding_arabic(client, rmock): "links": { "next": None, "prev": None, - "profile": "/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/profile/", + "profile": f"http://127.0.0.1:{client.port}/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/profile/", }, "meta": {"page": 1, "page_size": 20, "total": 10}, } @@ -205,7 +205,7 @@ async def test_api_with_unsupported_args(client, rmock): "links": { "next": None, "prev": None, - "profile": "/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/profile/", + "profile": f"http://127.0.0.1:{client.port}/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/profile/", }, "meta": {"page": 1, "page_size": 20, "total": 10}, } @@ -225,9 +225,10 @@ async def test_api_pagination(client, rmock): body = { "data": [{"such": "data"}], "links": { - "next": "/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/data/?page=2&page_size=1", + "next": f"http://127.0.0.1:{client.port}" + "/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/data/?page=2&page_size=1", "prev": None, - "profile": "/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/profile/", + "profile": f"http://127.0.0.1:{client.port}/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/profile/", }, "meta": {"page": 1, "page_size": 1, "total": 2}, } @@ -246,8 +247,9 @@ async def test_api_pagination(client, rmock): "data": [{"such": "data"}], "links": { "next": None, - "prev": "/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/data/?page=1&page_size=1", - "profile": "/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/profile/", + "prev": f"http://127.0.0.1:{client.port}" + "/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/data/?page=1&page_size=1", + "profile": f"http://127.0.0.1:{client.port}/api/resources/60963939-6ada-46bc-9a29-b288b16d969b/profile/", }, "meta": {"page": 2, "page_size": 1, "total": 2}, } diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..6cf332a --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,24 @@ + +from aiohttp.test_utils import make_mocked_request + +from api_tabular.utils import build_link_with_page, url_for + + +def test_build_link_with_page(): + request = make_mocked_request("GET", "/api/test?foo=bar") + link = build_link_with_page(request, query_string=["foo=1", "bar=3"], page=2, page_size=10) + assert link == f"{request.scheme}://{request.host}/api/test?foo=1&bar=3&page=2&page_size=10" + + +def test_url_for(client): + request = make_mocked_request("GET", "/api/test?foo=bar") + request.app.router = client.app.router + url = url_for(request, 'profile', rid='rid') + assert str(url) == '/api/resources/rid/profile/' + + +def test_url_for_external(client): + request = make_mocked_request("GET", "/api/test?foo=bar") + request.app.router = client.app.router + url = url_for(request, 'profile', rid='rid', _external=True) + assert str(url) == f'{request.scheme}://{request.host}/api/resources/rid/profile/'