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

Adds acis.py for ACIS Web Services functionality #177

Merged
merged 17 commits into from
Jan 30, 2018

Conversation

WEP11
Copy link
Contributor

@WEP11 WEP11 commented Jan 9, 2018

Adds acis.py, which provides a function for connecting to ACIS Web Services. This function is Python 3 compatible, but will need to use urllib2 to become Python2.7 compatible.

Updates docstring text to better reflect parameters
Copy link
Member

@dopplershift dopplershift left a comment

Choose a reason for hiding this comment

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

Thanks! I have a few comments, but really they just relate to better integration into Siphon. Also, can you move this into the simplewebservice directory? That's where we're starting to accumulate things like this.

Overall, the code does seem fine and clearly would simplify access to the service. Would be good to eventually add an example to show how useful this is.

siphon/acis.py Outdated
timeout=60

try:
response = urllib.request.urlopen(req, timeout=timeout) # 10-Minute Timeout
Copy link
Member

Choose a reason for hiding this comment

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

I don't think that comment is correct any more 😁

Would also need to be updated to use requests.

siphon/acis.py Outdated
return json.loads(response.read())
except urllib.request.HTTPError as error:
if error.code == 400:
print(error.msg)
Copy link
Member

Choose a reason for hiding this comment

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

Instead of catching the error, printing the message, and returning False, I think it's better to just let the error bubble to the top. That turns the code for users from (currently):

response = acisRequest(...)
if response == False:
    # Handle error
else:
    # Do stuff with data

into

try:
    response = acisRequest(...)
    # Do stuff with data
except HTTPError:
    # Handle errors

It's shorter code in the library, just as easy for users, and doesn't involve having changing return types. If you're worried about having multiple exception types, you could create a custom Exception subclass and raise that from inside the function.

siphon/acis.py Outdated
import json
import socket

def acisRequest(method, params):
Copy link
Member

Choose a reason for hiding this comment

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

How about acis_request? That matches our existing naming structure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That certainly can be done.

siphon/acis.py Outdated

Returns
----------
Connection Success: A dictionary that is derived from JSON data from the remote API
Copy link
Member

Choose a reason for hiding this comment

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

Would need to be updated based on discussion below. Also Sphinx will pitch a fit unless the length of "---------" matches the length of "Returns".

siphon/acis.py Outdated

Parameters
----------
method - The Web Services request method (StnMeta, StnData, MultiStnData, etc)
Copy link
Member

Choose a reason for hiding this comment

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

Parameters docs look like:

method : str
    The Web Services request method (StnMeta, StnData, MultiStnData, etc.)

siphon/acis.py Outdated

base_url = 'http://data.rcc-acis.org/' # ACIS Web API URL

req = urllib.request.Request(base_url+method,
Copy link
Member

Choose a reason for hiding this comment

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

Do you think you could adapt this to use the requests module? We're already using it internally, it would solve your Python 2 problem, and it sets our own "User-Agent" header. I'm happy to look into how to do this if you can clarify how the data argument gets used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The data argument is simply a JSON object containing parameters (Station ID, dates, variables, etc) for the HTTP request. I'm more than happy to adapt it, there was no reason for using urllib other than that's what was used in the old Python 2 version of this code.

Copy link
Member

Choose a reason for hiding this comment

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

Ok, then I'll leave you to it, but feel free to scream if you run into problems.

@WEP11
Copy link
Contributor Author

WEP11 commented Jan 17, 2018

I am working on some use case examples. Will those go into /examples?

@dopplershift
Copy link
Member

Yeah, I think that makes sense, though if you have several, you could do examples/acis.

@WEP11
Copy link
Contributor Author

WEP11 commented Jan 18, 2018

Latest push adds two examples to the documentation and finalizes some of the acis_request function code. Exception handling is certainly open to feedback.

Copy link
Member

@dopplershift dopplershift left a comment

Choose a reason for hiding this comment

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

Looks like I failed to submit this yesterday. Oops.

Exception handling actually looks fine, at least to my eyes.

Some general style comments, especially for examples, to make the bots happy:

  • a space after every comma
  • Only use single quotes
  • a space after the colon in a dictionary

Otherwise, I think these examples look great!

base_url = 'http://data.rcc-acis.org/' # ACIS Web API URL

if method == 'MultiStnData':
timeout=300
Copy link
Member

Choose a reason for hiding this comment

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

Could use some more spaces around the = here. Or if you like:

timeout = 300 if method == 'MultiStnData' else 60

but that's just a matter of personal taste.

timeout=60

try:
response = requests.post(base_url+method, data=params, timeout=timeout)
Copy link
Member

Choose a reason for hiding this comment

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

Can you put some spaces around the +?

Copy link
Member

Choose a reason for hiding this comment

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

Also, if you change requests.post() to create_http_session().post() (after doing from ..http_util import create_http_session), you'll get e.g. "Siphon(0.6.0)" in the logs for ACIS. Not sure if you'd be interested in seeing how much use the service gets from us--or if you even have access to those logs yourself. This one really doesn't matter to me since we'll never see the logs.

Parameters
----------
method : str
The Web Services request method (MultiStn,StnMeta,etc)
Copy link
Member

Choose a reason for hiding this comment

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

Is the exhaustive list of methods up above? If so, probably good to just list all 5 here rather than abbreviating the list.


class ACIS_API_Exception(Exception):
"""

Copy link
Member

Choose a reason for hiding this comment

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

Can you kill the blank line?

timeout=60

try:
response = requests.post(base_url+method, json=params, timeout=timeout)
Copy link
Member

Choose a reason for hiding this comment

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

Can you add spaces around the +?

Copy link
Member

Choose a reason for hiding this comment

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

Also, if you change requests.post() to create_http_session().post() (after doing from ..http_util import create_http_session), you'll get e.g. "Siphon(0.6.0)" in the logs for ACIS. Not sure if you'd be interested in seeing how much use the service gets from us--or if you even have access to those logs yourself. This one really doesn't matter to me since we'll never see the logs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Neat! I'll have to double check with the developer that manages the servers to make sure, but I know it'd be interesting to see. (We're more than happy to share those statistics if we get them)

# all 4 days of data, we would simply remove that parameter.
#
# To wrap up this example, we will finally plot our precipitation sums and
# departures onto a map using CartoPy and MetPy. To do this we will utilize
Copy link
Member

Choose a reason for hiding this comment

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

It doesn't look like you're actually using MetPy here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You caught me! I was, but then I noticed the implementation I had was poor. Looks like I forgot to proofread that part of the tutorial afterwards.

except requests.exceptions.TooManyRedirects:
raise ACIS_API_Exception("Bad URL. Check your ACIS connection method string.")
except ValueError:
raise ACIS_API_Exception("No data returned! The ACIS parameter dictionary\
Copy link
Member

Choose a reason for hiding this comment

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

Single quotes, please. Also, instead of the line continuation (\), you can just end the line with a ', and then open a new string with a ' on the next line; since it's within the (), Python will concatenate them for you. So:

raise ACIS_API_Exception('No data returned! The ACIS parameter dictionary'
                         'may be incorrectly formatted')

@dopplershift
Copy link
Member

We'll also need to add a test. Probably easiest to start with one from siphon/tests/test_wyoming.py. The recorder.use_cassette the first time you run the test will record all of the web requests and responses; future runs of the test (when it can find the recorded data--so it needs to be included in a commit) will use the recorded data so that we don't have tests that need a proper network, server availability, etc. You'll need to make sure vcrpy is installed.

If setting that up is a problem, I'm happy to jump in and help out.

- Changes use of requests directly in acis_request() to use siphon create_http_session() function.
@WEP11
Copy link
Contributor Author

WEP11 commented Jan 19, 2018

Alright, I have a unit test file that runs fine when run I run the individual function. How can I run the test to generate the recorded data you mentioned? Thanks!

@WEP11
Copy link
Contributor Author

WEP11 commented Jan 19, 2018

This version adds the unit test, fixes the formatting errors mentioned, and adds the unit test data to the fixtures folder. I completely missed that the data was already present in my fork (I imagined a gitignore for some reason). I'll monitor the bots to see what they find, thanks!

Copy link
Member

@dopplershift dopplershift left a comment

Choose a reason for hiding this comment

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

Just a minor change on the test, otherwise that looks just like what I was looking for. Modulo some minor style things, this is almost there.

recorder = get_recorder(__file__)

@recorder.use_cassette('acis_request')
def test_acis():
Copy link
Member

Choose a reason for hiding this comment

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

Can you break this up into 4 individual tests, one for each request method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I certainly can

"""Testing ACIS Gridded Data request."""
data = acis_request('GridData', {'loc': '-95.36, 29.76', 'sdate': '2000-01',
'edate': '2000-07', 'grid': '3', 'elems': [
{'name': 'maxt', 'interval': 'mly', 'reduce': 'max',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm unsure of how to take care of this PEP8 error. I thought line 44 might have been too long, but instead I created the E128 error.
siphon/tests/test_acis.py:44:25: E122 continuation line missing indentation or outdented siphon/tests/test_acis.py:45:25: E128 continuation line under-indented for visual indent

Copy link
Member

Choose a reason for hiding this comment

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

Try this:

    data = acis_request('GridData',
                        {'loc': '-95.36, 29.76', 'sdate': '2000-01', 'edate': '2000-07',
                         'grid': '3', 'elems': [{'name': 'maxt', 'interval': 'mly',
                                                 'reduce': 'max', 'smry': 'max'}]})

Arguments to the function, keys in the dictionary, items in the list--they line up when wrapped.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That seems to have worked on my end, we'll see if that silences the Travis-CI. (Right after I make the acis_request function in acis.py have an imperative mood)

recorder = get_recorder(__file__)


@recorder.use_cassette('acis_request')
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I used one "cassette" for all 5 tests.. Is that alright? They're all JSON strings.

Copy link
Member

Choose a reason for hiding this comment

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

I think that's fine for now. If we ever need to regenerate, we'll have to separate them, since the cassette is only ever generated from a single test.

@dopplershift dopplershift self-assigned this Jan 28, 2018
@dopplershift dopplershift added this to the 0.7 milestone Jan 28, 2018
dopplershift
dopplershift previously approved these changes Jan 30, 2018
Copy link
Member

@dopplershift dopplershift left a comment

Choose a reason for hiding this comment

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

Found a few things, but nothing I’d hold this up for. Feel free to ignore. Let me know and I’ll merge.


for stn in myData['data']:
# Skip threaded stations! They have no lat/lons
if stn['meta']['sids'][-1][-1] == '9':
Copy link
Member

Choose a reason for hiding this comment

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

Maybe

if stn[‘meta’][‘sids’][-1].endswith(‘9’)

avgt = []
dates = []
for obs in myData['data']:
if obs[0][-2:] == '01':
Copy link
Member

Choose a reason for hiding this comment

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

How about:

If obs[0].endswith(‘01’)

?

if stn['meta']['sids'][-1][-1] == '9':
continue
# Skip stations with missing data
if stn['smry'][0] == 'M' or stn['smry'][1] == 'M':
Copy link
Member

Choose a reason for hiding this comment

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

What comes before “M” if it’s at index 1? White space? If so, what about:

If stn[‘smry’].lstrip().startswith(‘M’)

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this case 'smry' is actually a list of summary values. So in this case the 'smry' would have one value for the total precip and another for the departure from normal.

@WEP11
Copy link
Contributor Author

WEP11 commented Jan 30, 2018

That push should take care of most of the changes you found.. I 'dismissed' your review I guess?

@dopplershift
Copy link
Member

I have it set to clear the review when new code is uploaded.

This is good. Thanks! 🎉 Congrats on your PR!

@dopplershift dopplershift merged commit c664e04 into Unidata:master Jan 30, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants