Dane pogodowe, opracowaliśmy – no, są. Co dalej?

Warto je gdzieś przechować, aby można było odczytać i przetwarzać w widgetach, tworząc obrazy albo zamieniając na mowę… w każdym razie do dalszego przetwarzania.

Załóżmy, że uznaliśmy zewnętrzną bazę danych jako dobre miejsce do przechowywania aktualnej pogody i najaktualniejszej prognozy. Z bazy danych łatwo i przyjemnie będzie je nam wydobyć w zasadzie dowolną technologią… o tym później.

 

Opowiem tutaj o sposobie odczytu a później (!) zapisu przy użyciu API.

Zbudujmy małe API (Application Programming Interface), które będziemy używać do zapisu i odczytu danych do bazy.

Bazą będzie MySQL, formatem zapisu i odczytu JSON. API będzie napędzane przez Flask.

Konfiguracją servera zajęliśmy się w innym miejscu, tutaj wspomnimy jedynie o czubaszku naszej lodowej gAPIóry.

Python ‚wapi.py’:

# -*- coding: utf-8 -*-

from flask import Flask, request, jsonify
from flask_restful import Resource, Api

import weather

app = Flask(__name__)
api = Api(app)

DOCS_PATH = u'http://wiks.eu/docs/api'

class Index(Resource):
    """
    powitalne info api,
    rozdziela ruch GET na poszczególne moduły
    """
    def get(self):
        """
        zwykła pusta GET to przywitanie i wskazanie ścieżki do dokumentacji

        :return:
        """

        result = {'message': 'Hey, docs are here :-)',
                  'url': DOCS_PATH,
                  }
        return jsonify(result)

# ogólna info:
api.add_resource(Index, '/')
api.add_resource(weather.Weather, '/weather')

if __name__ == '__main__':

    log_main.debug(u'APP running ' + unicode(datetime.now()) + u' ...')

    app.run(port=8664)

To co zdefiniowane powyżej to:

kodowanie strony, import bibliotek (w tym naszej weather dla API) , zdefiniowana klasa Index – pokaże opis co tu jest, dzięki temu będzie można opublikować ładną (lub taką sobie) dokumentację dla tych co zbłądzili i chcą z tej API skorzystać…

Dodajemy zasoby obsługujące wywołania (to ‚api.add. …’) dla zdefiniowanego ‚Index’ (gdy po adresie głównym nic nie będzie) oraz dla ‚weather’ (gdy po adresie głównym będzie ‚/weather’), oraz -jeśli to plik główny – odpalamy API – tutaj na porcie 8664 .

Teraz wnętrze pogodowej API, czyli wspomniany wcześniej importowany i dodawany zasób ‚weather’ i ‚weather.Weather’

# -*- coding: utf-8 -*-

from flask import request, jsonify
from flask_restful import Resource, Api

from sqlalchemy import create_engine
from sqlalchemy.engine.url import URL
import db_settings_creds

from datetime import datetime

path_for_create_engine_db = str(URL(**db_settings_creds.DATABASES_API))
db_connect = create_engine(path_for_create_engine_db + '?charset=utf8',
                           pool_pre_ping=True)
db_connect.encoding = 'utf-8'


class Weather(Resource):

    def get(self):
        """
        pytanie o pogodę metodą GET, zwraca:
        aktualną pogodę oraz najbliższą - na 12h prognozę,
        a także prognozę na kilka najbliższych dni

        :return: unicode --> dict --> JSON
        """
        conn = db_connect.connect()
        result = {'place': u'Poland, Świnoujście'}
        # część 1 sza:
        query1 = "SELECT "
        query1 += "* "
        query1 += "FROM `wiks_weather`.`openweather_weather` WHERE 1 ORDER BY `lp` DESC LIMIT 1"
        res1 = conn.execute(query1)
        one_row1 = res1.fetchone()
        w = {}
        if one_row1:
            if 'dt' in one_row1:
                result['dt'] = one_row1['dt'].strftime('%Y%m%d %H%M%S')
            if 'teraz' in one_row1:
                w['now'] = one_row1['teraz']
            if 'forecast' in one_row1:
                w['12h'] = one_row1['forecast']
        result['weather'] = w
        # część 2 ga:
        query2 = "SELECT "
        query2 += "`for_date`, `forec_descr` "
        query2 += "FROM `wiks_weather`.`openweather_my_text_3dforecast` " \
                  "WHERE `for_date` > CURDATE() ORDER BY `lp` DESC LIMIT 5"
        res2 = conn.execute(query2)
        f = {}
        if res2:
            for row2 in res2:
                f[row2['for_date'].strftime('%Y%m%d')] = row2['forec_descr']
        result['forecast'] = f
        return jsonify(result)

Z rzeczy powyżej, o których trzeba wspomnieć:

path_for_create_engine_db = str(URL(**db_settings_creds.DATABASES_API))
db_connect = create_engine(path_for_create_engine_db + ‚?charset=utf8’,
pool_pre_ping=True)
db_connect.encoding = ‚utf-8’

Tworzy – używając narzędzia SQLAlchemy (było wcześniej ‚from sqlalchemy import create_engine’ ) – połaczenie do bazy danych, przy użyciu zapisanych w pliku ‚db_settings_creds’ danych takich jak miejsce bazy, nazwa użytkownika i hasło. Zobaczmy:

DATABASES_API = {
    'drivername': 'mysql',
    'host': 'localhost',  
    'port': 3306,
    'username': 'nazwa_uzytkownika',
    'password': 'hasło_użytkownika',
    'database': 'wiks_weather',
}

API będzie działało na komputerze, na którym jest baza danych, dlatego jako host podajemy localhost (czyli baza jest local z punktu widzenia API). Gdyby -np do testów odpalać API z innego komputera – należy podać tutaj pełne IP.

Jako drivername – mysql, to dlatego, że dzięki SQLAlchemy możemy obsługiwać różne typy baz danych, a teraz chcemy właśnie MySQL.

Dodawany zasób ‚weather.Weather’ to: plik który importowaliśmy ‚weather’ oraz występująca w nim klasa ‚Weather’:

class Weather(Resource) i get(self):

To działa tak: wywołanie (lub wpisanie w okno przeglądarki internetowej) adresu naszego API wraz ‚/weather’ przeniesie nas do metody get klasy Weather, którą powyżej zdefiniowaliśmy.

Sprawdźmy: http://api.wiks.eu/weather

Oczywiście inny będzie nasz adres i możemy w nim zdefiniować cokolwiek…

def get i co w niej?

Połączenie z bazą danych (conn), przygotowanie zmiennej-słownika w którym umieścimy i wyślemy w świat wynik:

result = {‚place’: u’Poland, Świnoujście’}  – tak, tak, w formacie JSON będzie,

polecenie dla bazy danych:

"SELECT * FROM `wiks_weather`.`openweather_weather` WHERE 1 ORDER BY `lp` DESC LIMIT 1"

czyli: pobierz (SELECT) wszystko (*) z (FROM) bazy danych o nazwie (`wiks_weather`) i tabeli o nazwie (`openweather_weather`) gdzie (WHERE) 1 (tutaj oznacza to pobranie absolutnie wszystkich rekordów/ wierszy/ zapisów, bo wszystkie pasują do WHERE 1) uporządkowanych według kolumny ‚lp’ (ORDER BY `lp`) – uporządkowanych malejąco (DESC) i ogranicz wynik do jednego – pierwszego rekordu (LIMIT 1).

Właściwie można było napisać: „SELECT * FROM `wiks_weather`.`openweather_weather`” – wynik taki sam (ale miałbym mniej do opisywania, poza tym -wydało by się zbyt proste…) 🙂

Teraz wykonaj polecenie dla bazy danych (zwane query, a tutaj query1) – (conn.execute(query1)) i pobierz jeden wiersz z wyniku (one_row1 = res1.fetchone()):

res1 = conn.execute(query1)
one_row1 = res1.fetchone()

…następnie z pobranego wiersza wyniku , jeśli zawiera pola (czyli nazwy kolumn w naszej tabeli bazy danych) takie jak ‚dt’ itd., to przypisz ich wartości do pól w utworzonej zmiennej/słowniku ‚w’:

w = {}
if one_row1:
    if 'dt' in one_row1:
        result['dt'] = one_row1['dt'].strftime('%Y%m%d %H%M%S')
    if 'teraz' in one_row1:
        w['now'] = one_row1['teraz']
    if 'forecast' in one_row1:
        w['12h'] = one_row1['forecast']
result['weather'] = w

…dla pola ‚dt’ wykonujemy jeszcze zamianę na postać tekstową daty i czasu (.strftime(‚%Y%m%d %H%M%S’)), wynik zapisuję do ‚result’.

Po tym ‚result’ powinno wyglądać jakoś tak:

{
  "dt": "20180424 093000", 
  "place": "Poland, \u015awinouj\u015bcie", 
  "weather": {
    "12h": "w godz.14-... przelotny deszcz, temp. +9...+13\u00b0C, umiarkowany...do\u015b\u0107 silny wiatr (4-5B) pocz\u0105tkowo SW zmieniaj\u0105cy si\u0119 na W, ci\u015bnienie 1024 hPa, zachmurzenie 5-7/8, wilgotno\u015b\u0107 75...84 %", 
    "now": "bezchmurne niebo, temperatura +12\u00b0C, wiatr z kierunku SW 3B (ok.5m/s), ci\u015bnienie 1012hPa, zachmurzenie 0/8 (0%), wilgotno\u015b\u0107 66% "
  }
}

dalej – # część 2 ga pobieramy w podobny sposób prognozę (forecast) i dodajemy do wyniku.

u mnie wygląda to tak, -sprawdźmy: http://api.wiks.eu/weather

 

Mamy część pierwszą – odczyt, ale dane trzeba wpisać aby móc odczytać, a więc:

Zapis aktualnej pogody dla danego miejsca oznacza, że to co było wcześniej należy usunąć.

Polecenie dla bazy danych:

query2 = "DELETE FROM `wiks_weather`.`openweather_weather` WHERE `name` = '" + weather_now['name'] + "' "

czyli: usuń (DELETE) z bazy danych, tabeli gdzie ‚name’ = jakaś zadana wartość.
Ta zadana wartość to wpisana nazwa miejsca, np ‚Swinoujscie’.

Po tym wpisujemy:

query3 = "INSERT INTO `" + db_name + "`.`openweather_weather` ("
query3 += "`dt_python`, "
query3 += "`dt`, "
query3 += "`name`, `lat`, `lon`, "
query3 += "`opis_id`, "
query3 += "`opisEN`, `wiatr_predk`, `wiatr_kier`, "
query3 += "`wilgotnosc`, `cisnienie`, "
query3 += "`zachmurzenie`, `temp`, "
query3 += "`forecast`, "
query3 += "`teraz`"
query3 += ") VALUES ("
query3 += " '" + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "', "
query3 += " '" + weather_now['dt'] + "', "
query3 += " '" + weather_now['name'] + "', '" + str(weather_now['lat']) + "', '" + str(weather_now['lon']) + "', "
query3 += " '" + str(weather_now['opis_id']) + "', "
query3 += " '" + weather_now['opisEN'] + "', '" + str(weather_now['wiatr_predk']) + "', '" + str(weather_now['wiatr_kier']) + "', "
query3 += " '" + str(weather_now['wilgotnosc']) + "', '" + str(weather_now['cisnienie']) + "', "
query3 += " '" + str(weather_now['zachmurzenie']) + "', '" + str(weather_now['temp']) + "', "
query3 += " '" + rx_content['descr_12h'] + "', "
query3 += " '" + rx_content['descr_now'] + "'"
query3 += ");"

…i tu więcej kodu, ale wszystko dość intuicyjne:

wstaw (INSERT) do bazy danych, tabeli , (nazwy pól objęte nawiasem) wartości (VALUES) – lista wartości objętych nawiasem.

Wartości zostaną przesłane do API, i wpisane do tabeli. Teraz odczyt -jak opisano na początku poda te właśnie wartości.

Trochę pisania było… jak by to wyglądało przy (nieco) lepszym wykorzystaniu narzędzia SQLAlchemy?

Otóż opisywana kolenda DELETE:

query2 = "DELETE FROM `wiks_weather`.`openweather_weather` WHERE `name` = '" + weather_now['name'] + "' "

wygląda tak:

dele2 = delete(self.tbl_openweather_weather,
               self.tbl_openweather_weather.c.name == weather_now['name'])

zaś ostatnia komenda INSERT:

ins = self.tbl_openweather_weather.insert(values=dict(
      dt=weather_now['dt'],
      dt_python=datetime.now(),
      name=weather_now['name'],
      lat=weather_now['lat'],
      lon=weather_now['lon'],
      opisEN=weather_now['opisEN'],
      opis_id=weather_now['opis_id'],
      wiatr_predk=weather_now['wiatr_predk'],
      wiatr_kier=weather_now['wiatr_kier'],
      wilgotnosc=weather_now['wilgotnosc'],
      cisnienie=weather_now['cisnienie'],
      zachmurzenie=weather_now['zachmurzenie'],
      temp=weather_now['temp'],
      forecast=rx_content[0]['descr_12h'],
      teraz=rx_content[0]['descr_now'],
                      )
                    )

…o tym i innych – bardziej zaawansowanych, abstrakcyjnych i bardziej niezależnych sposobach wykorzystania SQLAlchemy napiszę nieco później.

Wróćmy teraz do zapisu pogody do API.

będziemy używać PUT (jednej z metod protokołu HTTP) , a więc w naszym Flaskowym API:

def put(self)

zaczniemy od odczytu tego co przyszło:

request_get_json = request.get_json()

a dalej wrzucimy do bazy danych. Zaraz zaraz, a jeśli ktoś inny wyśle złośliwie dane w taki sposób? Nooo to API je również przyjmie i wpisze do bazy… straszność ogromna!

Jak temu zaradzić?

Należałoby sprawdzić, czy użytkownik może i powinien… pierwszym ze sposobów – znane z czasów jaskiniowych (?) ktoś ty i podaj hasło!, czyli nazwa użytkownika i hasło, albo user i pass. Po stornie nadawcy i odbiorcy muszą być identyczne.

Czyli nadając dodatkowo dwa pola:

CREDS_WEATHER_API = {
    'user': 'jakis_user123',
    'pass': 'jakies_haslo123',

odbieramy również je i porównujemy do spodziewanych:

auth = False
if 'user' in request_get_json \
        and 'pass' in request_get_json \
        and 'content' in request_get_json:
    if request_get_json['user'] == CREDS['user'] \
            and request_get_json['pass'] == CREDS['pass']:
        auth = True

Przy czym nasza ‚auth’ będzie posiadała informacje czy użytkownik może (True) czy nie (False) dokonać zapisu…

no i teraz bułka z masłem, odczyt i do bazy…

Jak wysłać dane do API?

def send_weather_to_remote(content_list):
    """
    wyślij do zdalnego
    
    :param content_list: 
    :return: 
    """

    CREDS = db_settings_local.CREDS_WEATHER_API
    data = {
        'content': content_list,
        'user': CREDS['user'],
        'pass': CREDS['pass'],
    }
    headers = {"Content-Type": "application/json"}
    url = "http://---adres pod który wysyłamy ---"
    r = requests.put(url, data=json.dumps(data), headers=headers)

przy czym w pliku ‚db_settings_local’ zdefiniowałem „kto tam i hasło!”:

CREDS_WEATHER_API = {
    'user': 'jakis_user123',
    'pass': 'jakies_haslo123',
}

Aby nie przesyłać za każdym razem user & pass wymyślono coś, co nazywa się ‚OAuth2’ i nawet jest kilka miejsc gdzie to ładnie opisano.  Polecam poszukać!

Radości!

WikS.eu