API weather

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

Loading Disqus Comments ...
Loading Facebook Comments ...

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *