Skip to content

Commit

Permalink
Merge pull request #15 from CDJellen/develop
Browse files Browse the repository at this point in the history
Add more tests and documentation
  • Loading branch information
CDJellen committed Sep 12, 2022
2 parents f31006f + 82450ff commit f0e6d94
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 9 deletions.
17 changes: 17 additions & 0 deletions ndbc_api/config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
"""Stores the configuration information for the NDBC API.
Attributes:
LOGGER_NAME (:str:): The name for the `logging.Logger` in the api instance.
DEFAULT_CACHE_LIMIT (:int:): The station level limit for caching NDBC data
service requests.
VERIFY_HTTPS (:bool:): Whether to execute requests using HTTPS rather than
HTTP.
HTTP_RETRY (:int:): The number of times to retry requests to the NDBC data
service.
HTTP_BACKOFF_FACTOR (:float:): The backoff factor used when executing retry
requests to the NDBC data service.
HTTP_DELAY (:int:) The delay between requests submitted to the NDBC data
service, in milliseconds.
HTTP_DEBUG (:bool:): Whether to log requests and responses to the NDBC API's
log (a `logging.Logger`) as debug messages.
"""
LOGGER_NAME = 'NDBC-API'
DEFAULT_CACHE_LIMIT = 36
VERIFY_HTTPS = False
Expand Down
24 changes: 22 additions & 2 deletions ndbc_api/ndbc_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
Attributes:
log (:obj:`logging.Logger`): The logger at which to register HTTP
request and response status codes and headers used for debug
purposes.
request and response status codes and headers used for debug
purposes.
headers(:dict:): The request headers for use in the NDBC API's request
handler.
"""
import logging
import pickle
Expand Down Expand Up @@ -85,6 +87,7 @@ def __init__(
self,
logging_level: int = logging.WARNING if HTTP_DEBUG else 0,
cache_limit: int = DEFAULT_CACHE_LIMIT,
headers: dict = {},
delay: int = HTTP_DELAY,
retries: int = HTTP_RETRY,
backoff_factor: float = HTTP_BACKOFF_FACTOR,
Expand All @@ -94,11 +97,13 @@ def __init__(
"""Initializes the singleton `NdbcApi`, sets associated handlers."""
self.log.setLevel(logging_level)
self.cache_limit = cache_limit
self.headers = headers
self._handler = self._get_request_handler(
cache_limit=self.cache_limit,
delay=delay,
retries=retries,
backoff_factor=backoff_factor,
headers=self.headers,
debug=debug,
verify_https=verify_https,
)
Expand Down Expand Up @@ -145,6 +150,7 @@ def clear_cache(self) -> None:
delay=HTTP_DELAY,
retries=HTTP_RETRY,
backoff_factor=HTTP_BACKOFF_FACTOR,
headers=self.headers,
debug=HTTP_DEBUG,
verify_https=VERIFY_HTTPS,
)
Expand All @@ -157,6 +163,18 @@ def get_cache_limit(self) -> int:
"""Get the cache limit for the API's request cache."""
return self._handler.get_cache_limit()

def get_headers(self) -> dict:
"""Return the current headers used by the request handler."""
return self._handler.get_headers()

def update_headers(self, new: dict) -> None:
"""Add new headers to the request handler."""
self._handler.update_headers(new)

def set_headers(self, request_headers: dict) -> None:
"""Reset the request headers using the new supplied headers."""
self._handler.set_headers(request_headers)

def stations(self, as_df: bool = True) -> Union[pd.DataFrame, dict]:
"""Get all stations and station metadata from the NDBC.
Expand Down Expand Up @@ -402,6 +420,7 @@ def _get_request_handler(
delay: int,
retries: int,
backoff_factor: float,
headers: dict,
debug: bool,
verify_https: bool,
) -> Any:
Expand All @@ -412,6 +431,7 @@ def _get_request_handler(
delay=delay,
retries=retries,
backoff_factor=backoff_factor,
headers=headers,
debug=debug,
verify_https=verify_https,
)
Expand Down
69 changes: 62 additions & 7 deletions ndbc_api/utilities/req_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,23 @@
enforced on a station level.
Example:
TODO
```python3
handler = RequestHandler(
cache_limit=1,
delay=2,
retries=3,
backoff_factor=0.8,
debug=True,
verify_https=True,
)
response = handler.execute_request(
url='foo.bar'
)
```
Attributes:
None.
stations (:obj:`list`): A list of `Station`s to which requests have
been made.
"""
from typing import Union, List

Expand All @@ -33,15 +45,44 @@ class RequestHandler(metaclass=Singleton):
in the property's getter method.
Attributes:
stations (:obj:`list`): A list of `Station`s to which requests have
been made.
cache_limit (:int:): The handler's global limit for caching
`NdbcApi` responses. This is implemented as a least-recently
used cache, designed to conserve NDBC resources when querying
measurements for a given station over similar time ranges.
log (:obj:`logging.Logger`): The logger at which to register HTTP
request and response status codes and headers used for debug
purposes.
delay (:int:): The HTTP(s) request delay parameter, in seconds.
retries (:int:): = The number of times to retry a request to the NDBC data
service.
backoff_factor (:float:): The back-off parameter, used in conjunction with
`retries` to re-attempt requests to the NDBC data service.
headers (:dict:): The headers with which to execute the requests to the NDBC data
service.
debug (:bool:): A flag for verbose logging and response-level status reporting.
Affects the instance's `logging.Logger` and the behavior of its
private `RequestHandler` instance.
verify_https (:bool:): A flag which indicates whether to attempt requests to the
NDBC data service over HTTP or HTTPS.
"""

class Station:

"""The summary line for a class docstring should fit on one line.
If the class has public attributes, they may be documented here
in an ``Attributes`` section and follow the same formatting as a
function's ``Args`` section. Alternatively, attributes may be documented
inline with the attribute's declaration (see __init__ method below).
Properties created with the ``@property`` decorator should be documented
in the property's getter method.
Attributes:
id_ (:str:): The key for the `Station` object.
reqs (:obj:`ndbc_api.utilities.RequestCache`): The `RequestCache`
for the Station with the given `id_`, uses the cache limit of
its parent `RequestHandler`.
"""
__slots__ = 'id_', 'reqs'

def __init__(self, station_id: str, cache_limit: int) -> None:
Expand Down Expand Up @@ -71,24 +112,34 @@ def __init__(
self._session = self._create_session()

def get_cache_limit(self) -> int:
"""Return the current station-level cache limit for NDBC requests."""
return self._cache_limit

def set_cache_limit(self, cache_limit: int) -> None:
"""Set a new station-level cache limit for NDBC requests."""
self._cache_limit = cache_limit

def get_headers(self) -> dict:
"""Add new headers to future NDBC data service requests."""
return self._request_headers

def update_headers(self, new: dict) -> None:
"""Add new headers to future NDBC data service requests."""
self._request_headers.update(new)

def set_headers(self, request_headers: dict) -> None:
"""Reset the request headers using the new supplied headers."""
self._request_headers = request_headers

def has_station(self, station_id: Union[str, int]) -> bool:
"""Determine if the NDBC API already made a request to this station."""
for s in self.stations:
if s.id_ == station_id:
return True
return False

def get_station(self, station_id: Union[str, int]) -> Station:
"""Get `RequestCache` with `id_` matching the supplied `station_id`."""
if isinstance(station_id, int):
station_id = str(station_id)
if not self.has_station(station_id):
Expand All @@ -98,20 +149,22 @@ def get_station(self, station_id: Union[str, int]) -> Station:
return s

def add_station(self, station_id: Union[str, int]) -> None:
"""Add new new `RequestCache` for the supplied `station_id`."""
self.stations.append(
RequestHandler.Station(station_id=station_id,
cache_limit=self._cache_limit))

def handle_requests(self, station_id: Union[str, int],
reqs: List[str]) -> List[str]: # pragma: no cover

"""Handle many string-valued requests against a supplied station."""
responses = []
for req in reqs:
responses.append(self.handle_request(station_id=station_id,
req=req))
return responses

def handle_request(self, station_id: Union[str, int], req: str) -> dict:
"""Handle a string-valued requests against a supplied station."""
stn = self.get_station(station_id=station_id)
if req not in stn.reqs.cache:
resp = self.execute_request(url=req, headers=self._request_headers)
Expand All @@ -120,6 +173,7 @@ def handle_request(self, station_id: Union[str, int], req: str) -> dict:

def execute_request(self, url: str,
headers: dict) -> dict: # pragma: no cover
"""Execute a request with the current headers to NDBC data service."""
response = self._session.get(
url=url,
headers=headers,
Expand All @@ -136,6 +190,7 @@ def execute_request(self, url: str,
""" PRIVATE """

def _create_session(self) -> requests.Session:
"""create a new `Session` using `RequestHandler` configuration."""
session = requests.Session()
retry = Retry(
backoff_factor=self._backoff_factor,
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
'Intended Audience :: Developers',
'Topic :: Software Development :: Build Tools',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
Expand Down
23 changes: 23 additions & 0 deletions tests/test_ndbc_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,29 @@ def test_dump_cache_nonempty(ndbc_api):
test_fp.unlink(missing_ok=False)


def test_get_headers(ndbc_api):
want = {}
got = ndbc_api.get_headers()
assert want == got


def test_update_headers(ndbc_api):
current_headers = ndbc_api.get_headers()
new_headers = {'foo': 'bar'}
want = {**current_headers, **new_headers}
ndbc_api.update_headers(new_headers)
got = ndbc_api.get_headers()
assert want == got


def test_set_headers(ndbc_api):
new_headers = {'foo': 'bar'}
want = new_headers
ndbc_api.set_headers(new_headers)
got = ndbc_api.get_headers()
assert want == got


@pytest.mark.usefixtures('mock_socket', 'read_responses', 'read_parsed_yml')
def test_station(ndbc_api, mock_socket, read_responses, read_parsed_yml):
_ = mock_socket
Expand Down

0 comments on commit f0e6d94

Please sign in to comment.