fork download
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. ### BEGIN LICENSE
  4. #Copyright (c) 2009 Eugene Kaznacheev <qetzal@gmail.com>
  5. #Copyright (c) 2013 Joshua Tasker <jtasker@gmail.com>
  6.  
  7. #Permission is hereby granted, free of charge, to any person
  8. #obtaining a copy of this software and associated documentation
  9. #files (the "Software"), to deal in the Software without
  10. #restriction, including without limitation the rights to use,
  11. #copy, modify, merge, publish, distribute, sublicense, and/or sell
  12. #copies of the Software, and to permit persons to whom the
  13. #Software is furnished to do so, subject to the following
  14. #conditions:
  15.  
  16. #The above copyright notice and this permission notice shall be
  17. #included in all copies or substantial portions of the Software.
  18.  
  19. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  20. #EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
  21. #OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  22. #NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  23. #HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  24. #WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  25. #FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  26. #OTHER DEALINGS IN THE SOFTWARE.
  27. ### END LICENSE
  28.  
  29. """ Fetches weather reports from Yahoo! Weather, Weather.com and NOAA """
  30.  
  31. __version__ = "0.3.8"
  32.  
  33. try:
  34. # Python 3 imports
  35. from urllib.request import urlopen
  36. from urllib.parse import quote
  37. from urllib.parse import urlencode
  38. from urllib.error import URLError
  39. # needed for code to work on Python3
  40. xrange = range
  41. unicode = str
  42. except ImportError:
  43. # Python 2 imports
  44. from urllib2 import urlopen
  45. from urllib import quote
  46. from urllib import urlencode
  47. from urllib2 import URLError
  48. import sys
  49. import re
  50. from math import pow
  51. from xml.dom import minidom
  52. import json
  53.  
  54. try:
  55. from unidecode import unidecode
  56. except ImportError:
  57. pass
  58.  
  59. GOOGLE_COUNTRIES_URL = 'http://www.google.com/ig/countries?output=xml&hl=%s'
  60. GOOGLE_CITIES_URL = 'http://www.google.com/ig/cities?output=xml&' + \
  61. 'country=%s&hl=%s'
  62.  
  63. YAHOO_WEATHER_URL = 'http://x...content-available-to-author-only...o.com/forecastrss/%s_%s.xml'
  64. YAHOO_WEATHER_NS = 'http://x...content-available-to-author-only...o.com/ns/rss/1.0'
  65.  
  66. NOAA_WEATHER_URL = 'http://w...content-available-to-author-only...r.gov/xml/current_obs/%s.xml'
  67.  
  68. WEATHER_COM_URL = 'http://w...content-available-to-author-only...r.com/wxdata/weather/local/%s?' + \
  69. 'unit=%s&dayf=5&cc=*'
  70.  
  71. LOCID_SEARCH_URL = 'http://w...content-available-to-author-only...r.com/wxdata/search/search?where=%s'
  72.  
  73. WOEID_SEARCH_URL = 'http://q...content-available-to-author-only...s.com/v1/public/yql'
  74. WOEID_QUERY_STRING = 'select line1, line2, line3, line4, ' + \
  75. 'woeid from geo.placefinder where text="%s"'
  76.  
  77. #WXUG_BASE_URL = 'http://a...content-available-to-author-only...d.com/auto/wui/geo'
  78. #WXUG_FORECAST_URL = WXUG_BASE_URL + '/ForecastXML/index.xml?query=%s'
  79. #WXUG_CURRENT_URL = WXUG_BASE_URL + '/WXCurrentObXML/index.xml?query=%s'
  80. #WXUG_GEOLOOKUP_URL = WXUG_BASE_URL + '/GeoLookupXML/index.xml?query=%s'
  81. #WXUG_ALERTS_URL = WXUG_BASE_URL + '/AlertsXML/index.xml?query=%s'
  82.  
  83.  
  84.  
  85. class WindUnits:
  86. """Class for available wind unit systems"""
  87. MPS = 1
  88. MPH = 2
  89. BEAUFORT = 3
  90. KPH = 4
  91. KNOTS = 5
  92.  
  93.  
  94. def get_weather_from_weather_com(location_id, units = 'metric'):
  95. """Fetches weather report from Weather.com
  96.  
  97. Parameters:
  98. location_id: A five digit US zip code or location ID. To find your
  99. location ID, use function get_loc_id_from_weather_com().
  100.  
  101. units: type of units. 'metric' for metric and 'imperial' for non-metric.
  102. Note that choosing metric units changes all the weather units to metric.
  103. For example, wind speed will be reported as kilometers per hour and
  104. barometric pressure as millibars.
  105.  
  106. Returns:
  107. weather_data: a dictionary of weather data that exists in XML feed.
  108.  
  109. """
  110. location_id = quote(location_id)
  111. if units == 'metric':
  112. unit = 'm'
  113. elif units == 'imperial' or units == '': # for backwards compatibility
  114. unit = ''
  115. else:
  116. unit = 'm' # fallback to metric
  117. url = WEATHER_COM_URL % (location_id, unit)
  118. try:
  119. handler = urlopen(url)
  120. except URLError:
  121. return {'error': 'Could not connect to Weather.com'}
  122. if sys.version > '3':
  123. # Python 3
  124. content_type = dict(handler.getheaders())['Content-Type']
  125. else:
  126. # Python 2
  127. content_type = handler.info().dict['content-type']
  128. try:
  129. charset = re.search('charset\=(.*)', content_type).group(1)
  130. except AttributeError:
  131. charset = 'utf-8'
  132. if charset.lower() != 'utf-8':
  133. xml_response = handler.read().decode(charset).encode('utf-8')
  134. else:
  135. xml_response = handler.read()
  136. dom = minidom.parseString(xml_response)
  137. handler.close()
  138.  
  139. try:
  140. weather_dom = dom.getElementsByTagName('weather')[0]
  141. except IndexError:
  142. error_data = {'error': dom.getElementsByTagName('error')[
  143. 0].getElementsByTagName('err')[0].firstChild.data}
  144. dom.unlink()
  145. return error_data
  146.  
  147. key_map = {'head':'units', 'ut':'temperature', 'ud':'distance',
  148. 'us':'speed', 'up':'pressure', 'ur':'rainfall',
  149. 'loc':'location', 'dnam':'name', 'lat':'lat', 'lon':'lon',
  150. 'cc':'current_conditions', 'lsup':'last_updated',
  151. 'obst':'station', 'tmp':'temperature',
  152. 'flik':'feels_like', 't':'text', 'icon':'icon',
  153. 'bar':'barometer', 'r':'reading', 'd':'direction',
  154. 'wind':'wind', 's':'speed', 'gust':'gust', 'hmid':'humidity',
  155. 'vis':'visibility', 'uv':'uv', 'i':'index', 'dewp':'dewpoint',
  156. 'moon':'moon_phase', 'hi':'high', 'low':'low', 'sunr':'sunrise',
  157. 'suns':'sunset', 'bt':'brief_text', 'ppcp':'chance_precip'}
  158.  
  159. data_structure = {'head': ('ut', 'ud', 'us', 'up', 'ur'),
  160. 'loc': ('dnam', 'lat', 'lon'),
  161. 'cc': ('lsup', 'obst', 'tmp', 'flik', 't',
  162. 'icon', 'hmid', 'vis', 'dewp')}
  163. cc_structure = {'bar': ('r','d'),
  164. 'wind': ('s','gust','d','t'),
  165. 'uv': ('i','t'),
  166. 'moon': ('icon','t')}
  167.  
  168. # sanity check, skip missing items
  169. try:
  170. for (tag, list_of_tags2) in data_structure.items():
  171. for tag2 in list_of_tags2:
  172. if weather_dom.getElementsByTagName(tag)[0].childNodes.length == 0:
  173. data_structure[tag] = []
  174. except IndexError:
  175. error_data = {'error': 'Error parsing Weather.com response. Full response: %s' % xml_response}
  176. return error_data
  177.  
  178. try:
  179. weather_data = {}
  180. for (tag, list_of_tags2) in data_structure.items():
  181. key = key_map[tag]
  182. weather_data[key] = {}
  183. for tag2 in list_of_tags2:
  184. key2 = key_map[tag2]
  185. try:
  186. weather_data[key][key2] = weather_dom.getElementsByTagName(
  187. tag)[0].getElementsByTagName(tag2)[0].firstChild.data
  188. except AttributeError:
  189. # current tag has empty value
  190. weather_data[key][key2] = unicode('')
  191. except IndexError:
  192. error_data = {'error': 'Error parsing Weather.com response. Full response: %s' % xml_response}
  193. return error_data
  194.  
  195. if weather_dom.getElementsByTagName('cc')[0].childNodes.length > 0:
  196. cc_dom = weather_dom.getElementsByTagName('cc')[0]
  197. for (tag, list_of_tags2) in cc_structure.items():
  198. key = key_map[tag]
  199. weather_data['current_conditions'][key] = {}
  200. for tag2 in list_of_tags2:
  201. key2 = key_map[tag2]
  202. try:
  203. weather_data['current_conditions'][key][key2] = cc_dom.getElementsByTagName(
  204. tag)[0].getElementsByTagName(tag2)[0].firstChild.data
  205. except AttributeError:
  206. # current tag has empty value
  207. weather_data['current_conditions'][key][key2] = unicode('')
  208.  
  209. forecasts = []
  210. if len(weather_dom.getElementsByTagName('dayf')) > 0:
  211. time_of_day_map = {'d':'day', 'n':'night'}
  212. for forecast in weather_dom.getElementsByTagName('dayf')[0].getElementsByTagName('day'):
  213. tmp_forecast = {}
  214. tmp_forecast['day_of_week'] = forecast.getAttribute('t')
  215. tmp_forecast['date'] = forecast.getAttribute('dt')
  216. for tag in ('hi', 'low', 'sunr', 'suns'):
  217. key = key_map[tag]
  218. try:
  219. tmp_forecast[key] = forecast.getElementsByTagName(
  220. tag)[0].firstChild.data
  221. except AttributeError:
  222. # if nighttime on current day, key 'hi' is empty
  223. tmp_forecast[key] = unicode('')
  224. for part in forecast.getElementsByTagName('part'):
  225. time_of_day = time_of_day_map[part.getAttribute('p')]
  226. tmp_forecast[time_of_day] = {}
  227. for tag2 in ('icon', 't', 'bt', 'ppcp', 'hmid'):
  228. key2 = key_map[tag2]
  229. try:
  230. tmp_forecast[time_of_day][
  231. key2] = part.getElementsByTagName(tag2)[0].firstChild.data
  232. except AttributeError:
  233. # if nighttime on current day, keys 'icon' and 't' are empty
  234. tmp_forecast[time_of_day][key2] = unicode('')
  235. tmp_forecast[time_of_day]['wind'] = {}
  236. for tag2 in ('s', 'gust', 'd', 't'):
  237. key2 = key_map[tag2]
  238. tmp_forecast[time_of_day]['wind'][key2] = part.getElementsByTagName(
  239. 'wind')[0].getElementsByTagName(tag2)[0].firstChild.data
  240. forecasts.append(tmp_forecast)
  241.  
  242. weather_data['forecasts'] = forecasts
  243.  
  244. dom.unlink()
  245. return weather_data
  246.  
  247. def get_weather_from_google(location_id, hl = ''):
  248. """Fetches weather report from Google. No longer functional,
  249. since Google discontinued their Weather API as of Sep 2012.
  250. Method retained for backwards compatibility.
  251.  
  252. Returns:
  253. weather_data: a dictionary containing only the key 'error'
  254.  
  255. """
  256. weather_data = {'error': 'The Google Weather API has been ' + \
  257. 'discontinued as of September 2012.'}
  258. return weather_data
  259.  
  260. def get_countries_from_google(hl = ''):
  261. """Get list of countries in specified language from Google
  262.  
  263. Parameters:
  264. hl: the language parameter (language code). Default value is empty
  265. string, in this case Google will use English.
  266. Returns:
  267. countries: a list of elements(all countries that exists in XML feed).
  268. Each element is a dictionary with 'name' and 'iso_code' keys.
  269. For example: [{'iso_code': 'US', 'name': 'USA'},
  270. {'iso_code': 'FR', 'name': 'France'}]
  271.  
  272. """
  273. url = GOOGLE_COUNTRIES_URL % hl
  274.  
  275. try:
  276. handler = urlopen(url)
  277. except URLError:
  278. return [{'error':'Could not connect to Google'}]
  279. if sys.version > '3':
  280. # Python 3
  281. content_type = dict(handler.getheaders())['Content-Type']
  282. else:
  283. # Python 2
  284. content_type = handler.info().dict['content-type']
  285. try:
  286. charset = re.search('charset\=(.*)', content_type).group(1)
  287. except AttributeError:
  288. charset = 'utf-8'
  289. if charset.lower() != 'utf-8':
  290. xml_response = handler.read().decode(charset).encode('utf-8')
  291. else:
  292. xml_response = handler.read()
  293. dom = minidom.parseString(xml_response)
  294. handler.close()
  295.  
  296. countries = []
  297. countries_dom = dom.getElementsByTagName('country')
  298.  
  299. for country_dom in countries_dom:
  300. country = {}
  301. country['name'] = country_dom.getElementsByTagName(
  302. 'name')[0].getAttribute('data')
  303. country['iso_code'] = country_dom.getElementsByTagName(
  304. 'iso_code')[0].getAttribute('data')
  305. countries.append(country)
  306.  
  307. dom.unlink()
  308. return countries
  309.  
  310. def get_cities_from_google(country_code, hl = ''):
  311. """Get list of cities of necessary country in specified language from Google
  312.  
  313. Parameters:
  314. country_code: code of the necessary country. For example 'de' or 'fr'.
  315.  
  316. hl: the language parameter (language code). Default value is empty
  317. string, in this case Google will use English.
  318.  
  319. Returns:
  320. cities: a list of elements(all cities that exists in XML feed). Each
  321. element is a dictionary with 'name', 'latitude_e6' and 'longitude_e6'
  322. keys. For example: [{'longitude_e6': '1750000', 'name': 'Bourges',
  323. 'latitude_e6': '47979999'}]
  324.  
  325. """
  326. url = GOOGLE_CITIES_URL % (country_code.lower(), hl)
  327.  
  328. try:
  329. handler = urlopen(url)
  330. except URLError:
  331. return [{'error':'Could not connect to Google'}]
  332. if sys.version > '3':
  333. # Python 3
  334. content_type = dict(handler.getheaders())['Content-Type']
  335. else:
  336. # Python 2
  337. content_type = handler.info().dict['content-type']
  338. try:
  339. charset = re.search('charset\=(.*)', content_type).group(1)
  340. except AttributeError:
  341. charset = 'utf-8'
  342. if charset.lower() != 'utf-8':
  343. xml_response = handler.read().decode(charset).encode('utf-8')
  344. else:
  345. xml_response = handler.read()
  346. dom = minidom.parseString(xml_response)
  347. handler.close()
  348.  
  349. cities = []
  350. cities_dom = dom.getElementsByTagName('city')
  351.  
  352. for city_dom in cities_dom:
  353. city = {}
  354. city['name'] = city_dom.getElementsByTagName(
  355. 'name')[0].getAttribute('data')
  356. city['latitude_e6'] = city_dom.getElementsByTagName(
  357. 'latitude_e6')[0].getAttribute('data')
  358. city['longitude_e6'] = city_dom.getElementsByTagName(
  359. 'longitude_e6')[0].getAttribute('data')
  360. cities.append(city)
  361.  
  362. dom.unlink()
  363.  
  364. return cities
  365.  
  366. def get_weather_from_yahoo(location_id, units = 'metric'):
  367. """Fetches weather report from Yahoo! Weather
  368.  
  369. Parameters:
  370. location_id: A five digit US zip code or location ID. To find your
  371. location ID, use function get_location_ids().
  372.  
  373. units: type of units. 'metric' for metric and 'imperial' for non-metric.
  374. Note that choosing metric units changes all the weather units to
  375. metric. For example, wind speed will be reported as kilometers per
  376. hour and barometric pressure as millibars.
  377.  
  378. Returns:
  379. weather_data: a dictionary of weather data that exists in XML feed.
  380. See http://d...content-available-to-author-only...o.com/weather/#channel
  381.  
  382. """
  383. location_id = quote(location_id)
  384. if units == 'metric':
  385. unit = 'c'
  386. elif units == 'imperial' or units == '': # for backwards compatibility
  387. unit = 'f'
  388. else:
  389. unit = 'c' # fallback to metric
  390. url = YAHOO_WEATHER_URL % (location_id, unit)
  391. try:
  392. handler = urlopen(url)
  393. except URLError:
  394. return {'error': 'Could not connect to Yahoo! Weather'}
  395. if sys.version > '3':
  396. # Python 3
  397. content_type = dict(handler.getheaders())['Content-Type']
  398. else:
  399. # Python 2
  400. content_type = handler.info().dict['content-type']
  401. try:
  402. charset = re.search('charset\=(.*)', content_type).group(1)
  403. except AttributeError:
  404. charset = 'utf-8'
  405. if charset.lower() != 'utf-8':
  406. xml_response = handler.read().decode(charset).encode('utf-8')
  407. else:
  408. xml_response = handler.read()
  409. dom = minidom.parseString(xml_response)
  410. handler.close()
  411.  
  412. weather_data = {}
  413. try:
  414. weather_data['title'] = dom.getElementsByTagName(
  415. 'title')[0].firstChild.data
  416. weather_data['link'] = dom.getElementsByTagName(
  417. 'link')[0].firstChild.data
  418. except IndexError:
  419. error_data = {'error': dom.getElementsByTagName('item')[
  420. 0].getElementsByTagName('title')[0].firstChild.data}
  421. dom.unlink()
  422. return error_data
  423.  
  424. ns_data_structure = {
  425. 'location': ('city', 'region', 'country'),
  426. 'units': ('temperature', 'distance', 'pressure', 'speed'),
  427. 'wind': ('chill', 'direction', 'speed'),
  428. 'atmosphere': ('humidity', 'visibility', 'pressure', 'rising'),
  429. 'astronomy': ('sunrise', 'sunset'),
  430. 'condition': ('text', 'code', 'temp', 'date')
  431. }
  432.  
  433. for (tag, attrs) in ns_data_structure.items():
  434. weather_data[tag] = xml_get_ns_yahoo_tag(
  435. dom, YAHOO_WEATHER_NS, tag, attrs
  436. )
  437.  
  438. weather_data['geo'] = {}
  439. try:
  440. weather_data['geo']['lat'] = dom.getElementsByTagName(
  441. 'geo:lat')[0].firstChild.data
  442. weather_data['geo']['long'] = dom.getElementsByTagName(
  443. 'geo:long')[0].firstChild.data
  444. except AttributeError:
  445. weather_data['geo']['lat'] = unicode()
  446. weather_data['geo']['long'] = unicode()
  447.  
  448. weather_data['condition']['title'] = dom.getElementsByTagName(
  449. 'item')[0].getElementsByTagName('title')[0].firstChild.data
  450. weather_data['html_description'] = dom.getElementsByTagName(
  451. 'item')[0].getElementsByTagName('description')[0].firstChild.data
  452.  
  453. forecasts = []
  454. for forecast in dom.getElementsByTagNameNS(YAHOO_WEATHER_NS, 'forecast'):
  455. forecasts.append(xml_get_attrs(forecast,('day', 'date', 'low', 'high',
  456. 'text', 'code')))
  457. weather_data['forecasts'] = forecasts
  458.  
  459. dom.unlink()
  460. return weather_data
  461.  
  462. def get_everything_from_yahoo(country_code, cities):
  463. """Get all weather data from yahoo for a specific country.
  464.  
  465. Parameters:
  466. country_code: A four letter code of the necessary country.
  467. For example 'GMXX' or 'FRXX'.
  468. cities: The maximum number of cities for which to get data.
  469.  
  470. Returns:
  471. weather_reports: A dictionary containing weather data for each city.
  472.  
  473. """
  474. city_codes = yield_all_country_city_codes_yahoo(country_code, cities)
  475.  
  476. weather_reports = {}
  477. for city_c in city_codes:
  478. weather_data = get_weather_from_yahoo(city_c)
  479. if ('error' in weather_data):
  480. return weather_data
  481. city = weather_data['location']['city']
  482. weather_reports[city] = weather_data
  483.  
  484. return weather_reports
  485.  
  486. def yield_all_country_city_codes_yahoo(country_code, cities):
  487. """Yield all cities codes for a specific country.
  488.  
  489. Parameters:
  490. country_code: A four letter code of the necessary country.
  491. For example 'GMXX' or 'FRXX'.
  492. cities: The maximum number of cities to yield.
  493.  
  494. Returns:
  495. country_city_codes: A generator containing the city codes.
  496.  
  497. """
  498. # cities stands for the number of available cities
  499. for i in range(1, cities + 1):
  500. yield ''.join([country_code, (4 - len(str(i))) * '0', str(i)])
  501.  
  502. def get_weather_from_noaa(station_id):
  503. """Fetches weather report from NOAA: National Oceanic and Atmospheric
  504. Administration (United States)
  505.  
  506. Parameter:
  507. station_id: the ID of the weather station near the desired location
  508. To find your station ID, perform the following steps:
  509. 1. Open this URL: http://w...content-available-to-author-only...r.gov/xml/current_obs/seek.php?state=az&Find=Find
  510. 2. Select the necessary state state. Click 'Find'.
  511. 3. Find the necessary station in the 'Observation Location' column.
  512. 4. The station ID is in the URL for the weather page for that station.
  513. For example if the weather page is http://w...content-available-to-author-only...a.gov/weather/current/KPEO.html -- the station ID is KPEO.
  514.  
  515. Another way to get the station ID: use the 'Weather.location2station'
  516. function of this library: http://code.google.com/p/python-weather/
  517.  
  518. Returns:
  519. weather_data: a dictionary of weather data that exists in XML feed.
  520.  
  521. ( useful icons: http://w...content-available-to-author-only...r.gov/xml/current_obs/weather.php )
  522.  
  523. """
  524. station_id = quote(station_id)
  525. url = NOAA_WEATHER_URL % (station_id)
  526. try:
  527. handler = urlopen(url)
  528. except URLError:
  529. return {'error': 'Could not connect to NOAA'}
  530. if sys.version > '3':
  531. # Python 3
  532. content_type = dict(handler.getheaders())['Content-Type']
  533. else:
  534. # Python 2
  535. content_type = handler.info().dict['content-type']
  536. try:
  537. charset = re.search('charset\=(.*)', content_type).group(1)
  538. except AttributeError:
  539. charset = 'utf-8'
  540. if charset.lower() != 'utf-8':
  541. xml_response = handler.read().decode(charset).encode('utf-8')
  542. else:
  543. xml_response = handler.read()
  544. dom = minidom.parseString(xml_response)
  545. handler.close()
  546.  
  547. data_structure = ('suggested_pickup',
  548. 'suggested_pickup_period',
  549. 'location',
  550. 'station_id',
  551. 'latitude',
  552. 'longitude',
  553. 'observation_time',
  554. 'observation_time_rfc822',
  555. 'weather',
  556. 'temperature_string',
  557. 'temp_f',
  558. 'temp_c',
  559. 'relative_humidity',
  560. 'wind_string',
  561. 'wind_dir',
  562. 'wind_degrees',
  563. 'wind_mph',
  564. 'wind_gust_mph',
  565. 'pressure_string',
  566. 'pressure_mb',
  567. 'pressure_in',
  568. 'dewpoint_string',
  569. 'dewpoint_f',
  570. 'dewpoint_c',
  571. 'heat_index_string',
  572. 'heat_index_f',
  573. 'heat_index_c',
  574. 'windchill_string',
  575. 'windchill_f',
  576. 'windchill_c',
  577. 'icon_url_base',
  578. 'icon_url_name',
  579. 'two_day_history_url',
  580. 'ob_url'
  581. )
  582. weather_data = {}
  583. current_observation = dom.getElementsByTagName('current_observation')[0]
  584. for tag in data_structure:
  585. try:
  586. weather_data[tag] = current_observation.getElementsByTagName(
  587. tag)[0].firstChild.data
  588. except IndexError:
  589. pass
  590.  
  591. dom.unlink()
  592. return weather_data
  593.  
  594. def xml_get_ns_yahoo_tag(dom, ns, tag, attrs):
  595. """Parses the necessary tag and returns the dictionary with values
  596.  
  597. Parameters:
  598. dom: DOM
  599. ns: namespace
  600. tag: necessary tag
  601. attrs: tuple of attributes
  602.  
  603. Returns:
  604. a dictionary of elements
  605.  
  606. """
  607. element = dom.getElementsByTagNameNS(ns, tag)[0]
  608. return xml_get_attrs(element,attrs)
  609.  
  610. def xml_get_attrs(xml_element, attrs):
  611. """Returns the list of necessary attributes
  612.  
  613. Parameters:
  614. element: xml element
  615. attrs: tuple of attributes
  616.  
  617. Returns:
  618. a dictionary of elements
  619.  
  620. """
  621. result = {}
  622. for attr in attrs:
  623. result[attr] = xml_element.getAttribute(attr)
  624. return result
  625.  
  626. def wind_direction(degrees):
  627. """ Convert wind degrees to direction """
  628. try:
  629. degrees = int(degrees)
  630. except ValueError:
  631. return ''
  632.  
  633. if degrees < 23 or degrees >= 338:
  634. return 'N'
  635. elif degrees < 68:
  636. return 'NE'
  637. elif degrees < 113:
  638. return 'E'
  639. elif degrees < 158:
  640. return 'SE'
  641. elif degrees < 203:
  642. return 'S'
  643. elif degrees < 248:
  644. return 'SW'
  645. elif degrees < 293:
  646. return 'W'
  647. elif degrees < 338:
  648. return 'NW'
  649.  
  650. def wind_beaufort_scale(value, wind_units = WindUnits.KPH):
  651. """Convert wind speed value to Beaufort number (0-12)
  652.  
  653. The Beaufort wind force scale is an empirical measure that
  654. relates wind speed to observed conditions at sea or on land.
  655.  
  656. Parameters:
  657. value: wind speed value to convert
  658. wind_units: unit system of value, defaults to km/h
  659.  
  660. Returns:
  661. a string containing the Beaufort number from 0 to 12
  662.  
  663. """
  664. if wind_units == WindUnits.BEAUFORT:
  665. return str(value)
  666.  
  667. try:
  668. value = float(value)
  669. except ValueError:
  670. return ''
  671.  
  672. if value < 0.0:
  673. return ''
  674.  
  675. if wind_units == WindUnits.KPH:
  676. if value < 1:
  677. # Calm
  678. return '0'
  679. elif value <= 5.5:
  680. # Light air
  681. return '1'
  682. elif value <= 11:
  683. # Light breeze
  684. return '2'
  685. elif value <= 19:
  686. # Gentle breeze
  687. return '3'
  688. elif value <= 28:
  689. # Moderate breeze
  690. return '4'
  691. elif value <= 38:
  692. # Fresh breeze
  693. return '5'
  694. elif value <= 49:
  695. # Strong breeze
  696. return '6'
  697. elif value <= 61:
  698. # High wind, moderate gale, near gale
  699. return '7'
  700. elif value <= 74:
  701. # Gale, fresh gale
  702. return '8'
  703. elif value <= 88:
  704. # Strong gale
  705. return '9'
  706. elif value <= 102:
  707. # Storm, whole gale
  708. return '10'
  709. elif value <= 117:
  710. # Violent storm
  711. return '11'
  712. else:
  713. # Hurricane
  714. return '12'
  715.  
  716. if wind_units == WindUnits.MPH:
  717. if value < 1:
  718. return '0'
  719. elif value <= 3:
  720. return '1'
  721. elif value <= 7:
  722. return '2'
  723. elif value <= 12:
  724. return '3'
  725. elif value <= 17:
  726. return '4'
  727. elif value <= 24:
  728. return '5'
  729. elif value <= 30:
  730. return '6'
  731. elif value <= 38:
  732. return '7'
  733. elif value <= 46:
  734. return '8'
  735. elif value <= 54:
  736. return '9'
  737. elif value <= 63:
  738. return '10'
  739. elif value <= 73:
  740. return '11'
  741. else:
  742. return '12'
  743.  
  744. if wind_units == WindUnits.MPS:
  745. if value < 0.3:
  746. return '0'
  747. elif value <= 1.5:
  748. return '1'
  749. elif value <= 3.4:
  750. return '2'
  751. elif value <= 5.4:
  752. return '3'
  753. elif value <= 7.9:
  754. return '4'
  755. elif value <= 10.7:
  756. return '5'
  757. elif value <= 13.8:
  758. return '6'
  759. elif value <= 17.1:
  760. return '7'
  761. elif value <= 20.7:
  762. return '8'
  763. elif value <= 24.4:
  764. return '9'
  765. elif value <= 28.4:
  766. return '10'
  767. elif value <= 32.6:
  768. return '11'
  769. else:
  770. return '12'
  771.  
  772. if wind_units == WindUnits.KNOTS:
  773. if value < 1:
  774. return '0'
  775. if value <= 3:
  776. return '1'
  777. if value <= 6:
  778. return '2'
  779. if value <= 10:
  780. return '3'
  781. if value <= 16:
  782. return '4'
  783. if value <= 21:
  784. return '5'
  785. if value <= 27:
  786. return '6'
  787. if value <= 33:
  788. return '7'
  789. if value <= 40:
  790. return '8'
  791. if value <= 47:
  792. return '9'
  793. if value <= 55:
  794. return '10'
  795. if value <= 63:
  796. return '11'
  797. else:
  798. return '12'
  799.  
  800. def get_wind_direction(degrees):
  801. """ Same as wind_direction """
  802. return wind_direction(degrees)
  803.  
  804. def getText(nodelist):
  805. rc = ""
  806. for node in nodelist:
  807. if node.nodeType == node.TEXT_NODE:
  808. rc = rc + node.data
  809. return rc
  810.  
  811. def get_location_ids(search_string):
  812. """Get location IDs for place names matching a specified string.
  813. Same as get_loc_id_from_weather_com() but different return format.
  814.  
  815. Parameters:
  816. search_string: Plaintext string to match to available place names.
  817. For example, a search for 'Los Angeles' will return matches for the
  818. city of that name in California, Chile, Cuba, Nicaragua, etc as well
  819. as 'East Los Angeles, CA', 'Lake Los Angeles, CA', etc.
  820.  
  821. Returns:
  822. location_ids: A dictionary containing place names keyed to location ID
  823.  
  824. """
  825. loc_id_data = get_loc_id_from_weather_com(search_string)
  826. if 'error' in loc_id_data:
  827. return loc_id_data
  828.  
  829. location_ids = {}
  830. for i in xrange(loc_id_data['count']):
  831. location_ids[loc_id_data[i][0]] = loc_id_data[i][1]
  832. return location_ids
  833.  
  834. def get_loc_id_from_weather_com(search_string):
  835. """Get location IDs for place names matching a specified string.
  836. Same as get_location_ids() but different return format.
  837.  
  838. Parameters:
  839. search_string: Plaintext string to match to available place names.
  840. For example, a search for 'Los Angeles' will return matches for the
  841. city of that name in California, Chile, Cuba, Nicaragua, etc as well
  842. as 'East Los Angeles, CA', 'Lake Los Angeles, CA', etc.
  843.  
  844. Returns:
  845. loc_id_data: A dictionary of tuples in the following format:
  846. {'count': 2, 0: (LOCID1, Placename1), 1: (LOCID2, Placename2)}
  847.  
  848. """
  849. # Weather.com stores place names as ascii-only, so convert if possible
  850. try:
  851. # search_string = unidecode(search_string.encode('utf-8'))
  852. search_string = unidecode(search_string)
  853. except NameError:
  854. pass
  855.  
  856. url = LOCID_SEARCH_URL % quote(search_string)
  857. try:
  858. handler = urlopen(url)
  859. except URLError:
  860. return {'error': 'Could not connect to server'}
  861. if sys.version > '3':
  862. # Python 3
  863. content_type = dict(handler.getheaders())['Content-Type']
  864. else:
  865. # Python 2
  866. content_type = handler.info().dict['content-type']
  867. try:
  868. charset = re.search('charset\=(.*)', content_type).group(1)
  869. except AttributeError:
  870. charset = 'utf-8'
  871. if charset.lower() != 'utf-8':
  872. xml_response = handler.read().decode(charset).encode('utf-8')
  873. else:
  874. xml_response = handler.read()
  875. dom = minidom.parseString(xml_response)
  876. handler.close()
  877.  
  878. loc_id_data = {}
  879. try:
  880. num_locs = 0
  881. for loc in dom.getElementsByTagName('search')[0].getElementsByTagName('loc'):
  882. loc_id = loc.getAttribute('id') # loc id
  883. place_name = loc.firstChild.data # place name
  884. loc_id_data[num_locs] = (loc_id, place_name)
  885. num_locs += 1
  886. loc_id_data['count'] = num_locs
  887. except IndexError:
  888. error_data = {'error': 'No matching Location IDs found'}
  889. return error_data
  890. finally:
  891. dom.unlink()
  892.  
  893. return loc_id_data
  894.  
  895. def get_where_on_earth_ids(search_string):
  896. """Get Yahoo 'Where On Earth' ID for the place names that best match the
  897. specified string. Same as get_woeid_from_yahoo() but different return format.
  898.  
  899. Parameters:
  900. search_string: Plaintext string to match to available place names.
  901. Place can be a city, country, province, airport code, etc. Yahoo returns
  902. the 'Where On Earth' ID (WOEID) for the place name(s) that is the best
  903. match to the full string.
  904. For example, 'Paris' will match 'Paris, France', 'Deutschland' will match
  905. 'Germany', 'Ontario' will match 'Ontario, Canada', 'SFO' will match 'San
  906. Francisco International Airport', etc.
  907.  
  908. Returns:
  909. where_on_earth_ids: A dictionary containing place names keyed to WOEID.
  910.  
  911. """
  912. woeid_data = get_woeid_from_yahoo(search_string)
  913. if 'error' in woeid_data:
  914. return woeid_data
  915.  
  916. where_on_earth_ids = {}
  917. for i in xrange(woeid_data['count']):
  918. where_on_earth_ids[woeid_data[i][0]] = woeid_data[i][1]
  919. return where_on_earth_ids
  920.  
  921. def get_woeid_from_yahoo(search_string):
  922. """Get Yahoo WOEID for the place names that best match the specified string.
  923. Same as get_where_on_earth_ids() but different return format.
  924.  
  925. Parameters:
  926. search_string: Plaintext string to match to available place names.
  927. Place can be a city, country, province, airport code, etc. Yahoo returns
  928. the WOEID for the place name(s) that is the best match to the full string.
  929. For example, 'Paris' will match 'Paris, France', 'Deutschland' will match
  930. 'Germany', 'Ontario' will match 'Ontario, Canada', 'SFO' will match 'San
  931. Francisco International Airport', etc.
  932.  
  933. Returns:
  934. woeid_data: A dictionary of tuples in the following format:
  935. {'count': 2, 0: (WOEID1, Placename1), 1: (WOEID2, Placename2)}
  936.  
  937. """
  938. ## This uses Yahoo's YQL tables to directly query Yahoo's database, e.g.
  939. ## http://q...content-available-to-author-only...s.com/v1/public/yql?q=select%20*%20from%20geo.placefinder%20where%20text%3D%22New%20York%22
  940. if sys.version > '3':
  941. # Python 3
  942. encoded_string = search_string
  943. else:
  944. # Python 2
  945. encoded_string = search_string.encode('utf-8')
  946. params = {'q': WOEID_QUERY_STRING % encoded_string, 'format': 'json'}
  947. url = '?'.join((WOEID_SEARCH_URL, urlencode(params)))
  948. try:
  949. handler = urlopen(url)
  950. except URLError:
  951. return {'error': 'Could not connect to server'}
  952. if sys.version > '3':
  953. # Python 3
  954. content_type = dict(handler.getheaders())['Content-Type']
  955. else:
  956. # Python 2
  957. content_type = handler.info().dict['content-type']
  958. try:
  959. charset = re.search('charset\=(.*)', content_type).group(1)
  960. except AttributeError:
  961. charset = 'utf-8'
  962. if charset.lower() != 'utf-8':
  963. json_response = handler.read().decode(charset).encode('utf-8')
  964. else:
  965. json_response = handler.read()
  966. handler.close()
  967. yahoo_woeid_result = json.loads(json_response)
  968.  
  969. try:
  970. result = yahoo_woeid_result['query']['results']['Result']
  971. except KeyError:
  972. # On error, returned JSON evals to dictionary with one key, 'error'
  973. return yahoo_woeid_result
  974. except TypeError:
  975. return {'error': 'No matching place names found'}
  976.  
  977. woeid_data = {}
  978. woeid_data['count'] = yahoo_woeid_result['query']['count']
  979. for i in xrange(yahoo_woeid_result['query']['count']):
  980. try:
  981. place_data = result[i]
  982. except KeyError:
  983. place_data = result
  984. name_lines = [place_data[tag]
  985. for tag in ['line1','line2','line3','line4']
  986. if place_data[tag] is not None]
  987. place_name = ', '.join(name_lines)
  988. woeid_data[i] = (place_data['woeid'], place_name)
  989.  
  990. return woeid_data
  991.  
  992. def heat_index(temperature, humidity, units = 'metric'):
  993. """Calculate Heat Index for the specified temperature and humidity
  994.  
  995. The formula below approximates the heat index in degrees
  996. Fahrenheit, to within ±1.3 °F. It is the result of a
  997. multivariate fit (temperature equal to or greater than
  998. 80°F and relative humidity equal to or greater than 40%)
  999. to a model of the human body.
  1000.  
  1001. Heat Index = c_1 + (c_2 * T) + (c_3 * R) + (c_4 * T * R) +
  1002. (c_5 * T^2) + (c_6 * R^2) + (c_7 * T^2 * R) +
  1003. (c_8 * T * R^2) + (c_9 * T^2 * R^2)
  1004. where:
  1005. T = ambient dry-bulb temperature (in degrees Fahrenheit)
  1006. R = relative humidity (percentage value between 0 and 100)
  1007.  
  1008. Parameters:
  1009. temperature: air temperature in specified units
  1010. humidity: relative humidity (a percentage) at specified air temperature
  1011. units: type of units. 'metric' for metric and 'imperial' for non-metric.
  1012.  
  1013. Returns:
  1014. heat_index: a numerical value representing the heat index
  1015. in the temperature scale of the specified unit system.
  1016. Returns None if the specified temperature is less than 80°F
  1017. or the specified relative humidity is less than 40%.
  1018. """
  1019. # fallback to metric
  1020. if units != 'imperial' and units != '' and units != 'metric':
  1021. units = 'metric'
  1022.  
  1023. R = float(humidity)
  1024.  
  1025. if units == 'imperial' or units == '': # for backwards compatibility
  1026. T = float(temperature)
  1027. elif units == 'metric':
  1028. # Heat Index is calculated in F
  1029. T = (float(temperature) * 9.0/5.0) + 32.0
  1030.  
  1031. # Heat Index is only valid for temp >= 80°F and humidity >= 40%)
  1032. if (R < 40.0 or T < 80.0):
  1033. return None
  1034.  
  1035. Rsquared = pow(R, 2.0)
  1036. Tsquared = pow(T, 2.0)
  1037.  
  1038. # coefficients for calculation
  1039. c = [None, -42.379, 2.04901523, 10.14333127, -0.22475541,
  1040. -6.83783 * pow(10.0,-3.0), -5.481717 * pow(10.0,-2.0),
  1041. 1.22874 * pow(10.0,-3.0), 8.5282 * pow(10.0,-4.0),
  1042. -1.99 * pow(10.0,-6.0)]
  1043.  
  1044. heat_index = ( c[1] + (c[2]* T) + (c[3]* R) + (c[4]* T * R) +
  1045. (c[5]* Tsquared) + (c[6]* Rsquared) +
  1046. (c[7]* Tsquared * R) + (c[8]* T * Rsquared) +
  1047. (c[9]* Tsquared * Rsquared) )
  1048.  
  1049. # round to one decimal place
  1050. if units == 'metric':
  1051. return round(((heat_index - 32.0) * 5.0/9.0), 1)
  1052. else:
  1053. return round(heat_index, 1)
  1054.  
Success #stdin #stdout 0.07s 19288KB
stdin
Standard input is empty
stdout
Standard output is empty