mirror of
https://github.com/KevinMidboe/termForecast.git
synced 2025-10-29 09:50:18 +00:00
101
.gitignore
vendored
101
.gitignore
vendored
@@ -1,2 +1,99 @@
|
|||||||
.DS_Store
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__
|
__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/
|
||||||
|
|||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -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.
|
||||||
2
MANIFEST.in
Normal file
2
MANIFEST.in
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
include LICENSE
|
||||||
|
recursive-include docs *.gif
|
||||||
BIN
docs/demo.gif
Normal file
BIN
docs/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 773 KiB |
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
fire==0.1.1
|
||||||
|
geoip2==2.5.0
|
||||||
|
fuzzywuzzy==0.15.1
|
||||||
|
python-Levenshtein==0.12.0
|
||||||
38
setup.py
Executable file
38
setup.py
Executable file
@@ -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',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Before Width: | Height: | Size: 51 MiB After Width: | Height: | Size: 51 MiB |
127
termWeather/term_weather.py
Executable file
127
termWeather/term_weather.py
Executable file
@@ -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())
|
||||||
0
term_forecast/__init__.py
Executable file
0
term_forecast/__init__.py
Executable file
BIN
term_forecast/conf/GeoLite2-City.mmdb
Normal file
BIN
term_forecast/conf/GeoLite2-City.mmdb
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 MiB |
154
term_forecast/emojiParser.py
Executable file
154
term_forecast/emojiParser.py
Executable file
@@ -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()
|
||||||
40
term_forecast/loadingAnimation.py
Executable file
40
term_forecast/loadingAnimation.py
Executable file
@@ -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()
|
||||||
Reference in New Issue
Block a user