rumpsでMacメニューバー常駐の天気アプリを作る！

# メニューバー常駐のアプリを作りたい !
メニューバーにあるアプリを作りたいとふと思い立ちました。
まず、メニューバーとはここ↓
<img width="393" alt="menubar.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635099/b19beb7e-f38d-8e19-ec60-368aefb5d0c6.png">
どんな事ができるのか勉強がてら、お天気アプリを作ることにします。

# rumps とは
Mac のメニューバーを開発するために、多くの人が Objective-C や Swift を利用すると思います。（多分）
Python で Mac のメニューバーを簡単に作るためのライブラリとして、 rumps があります。
https://github.com/jaredks/rumps
rumps が使われているアプリが紹介されているので、参考にさせてもらいました。

# 準備
rumps を使うために、必要なものをインストールしていきます。 python3 からいきましょう。
（既にインストール済みの方は読み飛ばしてください）
まず、 python のバージョンを簡単に切り替え可能にしてくる pyenv をインストールします。

```
$ brew install pyenv
```

pyenv をインストール後、ホームディレクトリの .bash_profile に下記を追加します。

```bash:.bash_profile
export PYENV_ROOT=${HOME}/.pyenv
if [ -d "${PYENV_ROOT}" ]; then
   export PATH=${PYENV_ROOT}/bin:$PATH
   eval "$(pyenv init -)"
fi
```

```
$ source ~/.bash_profile
```

```bash
$ pyenv install 3.7.7
$ pyenv global 3.7.7
$ python -V
Python 3.7.7
$ pip -V
pip 19.2.3 from /Users/ユーザー名/.pyenv/versions/3.7.7/lib/python3.7/site-packages/pip (python 3.7)
```

次に venv で環境構築していきます。
venv を使うと pip によるパッケージの導入状態をプロジェクトごとに管理できるので便利です。

```bash
$ mkdir weather
$ cd weather
$ python -m venv venv
$ . venv/bin/activate
(venv) $ pip install rumps
```

# 使い方
下記の rumps のサンプルプログラムから見ていきましょう。

```python:sample.py
import rumps

class RumpsTest(rumps.App):
    @rumps.clicked("Hello World")
    def hello_world(self, _): 
        rumps.alert("Hello World!")

    @rumps.clicked("Check")
    def check(self, sender):
        sender.state = not sender.state

    @rumps.clicked("Notify")
    def sayhello(self, _): 
        rumps.notification("Hello", "hello", "hello world")

if __name__ == "__main__":
    RumpsTest("Rumps Test").run()
```

```bash
(venv) $ python sample.py
```

プログラムを実行すると、Mac のメニューバーにRumps Testのタイトルが現れ、
Rumps Test のタイトルをクリックすると以下のように表示されます。
<img width="482" alt="sample_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635099/8213f335-0aeb-96d5-425d-ddd3d3d81212.png">

```python
@rumps.clicked("Hello World") # 関数の前に記述することで、一覧にクリック可能なHello Worldが追加される。
def hello_world(self, _):
    rumps.alert("Hello World!") # Hello World!のアラートを表示させる。
```
```python
sender.state = not sender.state # Checkがクリックされるたびに、sender.stateが 1 or 0 に切り替わる。
```
```python
rumps.notification("Hello", "hello","hello world") # title, subtitle, message が通知される。
```

このように簡単にメニューバーのアプリを作る事ができます。
それでは実際にお天気アプリを作っていきましょう。

# お天気アプリを作ってみる
今回、天気情報を得るために、livedoor Weather Hacks API を利用します。
http://weather.livedoor.com/weather_hacks/webservice
データを取ってくるために Python の requests を使います。

```bash
(venv) $ pip install requests
```

weather.pyというファイル名で作っていきます。

```python:weather.py
import rumps
import requests

# Livedoor weather api
URL = 'http://weather.livedoor.com/forecast/webservice/json/v1'


class Weather(rumps.App):
    @rumps.clicked("Get weather")
    def get_weather(self, _):
        payload = {'city': '130010'} # 130010は東京都の東京のエリアコードです。
        data = requests.get(URL, params=payload).json()
        tenki = data['forecasts'][0]['telop']
        rumps.alert(tenki)


if __name__ == "__main__":
    Weather("Weather").run()
```

Get weather をクリックすると東京都の東京のエリアを指定し、
天気予報を取得して、アラートに表示させる事ができました。

東京都東京のエリアコードを直接記入しているので、エリアコードが分からないと不便です。
エリアコード一覧を取得し、東京都 東京をキーにして、エリアコードを入れられるようにしましょう。
Livedoor が公開している xml を取得して辞書に格納していきます。


```python:weather.py
import rumps
import requests

# Livedoor weather api
URL = 'http://weather.livedoor.com/forecast/webservice/json/v1'

# City list
XML = "http://weather.livedoor.com/forecast/rss/primary_area.xml"


class Weather(rumps.App):
    @rumps.clicked("Get weather")
    def get_weather(self, _): 
        self.area = self.get_city()
        payload = {'city': self.area[('東京都', '東京')]} # エリアコードではなく、エリアを指定しました。
        data = requests.get(URL, params=payload).json()
        tenki = data['forecasts'][0]['telop']
        rumps.alert(tenki)

    def get_city(self):
        area = {}
        src = et.fromstring(requests.get(XML).text)[0][12]
        for c in src.findall("pref"):
            for cc in c.findall("city"):
                key = (c.attrib["title"], cc.attrib["title"])
                area[key] = cc.attrib["id"]
        return area # {('都道府県', '地域'): 'エリアコード'} の形で辞書になっています。


if __name__ == "__main__":
    Weather("Weather").run()
```

東京都 東京の天気を知ることはできましたが、他の地域の天気も見てみたいですよね？
続いて、地域一覧から選択した地域の天気情報を見れるようにしてみましょう。
地域をクリックして、その地域の天気情報をアラートに出したいと思います。

```python:weather.py
    @rumps.clicked("東京都 東京")
    def get_weather(self, _): 
        self.area = self.get_city()
        payload = {'city': self.area[('東京都', '東京')]}
```

全ての地域に対して、上記のように書くのは大変なので、少しいじっていきましょう。

```python:weather.py
class Weather(rumps.App):
    def __init__(self, name):
        super(Weather, self).__init__(
            "Weather",
            menu=[
                rumps.MenuItem("Get weather", callback=self.get_weather)
            ]   
        )   

    def get_weather(self, _): 
        self.area = self.get_city()
        payload = {'city': self.area[('東京都', '東京')]}
        data = requests.get(URL, params=payload).json()
        tenki = data['forecasts'][0]['telop']
        rumps.alert(tenki)

```

コンストラクタを入れて、get_weather のデコレーションを消しました。
デコレーションの代わりに、rumps.MenuItem を使ってほぼ同様に動作するように変更しました。
ここから地域を選択できるようにしていきたいと思います。

```python:weather.py
class Weather(rumps.App):
    def __init__(self, name):
        super(Weather, self).__init__(
            "Weather",
            menu=[
                self.build_area()
            ]
        )

    def build_area(self):
        self.area = self.get_city()
        menu = rumps.MenuItem("Area")
        for (pref, area), code in self.area.items():
            title = "{} {}".format(pref, area)
            menu[title] = rumps.MenuItem(title)
        return menu
```

<img width="449" alt="sample_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635099/642f1a2f-2891-0361-60b3-a25755e9d8fa.png">

ひとまず画像のようにできていると思います。
次に地域をクリックすることで、アラートに表示させるようにしてみましょう。

```python:weather.py
    def build_area(self):
        self.area = self.get_city()
        menu = rumps.MenuItem("Area")
        for (pref, area), code in self.area.items():
            def get_weather(sender):
                payload = {'city': code}
                data = requests.get(URL, params=payload).json()
                tenki = data['forecasts'][0]['telop']
                rumps.alert(tenki)
            title = "{} {}".format(pref, area)
            menu[title] = rumps.MenuItem(title, callback=get_weather)
        return menu
```

こんな感じにしてみました。
とりあえずここまでにして、このプログラムをアプリ化 ( weather.appの形に ) していきます。

```bash
(venv) $ pip install py2app
(venv) $ py2applet --make-setup weather.py
```

py2applet のコマンドを実行すると setup.py が生成されたと思います。
setup.py に少しだけ追加します。

```python:setup.py
"""                                                                           
This is a setup.py script generated by py2applet

Usage:
    python setup.py py2app
"""

from setuptools import setup

APP = ['weather.py']
DATA_FILES = []
OPTIONS = { 
        'plist': {
            'LSUIElement': True, # これを指定すると、Dockに表示されなくなります。
        }   
}

setup(
    app=APP,
    data_files=DATA_FILES,
    options={'py2app': OPTIONS},
    setup_requires=['py2app'],
)
```

```bash
(venv) $ python setup.py py2app
...
...
ValueError: '/Users/ユーザ名/.pyenv/versions/3.7.7/lib/libpython3.7.dylib' does not exist
```

自分の環境では、エラーが出ました。
libpython3.7.dylib がないので Python 3.7.7 再インストールします。
インストールの際に、--enable-shared をつけることでいけるという情報があり、実施してみます。

```bash
(venv) $ deactive
$ cd
$ pyenv uninstall 3.7.7
$ PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 3.7.7
$ ls /Users/ユーザ名/.pyenv/versions/3.7.7/lib/
libpython3.7m.dylib   libpkgconfig   python3.7
```

python3.7.7 では libpython3.7m.dylib になっていました。
(python3.8.0 ではちゃんと libpython3.8.dylib でした。)
仕方ないので、 libpython3.7m.dylib を libpython3.7.dylib にリネームしました。

```bash
$ mv /Users/ユーザ名/.pyenv/versions/3.7.7/lib/libpython3.7m.dylib /Users/ユーザ名/.pyenv/versions/3.7.7/lib/libpython3.7.dylib
```

もう一度、setup.py を実行していきます。

```bash
$ cd weather
$ . venv/bin/activate
(venv) $ python setup.py py2app
...
...
Done!
```

なんとかうまくいきました。
dist の中に入っているアプリを実行することでメニューバーアプリが動くと思います。

いかがだったでしょうか。簡単にお天気アプリが作れました!

他の方々が作ってらっしゃる rumps アプリを大変参考にさせていただきました。
https://shinaji.bitbucket.io/2014/09/08/manu_bar_app.html
https://github.com/rbrich/computer-time/


最後に自分で使いやすいように改良してみました。
<img width="500" alt="sample_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635099/a7c42325-f550-685e-3a11-0db2781b83f4.gif">

# 課題
- ネットワークが繋がっていない場合、操作不能にしたい
  pythonから通信せずにネットワークが繋がっているか知りたい。（一秒毎に確認したい）

- せっかくPythonを使っているので、機械学習的な機能を入れたい！！
  天気情報を使って、有用な情報をアプリを使っているユーザに提供したい。
  twitterとかの情報を使って、テキストマイニング系で何かしたいと思いました。

- テストコードが全然書けてない、、
  現状のカバレッジ率がひどいです。いつかやります笑