Skip to content
Open
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
11 changes: 5 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ still evolving (we won't break things just for fun, but many things are still ch
work through design issues). Also, for a version `0.x.y`, we change `x` when we
release new features, and `y` when we make a release with only bug fixes.

We support Python >= 3.7.
We support Python >= 3.11.

Important Links
---------------
Expand All @@ -64,11 +64,10 @@ Important Links
Dependencies
------------

- requests>=1.2
- numpy>=1.8
- protobuf>=3.0.0a3
- beautifulsoup4>=4.6
- pandas
- numpy>=1.25.0
- pandas>=2.1.0
- protobuf>=4.24.3
- requests>=2.30.0

Developer Dependencies
----------------------
Expand Down
1 change: 0 additions & 1 deletion ci/Prerelease
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
beautifulsoup4>=0.0.dev0
numpy>=0.0.dev0
pandas>=0.0.dev0
protobuf>=0.0.dev0
Expand Down
1 change: 0 additions & 1 deletion ci/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
beautifulsoup4==4.15.0
numpy==2.4.6
pandas==3.0.3
protobuf==7.35.1
Expand Down
2 changes: 1 addition & 1 deletion docs/installguide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ years. For Python itself, that means supporting the last two minor releases.
Siphon currently supports the following versions of required dependencies:

.. literalinclude:: ../pyproject.toml
:start-at: beautifulsoup4
:start-at: numpy
:end-at: requests

Installation Instructions for NumPy can be found at:
Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ classifiers = [
]
requires-python = ">=3.11"
dependencies = [
"beautifulsoup4>=4.9.1",
"numpy>=1.25.0",
"pandas>=2.1.0",
"protobuf>=4.24.3",
Expand Down
86 changes: 28 additions & 58 deletions src/siphon/simplewebservice/wyoming.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,23 @@
# SPDX-License-Identifier: BSD-3-Clause
"""Read upper air data from the Wyoming archives."""

from datetime import datetime
from io import StringIO
import warnings

from bs4 import BeautifulSoup
import numpy as np
import pandas as pd
import requests

from .._tools import get_wind_components
from ..http_util import HTTPEndPoint


def _safe_float(s):
"""Convert to float, handling ****** as a string for missing."""
return pd.NA if all(c == '*' for c in s) or s == '-9999.0' else float(s)


class WyomingUpperAir(HTTPEndPoint):
"""Download and parse data from the University of Wyoming's upper air archive."""

def __init__(self):
"""Set up endpoint."""
super().__init__('http://weather.uwyo.edu/cgi-bin/sounding')
super().__init__('http://weather.uwyo.edu/wsgi')

@classmethod
def request_data(cls, time, site_id, recalc=False, **kwargs):
Expand Down Expand Up @@ -79,41 +73,26 @@ def _get_data(self, time, site_id, recalc=False):

"""
raw_data = self._get_data_raw(time, site_id, recalc=recalc)
soup = BeautifulSoup(raw_data, 'html.parser')
tabular_data = StringIO(soup.find_all('pre')[0].contents[0])
col_names = ['pressure', 'height', 'temperature', 'dewpoint', 'direction', 'speed']
df = pd.read_fwf(tabular_data, widths=[7] * 8, skiprows=5,
usecols=[0, 1, 2, 3, 6, 7], names=col_names)

df['u_wind'], df['v_wind'] = get_wind_components(df['speed'],
np.deg2rad(df['direction']))
# Parse CSV. skipinitialspace is used to handle missing data being encoded by just
# having spaces in the field
df = pd.read_csv(StringIO(raw_data), parse_dates=['time'],
date_format='%Y-%m-%d %H:%M:%S', skipinitialspace=True,
na_values={'latitude':'-99.9900', 'longitude':'-99.9900'})
df = df.rename(columns=lambda c: c.split('_')[0])
df = df.rename(columns=lambda c: c.replace(' ', '_'))
df = df.rename(columns={'geopotential_height': 'height',
'dew_point_temperature': 'dewpoint',
'wind_direction': 'direction', 'wind_speed': 'speed'})

# Drop any rows with all NaN values for T, Td, winds
df = df.dropna(subset=('temperature', 'dewpoint', 'direction', 'speed',
'u_wind', 'v_wind'), how='all').reset_index(drop=True)

# Parse metadata
meta_data = soup.find_all('pre')[1].contents[0]
lines = meta_data.splitlines()

# Convert values after table into key, value pairs using the name to the left of the :
post_values = dict(tuple(map(str.strip, line.split(': '))) for line in lines[1:])

station = post_values.get('Station identifier', '')
station_number = int(post_values['Station number'])
sounding_time = datetime.strptime(post_values['Observation time'], '%y%m%d/%H%M')
latitude = _safe_float(post_values.get('Station latitude', '******'))
longitude = _safe_float(post_values.get('Station longitude', '******'))
elevation = _safe_float(post_values.get('Station elevation', '******'))
pw = float(post_values['Precipitable water [mm] for entire sounding'])

df['station'] = station
df['station_number'] = station_number
df['time'] = sounding_time
df['latitude'] = latitude
df['longitude'] = longitude
df['elevation'] = elevation
df['pw'] = pw
df = df.dropna(subset=('temperature', 'dewpoint', 'direction', 'speed'),
how='all').reset_index(drop=True)

df['u_wind'], df['v_wind'] = get_wind_components(df['speed'],
np.deg2rad(df['direction']))
df['station'] = site_id
df['height'] = df['height'].astype('float')

# Add unit dictionary
with warnings.catch_warnings():
Expand All @@ -124,16 +103,13 @@ def _get_data(self, time, site_id, recalc=False):
'temperature': 'degC',
'dewpoint': 'degC',
'direction': 'degrees',
'speed': 'knot',
'u_wind': 'knot',
'v_wind': 'knot',
'speed': 'm/s',
'u_wind': 'm/s',
'v_wind': 'm/s',
'station': None,
'station_number': None,
'time': None,
'latitude': 'degrees',
'longitude': 'degrees',
'elevation': 'meter',
'pw': 'millimeter'}
'longitude': 'degrees'}

return df

Expand All @@ -155,17 +131,11 @@ def _get_data_raw(self, time, site_id, recalc=False):
text of the server response

"""
path = ('?region=naconf&TYPE=TEXT%3ALIST'
f'&YEAR={time:%Y}&MONTH={time:%m}&FROM={time:%d%H}&TO={time:%d%H}'
f'&STNM={site_id}')
path = f'sounding?type=TEXT%3ACSV&datetime={time:%Y-%m-%d %H:%M:%S}&id={site_id}'
if recalc:
path += '&REPLOT=1'

resp = self.get_path(path)
# See if the return is valid, but has no data
if resp.text.find("Can't") != -1:
raise ValueError(
f'No data available for {time:%Y-%m-%d %HZ} '
f'for station {site_id}.')

return resp.text
try:
return self.get_path(path).text
Comment thread
lesserwhirls marked this conversation as resolved.
except requests.HTTPError as e:
raise ValueError from e
Loading