diff --git a/.gitignore b/.gitignore index 6a4dae1..113294a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,99 @@ -.DS_Store -__pycache__ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..93b9eee --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Kevin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..68738d9 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include LICENSE +recursive-include docs *.gif \ No newline at end of file diff --git a/docs/demo.gif b/docs/demo.gif new file mode 100644 index 0000000..3d4d4bd Binary files /dev/null and b/docs/demo.gif differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d417fee --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fire==0.1.1 +geoip2==2.5.0 +fuzzywuzzy==0.15.1 +python-Levenshtein==0.12.0 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..112f5d6 --- /dev/null +++ b/setup.py @@ -0,0 +1,38 @@ +from setuptools import setup + +with open('requirements.txt') as f: + requirements = f.read().splitlines() + +setup( + name='term-forecast', + version='0.1.dev0', + author='Kevin Midboe', + author_email='support@kevinmidboe.com', + + description='Terminal Forcast is a easily accessible terminal based weather forecaster', + url='https://github.com/KevinMidboe/termWeather/', + license='MIT', + + packages=['term_forecast'], + + classifiers = [ + "Environment :: Console", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3 :: Only", + "License :: OSI Approved :: MIT License", + "Development Status :: 4 - Beta", + + 'Operating System :: OS Independent', + 'Operating System :: POSIX', + 'Operating System :: MacOS', + 'Operating System :: Unix', + ], + + install_requires=requirements, + + entry_points={ + 'console_scripts': [ + 'forecast = term_forecast.term_weather:main', + ], + } +) \ No newline at end of file diff --git a/conf/GeoLite2-City.mmdb b/termWeather/conf/GeoLite2-City.mmdb similarity index 100% rename from conf/GeoLite2-City.mmdb rename to termWeather/conf/GeoLite2-City.mmdb diff --git a/emojiParser.py b/termWeather/emojiParser.py similarity index 100% rename from emojiParser.py rename to termWeather/emojiParser.py diff --git a/loadingAnimation.py b/termWeather/loadingAnimation.py similarity index 100% rename from loadingAnimation.py rename to termWeather/loadingAnimation.py diff --git a/termWeather/term_weather.py b/termWeather/term_weather.py new file mode 100755 index 0000000..d2766db --- /dev/null +++ b/termWeather/term_weather.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3.6 +# -*- coding: utf-8 -*- +# @Author: KevinMidboe +# @Date: 2017-07-27 21:26:53 +# @Last Modified by: KevinMidboe +# @Last Modified time: 2017-07-30 18:56:27 + +# TODO LIST +# Get coordinates from IP ✔ +# Fetch coordinates from YR ✔ +# Convert coordinates to place name w/ google GeoCode api ✔ +# Parse return data +# Match weather description to icons ✔ +# Check internet connection in a strict way +# Add table for time periode +# Add cache for quicker location for same ip + +import fire, json, geoip2.database, ssl, os +from yr.libyr import Yr +from requests import get +from pprint import pprint +from sys import stdout + +from emojiParser import EmojiParser +from loadingAnimation import LoadingAnimation + + +class Location(object): + def __init__(self): + + abspath = os.path.abspath(__file__) + dname = os.path.dirname(abspath) + os.chdir(dname) + self.reader = geoip2.database.Reader('conf/GeoLite2-City.mmdb') + self.getIP() + + def getIP(self): + ip = get('https://api.ipify.org').text + self.ip = self.reader.city(ip) + + def getCoordinates(self): + lat = self.ip.location.latitude + long = self.ip.location.longitude + return [lat, long] + + def getAreaName(self): + lat, long = self.getCoordinates() + coordinates = ','.join([str(lat), str(long)]) + areaURL = 'https://maps.googleapis.com/maps/api/geocode/json?&latlng=' + + areaAPIResponse = json.loads(get(areaURL + coordinates).text) + closestArea = areaAPIResponse['results'][0]['address_components'] + + area = {} + + for item in closestArea: + if 'route' in item['types']: + area['street'] = item['long_name'] + + if 'locality' in item['types']: + area['town'] = item['long_name'] + + if 'administrative_area_level_1' in item['types']: + area['municipality'] = item['long_name'] + + if 'country' in item['types']: + area['country'] = item['long_name'] + + return area + + +class WeatherForecast(object): + def __init__(self, area=None): + # TODO search for area coordinates in a map + self.area = area + + self.name = None + self.number = None + self.variable = None + + def symbolVariables(self, symbol_obj): + self.name = symbol_obj['@name'] + self.number = symbol_obj['@number'] + self.variable = symbol_obj['@var'] + + def parseYrTemperature(self, temperature_obj): + return temperature_obj['@value'] + ' ' + temperature_obj['@unit'] + + def now(self): + location = Location() + self.area = location.getAreaName() + + # Create seperate function for formatting + locationName = '/'.join([self.area['country'], self.area['municipality'], self.area['town'], self.area['street']]) + + # Use the defined location name with yr API for location based weather information + weather = Yr(location_name=locationName) + now = json.loads(weather.now(as_json=True)) + + + temperature_output = self.parseYrTemperature(now['temperature']) + + emojiParser = EmojiParser(now['symbol']['@name']) + weatherIcon_output = emojiParser.convertSematicsToEmoji() + + return ('%s %s' % (temperature_output, weatherIcon_output)) + + +class TermWeather(object): + # Add now, forecast as args + def auto(self): + loadingAnimation = LoadingAnimation() + loadingAnimation.start() + weatherForecast = WeatherForecast() + forecast = weatherForecast.now() + loadingAnimation.stop() + stdout.write('\r%s \n' % forecast) + + def fetch(self, area=None): + weatherForecast = WeatherForcast(area) + weatherForecast.now() + + +if __name__ == '__main__': + ssl._create_default_https_context = ssl._create_unverified_context + + fire.Fire(TermWeather()) \ No newline at end of file diff --git a/term_forecast/__init__.py b/term_forecast/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/term_forecast/conf/GeoLite2-City.mmdb b/term_forecast/conf/GeoLite2-City.mmdb new file mode 100644 index 0000000..e7839d8 Binary files /dev/null and b/term_forecast/conf/GeoLite2-City.mmdb differ diff --git a/term_forecast/emojiParser.py b/term_forecast/emojiParser.py new file mode 100755 index 0000000..aff67a2 --- /dev/null +++ b/term_forecast/emojiParser.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3.6 +# -*- coding: utf-8 -*- +# @Author: KevinMidboe +# @Date: 2017-07-29 11:56:24 +# @Last Modified by: KevinMidboe +# @Last Modified time: 2017-07-30 13:17:19 + +from fuzzywuzzy import process + +# Find the first word, if it is a noun or a adjective. ✔️ +# Remove the adjective and split if there is a AND ✔️ +# Then match the first noun to list and add that emoji ✔️ +# and then match the second to list and add that emoji ✔️ +# REGEX this bitch up + +symbol_table = { + 'clear sky': '☀️', + 'fair': '🌤', + 'partly cloudy': '⛅️', + 'cloudy': ' ☁️ ', + 'thunder': '⚡️', + + 'rain showers': '🌦', + 'rain': '🌧', + 'sleet showers': '🌦 💦', + 'sleet': '🌨 💦', + 'snow showers': '⛅ ❄️', + 'snow': '🌨', + + 'rain': '🌧', + 'sleet': '🌧', + 'snow': '🌨', + + 'showers': '🌤' + } + +severity = { + 'rain': ['', ' ☂️', ' ☔️'], + 'sleet': [' 💦 ', ' 💧 ', ' 💧 💦 '], + 'snow': [' ❄️ ', ' ❄️ ❄️ ', ' ❄️ ❄️ ❄️ '] + } + +class EmojiParser(object): + def __init__(self, condition_text): + self.condition_expression = condition_text.lower() + self.severity = None + self.nouns = [] + + self.weather_nouns = ['cleary sky', 'fair', 'cloudy', 'rain', 'rain showers', 'sleet', + 'sleet showers', 'snow showers', 'thunder', 'snow'] + self.weather_adjectives = {'light': 0, 'normal': 1, 'heavy': 2} + + def __str__(self): + return str([self.condition_expression, self.severity, self.nouns]) + + # Splits and lowers the condition text for easier parsing + def splitCondition(self, condition): + return condition.split() + + # Takes a input or uses condition_expression to find adjective in sentence + def findAdjective(self, sentence=None): + if sentence is None: + sentence = self.condition_expression + + # Splits and iterates over each word in sentence + expression = self.splitCondition(sentence) + for word in expression: + if word in self.weather_adjectives: + # Return the word if matched with weather_adjectives + return word + + return None + + # Removes the first adjective in the a given sentence + def removeAdjective(self): + adjective = self.findAdjective() + if adjective: # Adjective is not None + expression = self.splitCondition(self.condition_expression) + expression.remove(adjective) + return ' '.join(expression) + else: + return self.condition_expression + + + def severityValue(self): + adjective = self.findAdjective() + + if adjective: + self.severity = self.weather_adjectives[adjective] + else: + self.severity = self.weather_adjectives['normal'] + + def findWeatherTokens(self): + # If present removes the leading adjective + sentence = self.removeAdjective() + + # If multiple tokens/weather_nouns split all between the 'and' + if 'and' in sentence: + self.nouns = sentence.split(' and ') + else: + self.nouns = [sentence] + + + # Use the symbol_table to convert the forecast name to emoji + def emojify(self, noun): + return symbol_table[noun] + + # Does as emojify above, but iterates over a list if multiple elements + def emojifyList(self, noun_list): + returnList = [] + + # TODO use more like a map function? + for noun in noun_list: + returnList.append(self.emojify(noun)) + + return ' '.join(returnList) + + def findPrimaryForecast(self): + # Copies the contents not the refrence to the list + noun_list = list(self.nouns) + forecast = noun_list.pop(0) + + # Translates to emoji once here instead of twice below + forecast_emoji = self.emojify(forecast) + + if forecast in severity: + return ('%s %s' % (forecast_emoji, severity[forecast])) + else: + return forecast_emoji + + + # Trying to analyze the semantics of the condition text + def emojifyWeatherForecast(self): + # Finds the tokens/nouns of weather for the given input text and severity value + self.findWeatherTokens() + self.severityValue() + + primary_forecast = self.findPrimaryForecast() + secondary_forecast = self.emojifyList(self.nouns[1:]) + + return ('%s %s' % (primary_forecast, secondary_forecast)) + + + def convertSematicsToEmoji(self): + return self.emojifyWeatherForecast() + + +def main(): + emojiParser = EmojiParser('Cloudy') + print(emojiParser.convertSematicsToEmoji()) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/term_forecast/loadingAnimation.py b/term_forecast/loadingAnimation.py new file mode 100755 index 0000000..1fc9436 --- /dev/null +++ b/term_forecast/loadingAnimation.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3.6 +# -*- coding: utf-8 -*- +# @Author: KevinMidboe +# @Date: 2017-07-30 13:53:38 +# @Last Modified by: KevinMidboe +# @Last Modified time: 2017-07-30 13:53:46 + +import itertools +from threading import Thread +from time import sleep +from sys import stdout + +class LoadingAnimation(object): + def __init__(self): + self.done = False + + def start(self): + t = Thread(target=self.animate) + t.start() + + def animate(self): + for c in itertools.cycle(['|', '/', '-', '\\']): + if self.done: + break + stdout.write('\rFetching ' + c) + stdout.flush() + sleep(0.1) + + def stop(self): + self.done = True + +def main(): + loadingAnimation = LoadingAnimation() + loadingAnimation.start() + sleep(2) + loadingAnimation.stop() + stdout.write('\rTemp \n') + +if __name__ == '__main__': + main() \ No newline at end of file