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

add changes #6

Merged
merged 6 commits into from
Feb 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ __pypackages__/
celerybeat-schedule
celerybeat.pid


# SageMath parsed files
*.sage.py

Expand All @@ -92,3 +93,6 @@ dmypy.json

# pytype static type analyzer
.pytype/


junit_report.xml
41 changes: 21 additions & 20 deletions ted_sws/notice_fetcher/adapters/ted_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
import json
from datetime import date
from typing import List

import requests as requests

from ted_sws.notice_fetcher.adapters.ted_api_abc import DocumentSearchABC
import requests
from ted_sws.notice_fetcher.adapters.ted_api_abc import DocumentSearchABC, RequestAPI

DEFAULT_TED_API_URL = "https://ted.europa.eu/api/v2.0/notices/search"
DEFAULT_TED_API_QUERY = {"pageSize": 100,
Expand All @@ -14,33 +12,36 @@
"fields": ["AA", "AC", "CY", "DD", "DI", "DS", "DT", "MA", "NC", "ND", "OC", "OJ", "OL", "OY",
"PC", "PD", "PR", "RC", "RN", "RP", "TD", "TVH", "TVL", "TY", "CONTENT"]}

class TedRequestAPI(RequestAPI):

def api_request_with_query(api_url: str, api_query: dict) -> dict:
"""
Method to make a post request to the API with a query (json). It will return the response body.
:param api_url:
:param api_query:
:return: dict
"""
def __call__(self, api_url: str, api_query: dict) -> dict:
"""
Method to make a post request to the API with a query (json). It will return the response body.
:param api_url:
:param api_query:
:return: dict
"""

response = requests.post(api_url, json=api_query)
if response.ok:
response_content = json.loads(response.text)
return response_content
else:
raise Exception(f"The API call failed with: {response}")
response = requests.post(api_url, json=api_query)
if response.ok:
response_content = json.loads(response.text)
return response_content
else:
raise Exception(f"The API call failed with: {response}")


class TedDocumentSearch(DocumentSearchABC):
"""
This class will fetch documents content
"""

def __init__(self, ted_api_url: str = DEFAULT_TED_API_URL):
def __init__(self, request_api: RequestAPI, ted_api_url: str = DEFAULT_TED_API_URL):
"""
The constructor will take the API url as a parameter
:param request_api:
:param ted_api_url:
"""
self.request_api = request_api
self.ted_api_url = ted_api_url

def get_by_wildcard_date(self, wildcard_date: str) -> List[dict]:
Expand Down Expand Up @@ -76,15 +77,15 @@ def get_by_query(self, query: dict) -> List[dict]:
"""
query.update(DEFAULT_TED_API_QUERY)

response_body = api_request_with_query(api_url=self.ted_api_url, api_query=query)
response_body = self.request_api(api_url=self.ted_api_url, api_query=query)

documents_number = response_body["total"]
result_pages = 1 + int(documents_number) // 100
documents_content = response_body["results"]

for page_number in range(2, result_pages + 1):
query["pageNum"] = page_number
response_body = api_request_with_query(api_url=self.ted_api_url, api_query=query)
response_body = self.request_api(api_url=self.ted_api_url, api_query=query)
documents_content += response_body["results"]
decoded_documents_content = []
for document_content in documents_content:
Expand Down
15 changes: 15 additions & 0 deletions ted_sws/notice_fetcher/adapters/ted_api_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@
from typing import List


class RequestAPI(abc.ABC):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value of this abstract class is to define the call methods.
I would recommend defining those here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

"""
This class is an abstract interface for requests to an API
"""
@abc.abstractmethod
def __call__(self, api_url: str, api_query: dict) -> dict:
"""
Method to make a post request to the API with a query (json). It will return the response body.
:param api_url:
:param api_query:
:return: dict
"""



class DocumentSearchABC(abc.ABC):
@abc.abstractmethod
def get_by_id(self, document_id: str) -> dict:
Expand Down
21 changes: 16 additions & 5 deletions ted_sws/notice_fetcher/services/notice_fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
from ted_sws.domain.model.manifestation import XMLManifestation
from ted_sws.domain.model.metadata import TEDMetadata
from ted_sws.domain.model.notice import Notice
from ted_sws.notice_fetcher.adapters.ted_api import TedDocumentSearch
from ted_sws.notice_fetcher.adapters.ted_api_abc import DocumentSearchABC


class NoticeFetcherABC(abc.ABC):
"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we decide to have only functions in the service layer of the application, then the class NoticeFetcher class can be merged into DocumetnSearch class. This is to be discussed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We decide to keep adapter logic separate of notice fetcher service logic.


"""

@abc.abstractmethod
def get_notice_by_id(self, document_id: str) -> Notice:
"""
Expand Down Expand Up @@ -48,6 +52,13 @@ class NoticeFetcher(NoticeFetcherABC):

"""

def __init__(self, document_search : DocumentSearchABC):
"""

:param document_search:
"""
self.document_search = document_search

def __create_notice(self, notice_data: dict) -> Notice:
"""

Expand All @@ -68,7 +79,7 @@ def get_notice_by_id(self, document_id):
:param document_id:
:return:
"""
document_result = TedDocumentSearch().get_by_id(document_id=document_id)
document_result = self.document_search.get_by_id(document_id=document_id)

return self.__create_notice(notice_data=document_result)

Expand All @@ -78,7 +89,7 @@ def get_notices_by_query(self, query: dict) -> List[Notice]:
:param query:
:return:
"""
documents = TedDocumentSearch().get_by_query(query=query)
documents = self.document_search.get_by_query(query=query)
return [self.__create_notice(notice_data=document) for document in documents]

def get_notices_by_date_range(self, start_date: date, end_date: date) -> List[Notice]:
Expand All @@ -88,7 +99,7 @@ def get_notices_by_date_range(self, start_date: date, end_date: date) -> List[No
:param end_date:
:return:
"""
documents = TedDocumentSearch().get_by_range_date(start_date=start_date, end_date=end_date)
documents = self.document_search.get_by_range_date(start_date=start_date, end_date=end_date)
return [self.__create_notice(notice_data=document) for document in documents]

def get_notices_by_date_wild_card(self, wildcard_date: str) -> List[Notice]:
Expand All @@ -97,5 +108,5 @@ def get_notices_by_date_wild_card(self, wildcard_date: str) -> List[Notice]:
:param wildcard_date:
:return:
"""
documents = TedDocumentSearch().get_by_wildcard_date(wildcard_date=wildcard_date)
documents = self.document_search.get_by_wildcard_date(wildcard_date=wildcard_date)
return [self.__create_notice(notice_data=document) for document in documents]
Empty file added tests/e2e/__init__.py
Empty file.
Empty file.
42 changes: 42 additions & 0 deletions tests/e2e/notice_fetcher/test_notice_fetcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import datetime
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks grteat!


from ted_sws.domain.model.notice import Notice, NoticeStatus
from ted_sws.notice_fetcher.adapters.ted_api import TedDocumentSearch, TedRequestAPI
from ted_sws.notice_fetcher.services.notice_fetcher import NoticeFetcher


def test_notice_fetcher_by_identifier():
document_id = "067623-2022"
notice = NoticeFetcher(document_search=TedDocumentSearch(request_api=TedRequestAPI())).get_notice_by_id(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use a fixture here for TedDocumentSearch.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

document_id=document_id)

assert isinstance(notice, Notice)
assert notice
assert notice.original_metadata
assert notice.ted_id
assert notice.ted_id == document_id
assert notice.xml_manifestation
assert notice.status == NoticeStatus.RAW


def test_notice_fetcher_by_search_query():
query = {"q": "ND=[67623-2022]"}

notices = NoticeFetcher(document_search=TedDocumentSearch(request_api=TedRequestAPI())).get_notices_by_query(
query=query)

assert isinstance(notices, list)
assert len(notices) == 1
assert isinstance(notices[0], Notice)


def test_notice_fetcher_by_date_range():
notices = NoticeFetcher(document_search=TedDocumentSearch(request_api=TedRequestAPI())).get_notices_by_date_range(
start_date=datetime.date(2022, 2, 3),
end_date=datetime.date(2022, 2, 3))
xml_text = "<NOTICE_DATA>"

assert isinstance(notices, list)
assert len(notices) == 95
assert isinstance(notices[0], Notice)
assert xml_text in notices[0].xml_manifestation.object_data
34 changes: 34 additions & 0 deletions tests/e2e/notice_fetcher/test_ted_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import datetime

import pytest

from ted_sws.notice_fetcher.adapters.ted_api import TedDocumentSearch, TedRequestAPI


def test_ted_api():
ted = TedDocumentSearch(request_api=TedRequestAPI())
xml_text = "<NOTICE_DATA>"

notice_by_id = ted.get_by_id(document_id="67623-2022")
notice_by_date = ted.get_by_range_date(start_date=datetime.date(2022, 2, 3), end_date=datetime.date(2022, 2, 3))
notice_by_date_wildcard = ted.get_by_wildcard_date(wildcard_date="20220203*")
notice_by_query = ted.get_by_query(query={"q": "ND=[67623-2022]"})

assert xml_text in notice_by_id["content"]
assert isinstance(notice_by_id, dict)
assert len(notice_by_date) == 95
assert len(notice_by_date_wildcard) == 95
assert isinstance(notice_by_date, list)
assert isinstance(notice_by_date_wildcard, list)
assert isinstance(notice_by_query, list)
assert isinstance(notice_by_date[0], dict)
assert isinstance(notice_by_date_wildcard[0], dict)
assert isinstance(notice_by_query[0], dict)
assert len(notice_by_query) == 1


def test_ted_api_error():
ted = TedDocumentSearch(request_api=TedRequestAPI())
with pytest.raises(Exception) as e:
ted.get_by_query(query={"q": "NDE=67623-2022"})
assert str(e.value) == "The API call failed with: <Response [500]>"
Empty file added tests/fakes/__init__.py
Empty file.
65 changes: 65 additions & 0 deletions tests/fakes/fake_ted_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import copy
import json
import pathlib
from datetime import date
from typing import List
from ted_sws.notice_fetcher.adapters.ted_api_abc import DocumentSearchABC, RequestAPI


def get_fake_api_response() -> dict:
path = pathlib.Path(__file__).parent.parent / "test_data" / "notices" / "2021-OJS237-623049.json"
return json.loads(path.read_text())


class FakeRequestAPI(RequestAPI):
"""

"""

def __call__(self, api_url: str, api_query: dict) -> dict:
"""

:param args:
:param kwargs:
:return:
"""
return copy.deepcopy(get_fake_api_response())


class FakeTedDocumentSearch(DocumentSearchABC):
"""

"""

def get_by_wildcard_date(self, wildcard_date: str) -> List[dict]:
"""

:param wildcard_date:
:return:
"""
return [get_fake_api_response()]

def get_by_id(self, document_id: str) -> dict:
"""

:param document_id:
:return:
"""
return get_fake_api_response()

def get_by_range_date(self, start_date: date, end_date: date) -> List[dict]:
"""

:param start_date:
:param end_date:
:return:
"""
return [get_fake_api_response()]

def get_by_query(self, query: dict) -> List[dict]:
"""

:param query:
:return:
"""
return [get_fake_api_response()]
72 changes: 39 additions & 33 deletions tests/test_data/notices/2021-OJS237-623049.json

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions tests/unit/notice_fetcher/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import base64
import json
import pathlib

import pytest

def get_api_response():
path = pathlib.Path(__file__).parent.parent.parent / "test_data" / "notices" / "2021-OJS237-623049.json"
return json.loads(path.read_text())
from ted_sws.notice_fetcher.adapters.ted_api import TedDocumentSearch
from tests.fakes.fake_ted_api import FakeRequestAPI



@pytest.fixture
def ted_document_search():
return TedDocumentSearch(request_api=FakeRequestAPI())
19 changes: 0 additions & 19 deletions tests/unit/notice_fetcher/fake_ted_api.py

This file was deleted.

2 changes: 1 addition & 1 deletion tests/unit/notice_fetcher/test_fake_ted_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import datetime

from tests.unit.notice_fetcher.fake_ted_api import FakeTedDocumentSearch
from tests.fakes.fake_ted_api import FakeTedDocumentSearch


def test_fake_ted_api():
Expand Down
Loading