From 729b179d438e3c965b13255d528c73020d0d11ad Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 9 Mar 2022 17:43:06 -0500 Subject: [PATCH] Lab is ready --- .gitignore | 2 + globalize_weather.sh | 13 +++ poetry.lock | 180 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 7 +- weather/weather.py | 21 +++++ weather/weather_apis.py | 80 ++++++++++++++++++ weather/weather_cli.py | 15 ++++ 7 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 globalize_weather.sh create mode 100644 poetry.lock create mode 100644 weather/weather.py create mode 100644 weather/weather_apis.py create mode 100644 weather/weather_cli.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a6fb03 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist/* +**/__pycache__/* diff --git a/globalize_weather.sh b/globalize_weather.sh new file mode 100644 index 0000000..78bfd91 --- /dev/null +++ b/globalize_weather.sh @@ -0,0 +1,13 @@ +# This shell shell script adds `weather` to your global PATH, so that you'll be able to +# run it from any Terminal session. If you want this, just run: +# +# source globalize_weather.sh + +CHECK=$(which weather 2>&1) +if [ $CHECK = "weather not found" ]; then + echo "The weather script was not found. Make sure you are in a Poetry shell and that you have run poetry install." +else + echo "\n# Adding path for weather" >> ~/.mwc_rc + echo "export PATH=\"$(dirname $(which weather)):\$PATH\"" >> ~/.mwc_rc + source ~/.mwc_rc +fi diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..604fe30 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,180 @@ +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.0.4" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "future" +version = "0.18.2" +description = "Clean single-source support for Python 3 and 2" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "geocoder" +version = "1.38.1" +description = "Geocoder is a simple and consistent geocoding library." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +click = "*" +future = "*" +ratelim = "*" +requests = "*" +six = "*" + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "ratelim" +version = "0.1.6" +description = "Makes it easy to respect rate limits." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +decorator = "*" + +[[package]] +name = "requests" +version = "2.27.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "urllib3" +version = "1.26.8" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.9" +content-hash = "8db6132dd735c9575de75a5b220e83f601f31e184320c37589cb255b78084b19" + +[metadata.files] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, +] +click = [ + {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, + {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +decorator = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] +future = [ + {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, +] +geocoder = [ + {file = "geocoder-1.38.1-py2.py3-none-any.whl", hash = "sha256:a733e1dfbce3f4e1a526cac03aadcedb8ed1239cf55bd7f3a23c60075121a834"}, + {file = "geocoder-1.38.1.tar.gz", hash = "sha256:c9925374c961577d0aee403b09e6f8ea1971d913f011f00ca70c76beaf7a77e7"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +ratelim = [ + {file = "ratelim-0.1.6-py2.py3-none-any.whl", hash = "sha256:e1a7dd39e6b552b7cc7f52169cd66cdb826a1a30198e355d7016012987c9ad08"}, + {file = "ratelim-0.1.6.tar.gz", hash = "sha256:826d32177e11f9a12831901c9fda6679fd5bbea3605910820167088f5acbb11d"}, +] +requests = [ + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +urllib3 = [ + {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, + {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, +] diff --git a/pyproject.toml b/pyproject.toml index 4063ba2..8f0897e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "mwc-pedprog-unit01-lab02" +name = "weather" version = "0.1.0" description = "" authors = ["Chris "] @@ -7,9 +7,14 @@ license = "MIT" [tool.poetry.dependencies] python = "^3.9" +requests = "^2.27.1" +geocoder = "^1.38.1" [tool.poetry.dev-dependencies] [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +weather = "weather.weather_cli:weather_cli" diff --git a/weather/weather.py b/weather/weather.py new file mode 100644 index 0000000..84e9637 --- /dev/null +++ b/weather/weather.py @@ -0,0 +1,21 @@ +from weather.weather_apis import ( + geocode_location, + geocode_ip_address, + get_weather_office, + get_forecast +) + +def print_weather(location=None, metric=False, verbose=False): + """Prints out a weather report using the provided location, or using + the user's current location if no location was provided. + When metric is True, prints out the weather in metric units. + When verbose is True, prints out a more detailed report. + """ + print("Not finished...") # YOUR CODE HERE! + +# This is a clunky way to check whether this module was called directly with `python weather.py`, +# or whether it's being imported by another module. If the module is being called, then we +# should actually run `print_weather`. But if this module is just being imported, we probably don't +# want this module to call any functions. We'll leave that up whoever is doing the importing. +if __name__ == "__main__": + print_weather() diff --git a/weather/weather_apis.py b/weather/weather_apis.py new file mode 100644 index 0000000..a13d36a --- /dev/null +++ b/weather/weather_apis.py @@ -0,0 +1,80 @@ +# Weather APIs + +# This module contains functions which interact with external APIs related to weather. +# The module relies on USA-specific services; it will need to be extended using local +# services for other regions. + +# The National Weather Service (NWS) provides weather forecasting services across US +# states and territories. NWS divides the country into a grid of 2.5km squares, and +# provides a forecast for each grid square. + +import geocoder +import requests + +def geocode_location(location_string): + """Translates a location string into latitude and longitude coordinates. + Uses the OpenStreetMap API. Returns a dict with keys 'lat' and 'lng' + as shown below. When no result is found, returns None. + + >>> geocode_location('11 Wall Street, New York') + {"lat": -74.010865, "lng": 40.7071407} + """ + result = geocoder.osm(location_string) + if result: + lat, lng = result.latlng + return {'lat': lat, 'lng': lng} + +def geocode_ip_address(ip_address=None): + """Translates an IP address into latitude and longitude coodrdinates. + When no IP address is provided, uses the user's current IP address. + + >>> geocode_ip_address() + {'lat': 23.6585116, 'lng': -102.0077097} + """ + result = geocoder.ip(ip_address or 'me') + if result: + lat, lng = result.latlng + return {'lat': lat, 'lng': lng} + +def get_weather_office(lat, lng): + """Looks up the NWS weather office for a pair of lat/lng coordinates. + Returns a dict containing keys 'office', 'x', and 'y'. + If no matching weather office is found, returns None. + + >>> coords = geocode_ip_address() + >>> get_weather_office(coords['lat'], coords['lng']) + {'office': 'BUF', 'x': 39, 'y': 59} + """ + url = "https://api.weather.gov/points/{},{}".format(lat, lng) + response = requests.get(url) + if response.ok: + result = response.json() + return { + "office": result['properties']['gridId'], + "x": result['properties']['gridX'], + 'y': result['properties']['gridY'] + } + +def get_forecast(office, x, y, metric=False): + """Fetches the weather forecast for the given NWS office, and (x, y) NWS grid tile. + Returns a list of time periods, where each time period is a dict containing keys + as shown below. If no forecast can be found, returns None. + When metric is True, returns temperatures in Celcius and wind speeds in km/hr. + + """ + url = "https://api.weather.gov/gridpoints/{}/{},{}/forecast".format(office, x, y) + if metric: + url += "?units=si" + response = requests.get(url) + if response.ok: + result = response.json() + forecast = [] + for period in result['properties']['periods']: + forecast.append({ + 'name': period['name'], + 'temperature': period['temperature'], + 'wind_speed': period['windSpeed'], + 'wind_direction': period['windDirection'], + 'description': period['shortForecast'], + }) + return forecast diff --git a/weather/weather_cli.py b/weather/weather_cli.py new file mode 100644 index 0000000..f454dc2 --- /dev/null +++ b/weather/weather_cli.py @@ -0,0 +1,15 @@ +from argparse import ArgumentParser +from weather.weather import print_weather + +def weather_cli(): + """Provides a command-line interface for weather. + This function creates an ArgumentParser, which parses command line arguments. + Then it calls `print_weather` with the provided arguments. + """ + parser = ArgumentParser("weather", description="Prints out a weather report") + parser.add_argument("-l", "--location", help="Location for weather forecast") + parser.add_argument("-m", "--metric", action="store_true", help="Use metric units") + parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output") + args = parser.parse_args() + print_weather(location=args.location, metric=args.metric, verbose=args.verbose) +