Standardem staje się że witam się z Wami rozpoczynając zdanie od „dawno tu nie pisałem” – dziś, niestety nie będzie inaczej – więc witam Was, dawno tu nic nie pisałem. Ale co nieco nadrobię. A mianowicie pokaże jak szybko i łatwo można zrobić stabilne API w Pythonie.
Używać będziemy swaggera, flaska i connexion’a – trzy biblioteki, pierwsza pozwala stworzyć API, tj jego strukturę, przyjmowane parametry itp, druga to mini serwer HTTP który będzie obsługiwał żądania a trzecia pozwoli połączyć nam definicję naszego API z właściwymi metodami które będą wykonywane w momencie nadejścia żądania. A i jeszcze SqlAlchemy – dość duży ORM, ale zdecydowanie ułatwia manipulacje na bazie danych.
No więc zacznijmy od.. Początku. Stwórzmy plik main.py:
1 2 3 4 5 | import connexion app = connexion.App(__name__, specification_dir='specs/') app.add_api('api10.yaml') app.run(port=8080) |
importujemy w nim connexion’a który opakowuje flaska pozwalając uruchomić serwer HTTP na porcie 8080. W przedostatniej linii wskazujemy plik z naszą specyfikacją. Plik będzie szukany w folderze zdefiniowanym argumentem specification_dir w linijce wyżej.
Podstawy specyfikacji swaggera – czyli pliku yaml (inny format danych, coś jak JSON) który przechowuje definicję naszego API. Minimalna wersja która nic nie robi wygląda tak:
1 2 3 4 5 6 7 8 | swagger: '2.0' info: title: CD's shelf' version: "1.0" consumes: - application/json produces: - application/json |
W sumie to tylko definiujemy jaki format danych otrzymujemy (Content-Type) i jaki deklarujemy się zwracać. Rozszerzmy to o pierwszą akcje:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | swagger: '2.0' info: title: CD's shelf' version: "1.0" consumes: - application/json produces: - application/json basePath: /1.0 paths: /cd: get: operationId: app.cd.get_cds summary: Get all cds responses: 200: description: Return cds schema: $ref: '#/definitions/CDExt' |
dodaliśmy definicję akcji która pod adresem „/cd” i metodą GET zwróci nam wszystkie płyty CD z naszej półki. Zauważ że w sekcji responses zdefiniowaliśmy jaki kod HTTP co oznacza oraz schemat odpowiedzi. Zazwyczaj pobierając dane chcemy pobierać je wraz z IDkiem elementu tak żeby ewentualnie móc go później edytować – dlatego korzystamy tutaj z schematu odpowiedzi „CDExt”.
Definicja schematu odpowiedzi wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | definitions: CD: type: object required: - artist - title properties: artist: type: string description: CD artists name example: "VNM" minLength: 1 maxLength: 100 title: type: string description: Title of CD example: "EDKT" minLength: 1 CDExt: type: object allOf: - $ref: '#/definitions/CD' - properties: cd_id: type: integer |
Określa ona dwa schematy, jeden to CD i posiada pola takie jak artist i title. Schemat CDExt rozszerza schemat CD o pole „cd_id”. Skoro mamy już definicję bazową (CD) to skorzystajmy z niej i napiszmy definicję dla metody POST:
1 2 3 4 5 6 7 8 9 10 11 12 13 | post: operationId: app.cd.post summary: Create a CD parameters: - name: cd in: body schema: $ref: '#/definitions/CD' responses: 200: description: CD created schema: $ref: '#/definitions/CDExt' |
Definiujemy teraz że w ciele zapytania metodą POST ma się znajdować JSON o definicji zgodnej z schematem CD. Pole operationId które pominąłem opowiadając o metodzie GET wskazuje ścieżkę do metody która ma się uruchomić w momencie przyjścia żądania. I w taki sposób mamy zdefiniowane metody do pobierania i tworzenia encji płyty CD. Dodajmy metodę która zwróci konkretny, wskazany w parametrze obiekt:
1 2 3 4 5 6 7 8 9 10 11 12 13 | /cd/{cd_id}: get: operationId: app.cd.get_cd summary: Get a cd parameters: - $ref: '#/parameters/cd_id' responses: 200: description: Return cd schema: $ref: '#/definitions/CD' 404: description: CD does not exist |
Tutaj jedyna różnica to użycie parametru, który możemy zdefiniować tak:
1 2 3 4 5 6 7 | parameters: cd_id: name: cd_id description: CD ID in: path type: integer required: true |
Taki zapis powoduje że będziemy oczekiwać żądania na adres w stylu cd/{cd_id}.
Podobnie zachowamy się dla PUT’a i DELETE’a:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | delete: operationId: app.cd.delete_cd summary: Remove cd parameters: - $ref: '#/parameters/cd_id' responses: 204: description: CD was deleted 404: description: CD does not exist put: operationId: app.cd.put summary: Update a CD parameters: - $ref: '#/parameters/cd_id' - name: cd in: body schema: $ref: '#/definitions/CD' responses: 200: description: CD updated schema: $ref: '#/definitions/CDExt' 404: description: CD not found! |
No i fajnie – mamy zdefiniowane całe nasze API umożliwiające modyfikacje płyt CD na naszej „półce”.
Zgodnie z polem „operationId” stworzymy metody odpowiadające naszej definicji – tworzymy folder o nazwie „app” a w nim plik __init__.py i obok niego plik cd.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | def get_cds(): pass def get_cd(cd_id): pass def put(cd_id, cd=None): pass def delete_cd(cd_id): pass def post(cd=None): pass |
Już teraz uruchomienie naszej aplikacji poprzez
1 2 | mmazurek@mmazurek ~/wpisconnectionx> python main.py * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit) |
i otworzenie w przeglądarce
1 | http://localhost:8080/1.0/ui/ |
A jak klikniemy na „List Operations” to mamy listę naszych endpointów:
Natomiast „Expand operations” pokaże nam wygenerowaną dokumentację do naszego API wraz z przykładami i możliwością testowania:
jest to ogromna wartość dodana, ponieważ modyfikując definicję automatycznie tworzymy dokumentację. Ogólnie pobaw się – fajnie.
No ale żeby to mogło faktycznie coś robić – powinno modyfikować bazę danych. Skorzystajmy z wcześniej wspomnianego ORMa i na wysokości pliku main.py stwórzmy folder „db” a w nim pliku __init__.py z zawartością:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | from sqlalchemy import * from sqlalchemy.engine.url import URL from sqlalchemy.orm import sessionmaker, scoped_session from sqlalchemy.ext.declarative import declarative_base import settings def db_connect(): """ Performs database connection using database settings from settings.py. Returns sqlalchemy engine instance """ return create_engine(URL(**settings.DATABASE)) def row2dict(row): d = {} for column in row.__table__.columns: d[column.name] = str(getattr(row, column.name)) return d session = scoped_session(sessionmaker(bind=db_connect())) Base = declarative_base() |
i obok niego plik settings.py:
1 2 3 4 5 6 7 8 | DATABASE = { 'drivername': 'postgres', 'host': 'localhost', 'port': '5432', 'username': 'test', 'password': 'test', 'database': 'wpis2' } |
Oczywiście należy mieć postgresa – ja użyłem do tego dockera – https://hub.docker.com/_/postgres/ więc u mnie wystaczy docker start postgres – u Ciebie nie wiele więcej ;)
PgAdminem logujemy się do postgresa i w schemacie public tworzymy tabelkę:
1 2 3 4 5 6 7 8 9 10 11 12 | CREATE TABLE cds ( id serial NOT NULL, artist CHARACTER VARYING(100), title CHARACTER VARYING(100), CONSTRAINT pk PRIMARY KEY (id) ) WITH ( OIDS=FALSE ); ALTER TABLE cds OWNER TO postgres; |
I wracając do kodu – w folderze db stwórzmy folder „models” i w nim plik cd.py z zawartością:
1 2 3 4 5 6 7 8 9 10 11 12 13 | from db import Base from sqlalchemy import Column, Integer, String class CD(Base): __tablename__ = 'cds' id = Column(Integer, primary_key=True) artist = Column(String) title = Column(String) def __repr__(self): return '<id {}>'.format(self.id) |
czyli po prostu mapujemy naszą tabelkę na obiekt. Teraz możemy już zaimplementować nasz plik cd.py z katalogu app:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | from db import session, row2dict from db.models.cd import CD def get_cds(collected=None): result = session.query(CD).all() rows = [] for row in result: rows.append(row2dict(row)) return rows, 200 def get_cd(cd_id): result = session.query(CD).get(cd_id) if not result: return None, 404 else: return row2dict(result), 200 def put(cd_id, cd=None): cd_obj = session.query(CD).get(cd_id) if not cd_obj: return None, 404 for key, value in cd.iteritems(): setattr(cd_obj, key, value) session.merge(cd_obj) session.commit() session.flush() return row2dict(cd_obj), 200 def delete_cd(cd_id): cd_obj = session.query(CD).get(cd_id) if not cd_obj: return None, 404 session.delete(cd_obj) session.commit() session.flush() return None, 204 def post(cd=None): cd = CD(**cd) session.add(cd) session.commit() session.flush() |
i cieszyć się stabilnym i spójnym API. Od teraz wszystkie akcje na tej stronie swaggera, będą faktycznie modyfikować bazę danych. Warto dodać, bo pominąłem to, że swagger w oparciu o definicje przeprowadza również walidacje przesyłanych danych a samo definiowanie schematów – pozwala uniknąć sytuacji gdzie raz jakaś dana jest wysyłana jako int a raz jako string.
Bierzcie i radujcie się z tego wszyscy :)
PS. Projekt udostępniam na githubie – github
Oj daaawnoo mnie tu nie było. Ale wakacje to był czas dużej liczby intensywnych wyjazdów i tak naprawdę, dopiero jakoś… Read More
Cześć! Zapraszam na krótkie podsumowanie kwietnia. Wyjazd do Niemiec A dokładniej pod granicę z Francją. Chrześnica miała pierwszą komunię. Po… Read More
Ostatnio tygodnie były tak bardzo wypełnione, że nie udało mi się napisać nawet krótkiego podsumowanie. Więc dziś zbiorczo podsumuję luty… Read More
Zapraszam na krótkie podsumowanie miesiąca. Książki W styczniu przeczytałem "Homo Deus: Historia jutra". Książka łudząco podoba do wcześniejszej książki tego… Read More
Cześć! Zapraszam na podsumowanie roku 2023. Książki Zacznijmy od książek. W tym roku cel 35 książek nie został osiągnięty. Niemniej… Read More
Zapraszam na krótkie podsumowanie miesiąca. Książki W grudniu skończyłem czytać Mein Kampf. Nudna książka. Ciekawsze fragmenty można by było streścić… Read More