├── .gitignore ├── LICENSE ├── README.md ├── environment.yml ├── meteogram ├── __init__.py ├── meteogram.py └── tests │ └── test_meteogram.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 John Leeman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # testing-with-python 2 | Software testing and testing automation with Python short-course 3 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | # Create full conda environment for development, including some useful tools 2 | name: python-testing 3 | channels: 4 | - conda-forge 5 | dependencies: 6 | - python=3 7 | - ipython 8 | - numpy 9 | - matplotlib 10 | - pandas>=0.24.2 11 | - pytest>=2.4 12 | - pytest-cov 13 | - pytest-flake8 14 | - pytest-mpl 15 | - pytest-runner 16 | - vcrpy 17 | -------------------------------------------------------------------------------- /meteogram/__init__.py: -------------------------------------------------------------------------------- 1 | """This module contains tools for making meteograms.""" 2 | -------------------------------------------------------------------------------- /meteogram/meteogram.py: -------------------------------------------------------------------------------- 1 | """Helps obtain, analyze, and plot surface observations as a meteogram.""" 2 | 3 | import datetime 4 | import numpy as np 5 | import pandas as pd 6 | import matplotlib.pyplot as plt 7 | from pandas.plotting import register_matplotlib_converters 8 | 9 | 10 | register_matplotlib_converters() 11 | 12 | 13 | def degF_to_degC(degF): 14 | """ 15 | Convert degF to degC 16 | 17 | Parameters 18 | ---------- 19 | degF : float 20 | Temperature in Fahrenheit. 21 | 22 | Returns 23 | ------- 24 | Temperature in Celsius 25 | 26 | """ 27 | return (degF - 32) * (5 / 9) 28 | 29 | 30 | def current_utc_time(): 31 | """ 32 | Return the current UTC date and time. 33 | 34 | Returns 35 | ------- 36 | datetime.dateimte: current UTC date and time 37 | 38 | """ 39 | return datetime.datetime.utcnow() 40 | 41 | 42 | def potential_temperature(pressure, temperature): 43 | """Calculate the potential temperature. 44 | 45 | Uses the Poisson equation to calculation the potential temperature 46 | given `pressure` and `temperature`. 47 | 48 | Parameters 49 | ---------- 50 | pressure 51 | The total atmospheric pressure 52 | temperature 53 | The temperature in kelvin 54 | 55 | Returns 56 | ------- 57 | The potential temperature corresponding to the temperature and 58 | pressure. 59 | 60 | For inputs of 800 hPa and 273 Kelvin, output should be 290.96 K 61 | 62 | """ 63 | return temperature / exner_function(pressure) 64 | 65 | 66 | def exner_function(pressure, reference_pressure=1000): 67 | r"""Calculate the Exner function. 68 | .. math:: \Pi = \left( \frac{p}{p_0} \right)^\kappa 69 | This can be used to calculate potential temperature from 70 | temperature (and visa-versa), since 71 | .. math:: \Pi = \frac{T}{\theta} 72 | 73 | Parameters 74 | ---------- 75 | pressure 76 | The total atmospheric pressure 77 | reference_pressure : `pint.Quantity`, optional 78 | The reference pressure against which to calculate the Exner 79 | function, defaults to 1000 hPa 80 | 81 | Returns 82 | ------- 83 | The value of the Exner function at the given pressure. 84 | 85 | """ 86 | return (pressure / reference_pressure)**0.28562982892500527 87 | 88 | 89 | def build_asos_request_url(station, start_date, end_date): 90 | """ 91 | Create a URL to request ASOS data from the Iowa State archive. 92 | 93 | Parameters 94 | ---------- 95 | station: str 96 | Station identifier 97 | start_date: datetime.datetime 98 | Starting time of data to be obtained 99 | end_data: datetime.datetime 100 | Ending time of data to be obtained 101 | 102 | Returns 103 | ------- 104 | str: URL of the data 105 | """ 106 | 107 | url_str = (f'https://mesonet.agron.iastate.edu/request/asos/' 108 | f'1min_dl.php?station%5B%5D={station}&tz=UTC&year1=' 109 | f'{start_date:%Y}&month1={start_date:%m}&day1={start_date:%d}' 110 | f'&hour1={start_date:%H}&minute1={start_date:%M}&year2=' 111 | f'{end_date:%Y}&month2={end_date:%m}&day2={end_date:%d}&hour2=' 112 | f'{end_date:%H}&minute2={end_date:%M}&vars%5B%5D=tmpf&vars%5B' 113 | f'%5D=dwpf&vars%5B%5D=sknt&vars%5B%5D=drct&' 114 | f'sample=1min&what=view&delim=comma&gis=yes') 115 | return url_str 116 | 117 | 118 | def download_asos_data(url): 119 | """ 120 | Download ASOS data from the Iowa State archive. 121 | 122 | Parameters 123 | ---------- 124 | url: str 125 | URL of the data 126 | 127 | Returns 128 | ------- 129 | pandas.DataFrame: Observation Data 130 | """ 131 | # Data at the URL are CSV format 132 | df = pd.read_csv(url) 133 | 134 | # There is a trailing comma, so remove the last column 135 | df.drop(columns=df.columns[-1], inplace=True) 136 | 137 | # Rename the columns to more useful names 138 | df.columns = ['station_id', 'station_name', 'latitude_deg', 139 | 'longitude_deg', 'UTC', 'temperature_degF', 140 | 'dewpoint_degF', 'wind_speed_knots', 141 | 'wind_direction_degrees'] 142 | 143 | # Parse the valid times into real datetimes 144 | df['UTC'] = pd.to_datetime(df['UTC']) 145 | return df 146 | 147 | 148 | def plot_meteogram(df): 149 | """ 150 | Plot a meteogram with matplotlib. 151 | 152 | Parameters 153 | ---------- 154 | df: pandas.DataFrame 155 | Dataframe of ASOS data 156 | 157 | Returns 158 | ------- 159 | matplotlib.figure.Figure, matplotlib.axes._subplots.AxesSubplot, 160 | matplotlib.axes._subplots.AxesSubplot, 161 | matplotlib.axes._subplots.AxesSubplot 162 | """ 163 | fig = plt.figure(figsize=(10, 5)) 164 | ax1 = plt.subplot(2, 1, 1) 165 | ax2 = plt.subplot(2, 1, 2, sharex=ax1) 166 | ax2b = ax2.twinx() 167 | 168 | temperature_ymin = min([df['temperature_degF'].min(), 169 | df['dewpoint_degF'].min()]) - 5 170 | 171 | temperature_ymax = max([df['temperature_degF'].max(), 172 | df['dewpoint_degF'].max()]) + 5 173 | 174 | ax1.fill_between(df['UTC'], df['temperature_degF'], 175 | temperature_ymin, color='tab:red') 176 | 177 | ax1.fill_between(df['UTC'], df['dewpoint_degF'], 178 | temperature_ymin, color='tab:green') 179 | 180 | ax2.fill_between(df['UTC'], df['wind_speed_knots'], 181 | df['wind_speed_knots'].min() - 5, color='tab:blue') 182 | 183 | ax2b.scatter(df['UTC'], df['wind_direction_degrees'], 184 | edgecolor='tab:olive', color='None') 185 | 186 | # Set limits 187 | ax1.set_xlim(df['UTC'].min(), df['UTC'].max()) 188 | ax1.set_ylim(temperature_ymin, temperature_ymax) 189 | ax2.set_ylim(df['wind_speed_knots'].min() - 5, 190 | df['wind_speed_knots'].max() + 5) 191 | ax2b.set_ylim(-10, 370) # Wind Direction with a bit of padding 192 | 193 | # Add some labels 194 | label_fontsize = 14 195 | ax2.set_xlabel('Observation Time', fontsize=label_fontsize) 196 | ax1.set_ylabel(u'\N{DEGREE SIGN}F', fontsize=label_fontsize) 197 | ax2.set_ylabel('Knots', fontsize=label_fontsize) 198 | ax2b.set_ylabel('Degrees', fontsize=label_fontsize) 199 | 200 | return fig, ax1, ax2, ax2b 201 | -------------------------------------------------------------------------------- /meteogram/tests/test_meteogram.py: -------------------------------------------------------------------------------- 1 | """Test use of the meteogram module.""" 2 | 3 | from meteogram import meteogram 4 | 5 | 6 | # 7 | # Example starter test 8 | # 9 | def test_degF_to_degC_at_freezing(): 10 | """ 11 | Test if celsius conversion is correct at freezing. 12 | """ 13 | # Setup 14 | freezing_degF = 32.0 15 | freezing_degC = 0.0 16 | 17 | # Exercise 18 | result = meteogram.degF_to_degC(freezing_degF) 19 | 20 | # Verify 21 | assert result == freezing_degC 22 | 23 | # Cleanup - none necessary 24 | 25 | # 26 | # Instructor led introductory examples 27 | # 28 | 29 | # 30 | # Instructor led examples of numerical comparison 31 | # 32 | 33 | # 34 | # Exercise 1 35 | # 36 | def test_build_asos_request_url_single_digit_datetimes(): 37 | """ 38 | Test building URL with single digit month and day. 39 | """ 40 | pass 41 | 42 | 43 | def test_build_asos_request_url_double_digit_datetimes(): 44 | """ 45 | Test building URL with double digit month and day. 46 | """ 47 | pass 48 | 49 | # 50 | # Exercise 1 - Stop Here 51 | # 52 | 53 | # 54 | # Exercise 2 - Add calculation tests here 55 | # 56 | 57 | # 58 | # Exercise 2 - Stop Here 59 | # 60 | 61 | # 62 | # Instructor led mock example 63 | # 64 | 65 | # 66 | # Exercise 3 67 | # 68 | 69 | # 70 | # Exercise 3 - Stop Here 71 | # 72 | 73 | # 74 | # Exercise 4 - Add any tests that you can to increase the library coverage. 75 | # think of cases that may not change coverage, but should be tested 76 | # for as well. 77 | # 78 | 79 | # 80 | # Exercise 4 - Stop Here 81 | # 82 | 83 | # 84 | # Instructor led example of image testing 85 | # 86 | 87 | # 88 | # Exercise 5 89 | # 90 | 91 | # 92 | # Exercise 5 - Stop Here 93 | # 94 | 95 | # 96 | # Exercise 6 97 | # 98 | 99 | # 100 | # Exercise 6 - Stop Here 101 | # 102 | 103 | # 104 | # Exercise 7 105 | # 106 | 107 | # 108 | # Exercise 7 - Stop Here 109 | # 110 | 111 | # Demonstration of TDD here (time permitting) 112 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup script for installing meteogram.""" 2 | 3 | from setuptools import find_packages, setup 4 | 5 | 6 | setup( 7 | name='meteogram', 8 | version=0.1, 9 | description='Collection of tools for reading, visualizing and' 10 | 'performing calculations with weather data.', 11 | long_description='The space MetPy aims for is GEMPAK ' 12 | '(and maybe NCL)-like functionality, in a way that ' 13 | 'plugs easily into the existing scientific Python ' 14 | 'ecosystem (numpy, scipy, matplotlib).', 15 | 16 | url='http://github.com/Unidata/MetPy', 17 | 18 | author='John Leeman', 19 | author_email='john@leemangeophysical.com', 20 | maintainer='John Leeman', 21 | maintainer_email='john@leemangeophysical.com', 22 | 23 | license='BSD', 24 | 25 | classifiers=['Development Status :: 4 - Beta', 26 | 'Programming Language :: Python :: 3', 27 | 'Programming Language :: Python :: 3.6', 28 | 'Programming Language :: Python :: 3.7', 29 | 'Topic :: Scientific/Engineering', 30 | 'Topic :: Scientific/Engineering :: Atmospheric Science', 31 | 'Intended Audience :: Science/Research', 32 | 'Operating System :: OS Independent', 33 | 'License :: OSI Approved :: BSD License'], 34 | keywords='meteorology weather', 35 | 36 | python_requires='!=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*', 37 | install_requires=['matplotlib>=2.2.0', 'numpy>=1.12.0', 'pandas>=0.24.2'], 38 | extras_require={ 39 | 'test': ['pytest>=2.4', 'pytest-runner', 'pytest-mpl', 'pytest-flake8'] 40 | }, 41 | 42 | 43 | zip_safe=True) 44 | --------------------------------------------------------------------------------