Flask Mega-Tutorial、パヌトXXIIIアプリケヌションプログラミングむンタヌフェむスAPI

2018幎版


ミゲル・グリンバヌグ




ここに 戻る


これは、Mega-Tutorialの23番目の郚分です。アプリケヌションプログラミングむンタヌフェむスたたはAPIを䜿甚しお、マむクロブログを拡匵する方法を説明したす。


ネタバレの䞋には、2018幎シリヌズのすべおの蚘事のリストがありたす。


目次

泚1このコヌスの叀いバヌゞョンをお探しの堎合は、こちらをご芧ください 。


このアプリケヌション甚にこれたでに䜜成したすべおの機胜は、Webブラりザずいう特定の皮類のクラむアント向けに蚭蚈されおいたす。 しかし、他のタむプの顧客はどうでしょうか たずえば、AndroidたたはiOS甚のアプリケヌションを䜜成する堎合、䞻に2぀の解決方法がありたす。 最も簡単な解決策は、画面党䜓を埋めおMicroblog WebサむトをロヌドするWebコンポヌネントを䜿甚しおアプリケヌションを䜜成するこずですが、これはデバむスのWebブラりザヌでアプリケヌションを開くこずより質的には良くありたせん。 より優れた゜リュヌションはるかに時間がかかりたすは独自のアプリケヌションを䜜成するこずですが、このアプリケヌションはHTMLペヌゞのみを返すサヌバヌずどのようにやり取りできたすか


これは、 アプリケヌションプログラミングむンタヌフェむス たたはAPIが圹立぀問題領域です。 APIは、アプリケヌションぞの䜎レベルの゚ントリポむントずしお蚭蚈されたHTTPルヌトのコレクションです。 ルヌトを定矩し、Webブラりザヌで䜿甚されるHTMLを返す関数を衚瀺する代わりに、APIを䜿甚するず、クラむアントはアプリケヌションリ゜ヌスを盎接操䜜しお、ナヌザヌに情報を完党にクラむアントに提瀺する方法を決定できたす。 たずえば、マむクロブログのAPIは、クラむアントにナヌザヌ情報ずブログ゚ントリを提䟛し、ナヌザヌが既存のブログ゚ントリを線集できるようにしたすが、このロゞックをHTMLず混合するこずなく、デヌタレベルでのみ可胜です。


アプリケヌションで珟圚定矩されおいるすべおのルヌトを調べるず、䞊蚘で䜿甚したAPI定矩に䞀臎するものがいく぀かあるこずに気付くでしょう。 それらを芋぀けたしたか 第14章で定矩されおいる/ translateルヌトなど、JSONを返すいく぀かのルヌトに぀いお説明しおいたす。 これは、 POSTリク゚ストでテキスト、゜ヌス蚀語、および宛先蚀語、すべおのデヌタをJSON圢匏で受け入れるルヌトです。 この芁求に察する応答は、このテキストの翻蚳であり、JSON圢匏でもありたす。 サヌバヌは芁求された情報のみを返し、クラむアントにはこの情報をナヌザヌに提瀺する責任がありたす。


アプリケヌションのJSONルヌトには「フィヌル」APIがありたすが、ブラりザヌで実行されるWebアプリケヌションをサポヌトするように蚭蚈されおいたす。
アプリケヌション内のJSONルヌトにはAPIがありたすが、「感芚」は、ブラりザヌで実行されるWebアプリケヌションをサポヌトするように蚭蚈されおいるこずです。 スマヌトフォンアプリケヌションがこれらのルヌトを䜿甚したい堎合、登録されたナヌザヌが必芁であり、ログむンはHTMLフォヌムを介しおのみ可胜であるため、䜿甚できないこずに泚意しおください。 この章では、Webブラりザヌに䟝存しないAPIを䜜成する方法を説明し、どのクラむアントがそれらに接続するかを仮定したせん。


この章のGitHubリンク Browse 、 Zip 、 Diff 。


API蚭蚈の基瀎ずしおのREST


誰かが䞊蚘の私の声明に匷く反察するかもしれたせん/その翻蚳ず他のJSONルヌトはAPIルヌトです。 他の人は、それらが䞍十分に蚭蚈されたAPIであるず考えるずいう譊告に同意するかもしれたせん。 では、適切に蚭蚈されたAPIの特城は䜕ですかたた、JSONルヌトがこのカテゎリ倖にあるのはなぜですか


REST APIずいう甚語を聞いたこずがあるかもしれたせん。 RESTは、Representational State Transferの略で、Roy Fielding博士が博士論文で提案したアヌキテクチャです。 フィヌルディング博士は、RESTの6぀の特城をかなり抜象的か぀䞀般的な方法で瀺しおいたす。


フィヌルディング博士の論文ずは別に、他の信頌できるREST仕様はなく、読者に自由に解釈させる䜙地がありたす。 䞎えられたREST APIが䞀貫しおいるかどうかのトピックは、REST APIが6぀のすべおの特性を順守し、RESTの「プラグマティスト」ず比范しお明確に行う必芁があるず信じるREST「玔粋䞻矩者」の間の激しい議論の源です。フィヌルディング博士の論文でガむドラむンたたは掚奚事項ずしお提瀺されたアむデア。 フィヌルディング博士自身が玔粋䞻矩者陣営の味方であり、ブログずオンラむンの解説に぀いおの圌のビゞョンにいく぀かの远加の掞察を䞎えたした。


珟圚実装されおいるAPIの倧郚分は、「実甚的な」REST実装に準拠しおいたす。 これには、Facebook、GitHub、Twitterなどの「ビッグプレヌダヌ」のほずんどのAPIが含たれたす。 ほずんどのAPIは玔粋䞻矩者が必須ず考える実装の詳现をスキップするため、玔粋なRESTず満堎䞀臎で考えられるパブリックAPIはほずんどありたせん。 Dr. Fieldingや他のRESTの玔粋䞻矩者はREST APIがそうであるかそうでないずいう厳栌な芋解にもかかわらず、゜フトりェア業界は通垞、実甚的な意味でRESTに蚀及しおいたす。


REST論文の内容を理解するために、以䞋のセクションでは、フィヌルディング博士がリストした6぀の原則に぀いお説明したす。


クラむアントサヌバヌ


クラむアントずサヌバヌの原則は非垞に単玔です。RESTAPIでは、クラむアントずサヌバヌの圹割を明確に区別する必芁があるこずを瀺しおいるだけです。 実際には、これは、クラむアントずサヌバヌがトランスポヌトを介しお盞互䜜甚する別個のプロセスにあるこずを意味したす。ほずんどの堎合、これはTCPネットワヌク䞊のHTTPプロトコルです。


階局化システム


階局化システム マルチレベルシステム の原理では、クラむアントがサヌバヌず察話する必芁がある堎合、実際のサヌバヌではなく、仲介者に接続できたす。 アむデアは、クラむアントがサヌバヌに盎接接続されおいない堎合、クラむアントが芁求を送信する方法にたったく違いはないずいうこずです。実際、タヌゲットサヌバヌに接続されおいるかどうかさえわからない堎合もありたす。 同様に、この原則では、サヌバヌはクラむアントから盎接ではなく、仲介者からクラむアント芁求を受信できるため、接続の反察偎がクラむアントであるず想定しおはなりたせん。


これは重芁なREST機胜です。䞭間ノヌドを远加できるため、アプリケヌションアヌキテクトはロヌドバランサヌ、キャッシュ、プロキシなどを䜿甚しお倧量の芁求を満たすこずができる倧芏暡で耇雑なネットワヌクを開発できたす。


キャッシュ


この原則は、サヌバヌたたはブロヌカヌがシステムパフォヌマンスを向䞊させるために頻繁に受信される芁求に察する応答をキャッシュできるこずを明瀺的に瀺すこずにより、階局型システムを拡匵したす。 おそらくおなじみのキャッシュ実装がありたす。すべおのWebブラりザヌに1぀ありたす。 Webブラりザヌのキャッシュレむダヌは、画像などの同じファむルを䜕床も芁求するこずを避けるためによく䜿甚されたす。


APIの目的のために、タヌゲットサヌバヌは、キャッシュ制埡の助けを借りお、応答がクラむアントに返されるずきに仲介者が応答をキャッシュできるかどうかを瀺す必芁がありたす。 セキュリティ䞊の理由から、実皌働環境にデプロむされたAPIは暗号化を䜿甚する必芁があるため、通垞、ホストがSSL接続を終了するか埩号化および再暗号化しない限り、キャッシングは䞭間ホストで実行されたせん。


オンデマンドのコヌド


これは、サヌバヌがクラむアントぞの応答で実行可胜コヌドを提䟛できるこずを瀺すオプションの芁件です。 この原則では、クラむアントが実行可胜な実行可胜コヌドに぀いおサヌバヌずクラむアント間の合意が必芁であるため、これはAPIでほずんど䜿甚されたせん。 サヌバヌがWebブラりザヌを実行するためにJavaScriptコヌドを返すず思うかもしれたせんが、RESTはWebブラりザヌクラむアント専甚に蚭蚈されおいたせん。 たずえば、クラむアントがiOSたたはAndroidデバむスの堎合、JavaScriptの実行はより耇雑になる可胜性がありたす。


ステヌトレス


ステヌトレスの原則は、RESTの玔粋䞻矩者ず実甚䞻矩者の間のほずんどの議論の䞭心にある2぀のうちの1぀です。 REST APIは、このクラむアントがリク゚ストを送信するたびに呌び出されるクラむアントの状態を保存すべきではないず述べおいたす。 これは、アプリケヌションのペヌゞをナビゲヌトするずきにナヌザヌを「蚘憶」するWeb開発で䞀般的なメカニズムは䜿甚できないこずを意味したす。 ステヌトレスAPIでは、各リク゚ストに、サヌバヌがクラむアントを識別および認蚌し、リク゚ストを完了する必芁がある情報を含める必芁がありたす。 たた、サヌバヌはクラむアント接続に関連するデヌタをデヌタベヌスたたはその他の圢匏のストレヌゞに保存できないこずも意味したす。


なぜRESTがステヌトレスサヌバヌを必芁ずするのか疑問に思っおいる堎合、䞻な理由は、ステヌトレスサヌバヌのスケヌリングが非垞に簡単で、ロヌドバランサヌの背埌で耇数のサヌバヌむンスタンスを起動するだけでよいこずです。 サヌバヌがクラむアントの状態を保存する堎合、耇数のサヌバヌがこの状態にアクセスしお曎新する方法を調べる必芁があるため、たたはこのクラむアントが垞にスティッキヌセッションず呌ばれる同じサヌバヌによっお垞に凊理されるようにする必芁があるため、状況はより耇雑になりたす。


章の冒頭で説明した/翻蚳ルヌトをもう䞀床芋るず、このルヌトに関連付けられたビュヌ関数はFlask-Loginの@login_requiredデコヌダヌに䟝存しおいるため、RESTfulずは芋なされないこずが@login_requiredたす。 Flaskナヌザヌセッションのナヌザヌ状態。


統䞀されたむンタヌフェヌス


最埌に、最も重芁で、最も議論され、最も曖昧に文曞化されたREST原則は、単䞀のむンタヌフェヌスです。 フィヌルディング博士は、単䞀のRESTむンタヌフェヌスの4぀の特城的な偎面を列挙しおいたすナニヌクなリ゜ヌス識別子、リ゜ヌス衚珟、自己蚘述メッセヌゞ、ハむパヌメディア。


䞀意のリ゜ヌス識別子は、䞀意のURLを各リ゜ヌスに割り圓おるこずによっお取埗されたす。 たずえば、このナヌザヌに関連付けられおいるURLは、/ api / users / <user-id>です。ここで、<user-id>は、デヌタベヌステヌブルのプラむマリキヌずしおナヌザヌに割り圓おられた識別子です。 これはほずんどのAPIで完党に受け入れられたす。


リ゜ヌス衚珟の䜿甚は、サヌバヌずクラむアントがリ゜ヌスに関する情報を亀換する堎合、䞀貫した圢匏を䜿甚する必芁があるこずを意味したす。 最近のほずんどのAPIでは、JSON圢匏を䜿甚しおリ゜ヌス衚珟を構築したす。 APIは、リ゜ヌスを衚すためのいく぀かの圢匏をサポヌトできたす。この堎合、HTTPプロトコルのコンテンツネゎシ゚ヌションパラメヌタヌは、クラむアントずサヌバヌが䞡方のナヌザヌが奜む圢匏をネゎシ゚ヌトできるメカニズムです。


自己蚘述メッセヌゞは、クラむアントずサヌバヌ間で亀換される芁求ず応答に、盞手が必芁ずするすべおの情報を含める必芁があるこずを意味したす。 兞型的な䟋は、クラむアントがサヌバヌから受信したい操䜜を瀺すために䜿甚されるHTTP芁求メ゜ッドです。 GETは、クラむアントがリ゜ヌスに関する情報を取埗したいこずを瀺し、 POST芁求はクラむアントが新しいリ゜ヌスを䜜成したいこずを瀺し、 PUTたたはPATCH芁求は既存のリ゜ヌスぞの倉曎を決定し、 DELETE芁求はリ゜ヌスの削陀を瀺したす。 タヌゲットリ゜ヌスは、HTTPヘッダヌ、URLのリク゚スト文字列の䞀郚、たたはリク゚スト本文で提䟛される远加情報ずずもに、リク゚ストURLずしお指定されたす。


ハむパヌメディアの芁件は、倚くの物議を醞すものであり、少数のAPIで実装されおいるものであり、それを実装するAPIは、RESTの玔粋䞻矩者を満足させるためにめったにそうしたせん。 アプリケヌション内のすべおのリ゜ヌスが盞互接続されおいるため、リ゜ヌスビュヌにリンクを含める必芁がありたす。これにより、顧客がリンクをトラバヌスしお新しいリ゜ヌスを芋぀けるこずができたす。もう䞀぀。 クラむアントは、リ゜ヌスを事前に知らなくおもAPIにアクセスし、ハむパヌメディアリンクをたどるだけでAPIに぀いお知るこずができたす。 この芁件の実装を耇雑にする偎面の1぀は、HTMLやXMLずは異なり、通垞APIでリ゜ヌスを衚すために䜿甚されるjson圢匏では、リンクを含める暙準的な方法が定矩されおいないため、特別なカスタム構造、たたはJSON-API 、 HAL 、 JSON-LDなど、このギャップを埋めようずする提案されたJSON拡匵の


ブルヌプリントAPIコンセプトの実装


APIの開発に関係するもののアむデアを提䟛するために、マむクロブログに远加したす。 これは完党なAPIではありたせん。ナヌザヌに関連するすべおの機胜を実装し、読者ぞのブログ投皿など、他のリ゜ヌスの実装は挔習ずしお残したす。


第15章で説明した抂念に埓っおすべおが線成および構造化されるように、すべおのAPIルヌトを含む新しいプロゞェクトを䜜成したす。 それでは、このプロゞェクトが存圚するディレクトリを䜜成するこずから始めたしょう。


 (venv) $ mkdir app/api 

ブルヌプリント__init __. pyファむル__init __. py __init __. pyは、他のブルヌプリントアプリケヌションず同様のブルヌプリントオブゞェクトを䜜成したす。


app/api/__init__.py: constructor API。

 from flask import Blueprint bp = Blueprint('api', __name__) from app.api import users, errors, tokens 

埪環䟝存゚ラヌを回避するために、むンポヌトをモゞュヌルの䞀番䞋に移動する必芁がある堎合があるこずをおそらく芚えおいるでしょう。 これがapp / api / users.py 、 app / api / errors.pyおよびapp / api / tokens.pyモゞュヌルこれはただ曞いおいたせんがプロゞェクトの䜜成埌にむンポヌトされる理由です 。


APIのメむンコンテンツはapp / api / users.pyモゞュヌルに保存されたす 。 次の衚に、実装するルヌトを瀺したす。


HTTPメ゜ッドリ゜ヌスURL泚釈
ゲット/api/users/<id>ナヌザヌを返したす。
ゲット/api/usersすべおのナヌザヌのコレクションを返したす。
ゲット/api/users/<id>/followersこのナヌザヌのフォロワヌを返したす。
ゲット/api/users/<id>/followedこのナヌザヌがフォロヌしおいるナヌザヌを返したす。
投皿/api/users新しいナヌザヌアカりントを登録したす。
眮く/api/users/<id>ナヌザヌを倉曎したす。

これらすべおのルヌトのプレヌスホルダヌを含むモゞュヌルフレヌムワヌクは次のようになりたす。


app/api/users.py:ナヌザヌAPIリ゜ヌスプレヌスホルダヌ。

 from app.api import bp @bp.route('/users/<int:id>', methods=['GET']) def get_user(id): pass @bp.route('/users', methods=['GET']) def get_users(): pass @bp.route('/users/<int:id>/followers', methods=['GET']) def get_followers(id): pass @bp.route('/users/<int:id>/followed', methods=['GET']) def get_followed(id): pass @bp.route('/users', methods=['POST']) def create_user(): pass @bp.route('/users/<int:id>', methods=['PUT']) def update_user(id): pass 

app / api / errors.pyモゞュヌルでは、゚ラヌ応答を凊理するいく぀かのヘルパヌ関数を定矩する必芁がありたす。 しかし、今、埌で入力するプレヌスホルダヌを䜜成したす。


app/api/errors.py:プレヌスホルダヌの凊理゚ラヌ。

 def bad_request(): pass 

認蚌サブシステムが定矩されるapp / api / tokens.pyモゞュヌル。 これにより、Webブラりザヌではないクラむアントに代替ログむン方法が提䟛されたす。 このモゞュヌルのプレヌスホルダヌも曞きたしょう


app/api/tokens.py:マヌカヌを凊理したす。

 def get_token(): pass def revoke_token(): pass 

新しいBlueprint APIスキヌマは、アプリケヌションファクトリ関数に登録する必芁がありたす。


app/__init__.py:芁玠図をアプリケヌションに登録したす。

 # ... def create_app(config_class=Config): app = Flask(__name__) # ... from app.api import bp as api_bp app.register_blueprint(api_bp, url_prefix='/api') # ... 

JSONオブゞェクトの圢匏でのナヌザヌの衚珟


APIを実装するずきに考慮する最初の偎面は、リ゜ヌスのプレれンテヌションを決定するこずです。 ナヌザヌず連携するAPIを実装するため、ナヌザヌリ゜ヌスのビュヌを解決する必芁がありたす。 いく぀かのブレヌンストヌミングの埌、次のjsonビュヌを思い぀きたした。


 { "id": 123, "username": "susan", "password": "my-password", "email": "susan@example.com", "last_seen": "2017-10-20T15:04:27Z", "about_me": "Hello, my name is Susan!", "post_count": 7, "follower_count": 35, "followed_count": 21, "_links": { "self": "/api/users/123", "followers": "/api/users/123/followers", "followed": "/api/users/123/followed", "avatar": "https://www.gravatar.com/avatar/..." } } 

フィヌルドの倚くは、ナヌザヌデヌタベヌスモデルから盎接取埗されたす。 passwordフィヌルドは、新しいナヌザヌを登録するずきにのみ䜿甚されるずいう点で異なりたす。 第5章でおわかりのように、ナヌザヌパスワヌドはデヌタベヌスに保存されず、ハッシュのみが保存されるため、パスワヌドが返されるこずはありたせん。 ナヌザヌのメヌルアドレスを公開したくないので、 emailフィヌルドも特別に凊理されたす。 電子メヌルフィヌルドは、ナヌザヌが自分の゚ントリを芁求した堎合にのみ返されたすが、他のナヌザヌから゚ントリを受信した堎合は返されたせん。 フィヌルドpost_count 、 follower_countおよびfollow_countは、デヌタベヌス内のフィヌルドずしおは存圚しないが、䟿宜䞊クラむアントに提䟛される「仮想」フィヌルドです。 これは玠晎らしい䟋であり、リ゜ヌスの衚瀺は、実際のリ゜ヌスがサヌバヌ䞊で定矩されおいる方法ず䞀臎する必芁がないこずを瀺しおいたす。


ハむパヌ_links芁件を実装する_linksセクションを_linksください。 特定のリンクには、珟圚のリ゜ヌスぞのリンク、そのナヌザヌをフォロヌしおいるナヌザヌのリスト、ナヌザヌがフォロヌしおいるナヌザヌのリスト、最埌にナヌザヌのアバタヌ画像ぞのリンクが含たれたす。 将来、このAPIにメッセヌゞを远加するこずにした堎合、ナヌザヌメッセヌゞのリストぞのリンクもここに含める必芁がありたす。


JSON圢匏の優れた点の1぀は、垞にPython蟞曞たたはリストビュヌずしお倉換されるこずです。 Python暙準ラむブラリのjsonパッケヌゞは、Pythonデヌタ構造ずJSONの倉換を凊理したす。 したがっお、これらのビュヌを生成するために、Python蟞曞を返すto_dict()ずいうUserモデルにメ゜ッドを远加したす。


app/models.py:ビュヌのナヌザヌモデル。

 from flask import url_for # ... class User(UserMixin, db.Model): # ... def to_dict(self, include_email=False): data = { 'id': self.id, 'username': self.username, 'last_seen': self.last_seen.isoformat() + 'Z', 'about_me': self.about_me, 'post_count': self.posts.count(), 'follower_count': self.followers.count(), 'followed_count': self.followed.count(), '_links': { 'self': url_for('api.get_user', id=self.id), 'followers': url_for('api.get_followers', id=self.id), 'followed': url_for('api.get_followed', id=self.id), 'avatar': self.avatar(128) } } if include_email: data['email'] = self.email return data 

この方法は特別な質問を匕き起こすものではなく、基本的に明確にする必芁がありたす。 停止したナヌザヌビュヌのディクショナリが生成されお返されたす。 䞊で述べたように、ナヌザヌが自分のデヌタを芁求したずきにのみメヌルを有効にしたいので、 emailフィヌルドには特別な凊理が必芁です。 したがっお、 include_emailフラグを䜿甚しお、このフィヌルドがビュヌに含たれおいるかどうかを刀断しinclude_email 。


last_seenフィヌルドがどのようにlast_seenかに泚目しおlast_seen 。 日付ず時刻のフィヌルドには、 ISO 8601圢匏を䜿甚したすisoformat()はisoformat()メ゜ッドを䜿甚しお生成できたす。 しかし、UTCであるが状態に蚘録されたタむムゟヌンを持たない単玔なdatetimeオブゞェクトを䜿甚するため、UTCのISO 8601タむムゟヌンコヌドであるZを最埌に远加する必芁がありたす。


amkkoからの説明 pythonでは、datetimeオブゞェクトはタむムゟヌンに関しお「単玔」たたは「単玔/認識」になりたす。

最埌に、ハむパヌメディアリンクの実装方法を確認したす。 他のアプリケヌションルヌトを指す3぀のリンクの堎合、 url_for()を䜿甚しおURL珟圚app / api / users.pyで定矩されおいる眮換芁玠のルックアップ関数を指したすurl_for()を生成したす。 アバタヌリンクは、アプリケヌションの倖郚のGravatar URLであるため、特別です。 avatar() , -.


to_dict() Python, JSON. , , User . from_dict() , Python :


app/models.py: .

 class User(UserMixin, db.Model): # ... def from_dict(self, data, new_user=False): for field in ['username', 'email', 'about_me']: if field in data: setattr(self, field, data[field]) if new_user and 'password' in data: self.set_password(data['password']) 

, : username , email about_me . , data , , setattr() Python, .


password , . new_user , , , . password , set_password() , .



, API . , , , . :


 { "items": [ { ... user resource ... }, { ... user resource ... }, ... ], "_meta": { "page": 1, "per_page": 10, "total_pages": 20, "total_items": 195 }, "_links": { "self": "http://localhost:5000/api/users?page=1", "next": "http://localhost:5000/api/users?page=2", "prev": null } } 

items - , , . _meta , . _links , , , .


- , , , , API , , . 16 , , , . , , , SearchableMixin , , . , mixin, PaginatedAPIMixin :


app/models.py: mixin.

 class PaginatedAPIMixin(object): @staticmethod def to_collection_dict(query, page, per_page, endpoint, **kwargs): resources = query.paginate(page, per_page, False) data = { 'items': [item.to_dict() for item in resources.items], '_meta': { 'page': page, 'per_page': per_page, 'total_pages': resources.pages, 'total_items': resources.total }, '_links': { 'self': url_for(endpoint, page=page, per_page=per_page, **kwargs), 'next': url_for(endpoint, page=page + 1, per_page=per_page, **kwargs) if resources.has_next else None, 'prev': url_for(endpoint, page=page - 1, per_page=per_page, **kwargs) if resources.has_prev else None } } return data 

to_collection_dict() , items , _meta _links . , , . - Flask-SQLAlchemy, . , . paginate() , , , -.


, . , , , url_for ('api.get_users', id = id, page = page) . url_for() , , url_for() . , kwargs url_for() . page per_page , API.


mixin User :


app/models.py: PaginatedAPIMixin .

 class User(PaginatedAPIMixin, UserMixin, db.Model): # ... 

, , .


゚ラヌ凊理


, 7 , , , -. API , « » , , . API JSON, API. , :


 { "error": "short error description", "message": "error message (optional)" } 

HTTP . , error_response() app/api/errors.py :


app/api/errors.py: .

 from flask import jsonify from werkzeug.http import HTTP_STATUS_CODES def error_response(status_code, message=None): payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unknown error')} if message: payload['message'] = message response = jsonify(payload) response.status_code = status_code return response 

HTTP_STATUS_CODES Werkzeug ( Flask), HTTP. error , . jsonify() Response Flask 200, .


, API , 400, " ". -, , , . , , . bad_request() , :


app/api/errors.py: .

 # ... def bad_request(message): return error_response(400, message) 


, JSON, , API.



, id :


app/api/users.py: .

 from flask import jsonify from app.models import User @bp.route('/users/<int:id>', methods=['GET']) def get_user(id): return jsonify(User.query.get_or_404(id).to_dict()) 

view URL-. get_or_404() get() , , , , , None , id , 404 . get_or_404() get() , , .


to_dict() , User , , Flask jsonify() JSON .


, API, , URL- :


 http://localhost:5000/api/users/1 

, JSON. id , , get_or_404() SQLAlchemy 404 ( , , JSON).


, HTTPie , HTTP- , Python, API:


 (venv) $ pip install httpie 

1 (, , ) :


 (venv) $ http GET http://localhost:5000/api/users/1 HTTP/1.0 200 OK Content-Length: 457 Content-Type: application/json Date: Mon, 27 Nov 2017 20:19:01 GMT Server: Werkzeug/0.12.2 Python/3.6.3 { "_links": { "avatar": "https://www.gravatar.com/avatar/993c...2724?d=identicon&s=128", "followed": "/api/users/1/followed", "followers": "/api/users/1/followers", "self": "/api/users/1" }, "about_me": "Hello! I'm the author of the Flask Mega-Tutorial.", "followed_count": 0, "follower_count": 1, "id": 1, "last_seen": "2017-11-26T07:40:52.942865Z", "post_count": 10, "username": "miguel" } 


, to_collection_dict() PaginatedAPIMixin:


app/api/users.py: .

 from flask import request @bp.route('/users', methods=['GET']) def get_users(): page = request.args.get('page', 1, type=int) per_page = min(request.args.get('per_page', 10, type=int), 100) data = User.to_collection_dict(User.query, page, per_page, 'api.get_users') return jsonify(data) 

page per_page , 1 10 , . per_page , 100. , . page per_page to_collection_query() , User.query - , . - api.get_users , , .


HTTPie, :


 (venv) $ http GET http://localhost:5000/api/users The next two endpoints are the ones that return the follower and followed users. These are fairly similar to the one above: app/api/users.py: Return followers and followed users. @bp.route('/users/<int:id>/followers', methods=['GET']) def get_followers(id): user = User.query.get_or_404(id) page = request.args.get('page', 1, type=int) per_page = min(request.args.get('per_page', 10, type=int), 100) data = User.to_collection_dict(user.followers, page, per_page, 'api.get_followers', id=id) return jsonify(data) @bp.route('/users/<int:id>/followed', methods=['GET']) def get_followed(id): user = User.query.get_or_404(id) page = request.args.get('page', 1, type=int) per_page = min(request.args.get('per_page', 10, type=int), 100) data = User.to_collection_dict(user.followed, page, per_page, 'api.get_followed', id=id) return jsonify(data) 

, id . , user.followers user.followed to_collection_dict() , , , , . to_collection_dict() — , kwargs , url_for() .


, HTTPie :


 (venv) $ http GET http://localhost:5000/api/users/1/followers (venv) $ http GET http://localhost:5000/api/users/1/followed 

, hypermedia URL-, _links .



POST /users . :


app/api/users.py: .

 from flask import url_for from app import db from app.api.errors import bad_request @bp.route('/users', methods=['POST']) def create_user(): data = request.get_json() or {} if 'username' not in data or 'email' not in data or 'password' not in data: return bad_request('must include username, email and password fields') if User.query.filter_by(username=data['username']).first(): return bad_request('please use a different username') if User.query.filter_by(email=data['email']).first(): return bad_request('please use a different email address') user = User() user.from_dict(data, new_user=True) db.session.add(user) db.session.commit() response = jsonify(user.to_dict()) response.status_code = 201 response.headers['Location'] = url_for('api.get_user', id=user.id) return response 

JSON , . Flask request.get_json() , JSON Python. None , JSON , , , request.get_json() {} .


, , , , . username , email password . - , bad_request() app/api/errors.py . , username email , , - , .


, , . from_dict() . new_user True , password , .


, , , to_dict() . POST , , 201 , , . , HTTP , 201 Location, URL- .


, HTTPie:


 (venv) $ http POST http://localhost:5000/api/users username=alice password=dog \ email=alice@example.com "about_me=Hello, my name is Alice!" 


, API, — , :


app/api/users.py: .

 @bp.route('/users/<int:id>', methods=['PUT']) def update_user(id): user = User.query.get_or_404(id) data = request.get_json() or {} if 'username' in data and data['username'] != user.username and \ User.query.filter_by(username=data['username']).first(): return bad_request('please use a different username') if 'email' in data and data['email'] != user.email and \ User.query.filter_by(email=data['email']).first(): return bad_request('please use a different email address') user.from_dict(data, new_user=False) db.session.commit() return jsonify(user.to_dict()) 

id URL, 404 , . , , username email , , , , . , , , . , , , , , , . - , 400 , .


From_dict() , , . 200 .


, about_me HTTPie:


 (venv) $ http PUT http://localhost:5000/api/users/2 "about_me=Hi, I am Miguel" 

API


API, , . , , , «AuthN» «AuthZ» . , , , , , , , .


API @login_required Flask-Login, . , HTML . API HTML , , , 401. , API - HTML. API 401, , , , .


()


API . API, , . API, , , . . User :


app/models.py: .

 import base64 from datetime import datetime, timedelta import os class User(UserMixin, PaginatedAPIMixin, db.Model): # ... token = db.Column(db.String(32), index=True, unique=True) token_expiration = db.Column(db.DateTime) # ... def get_token(self, expires_in=3600): now = datetime.utcnow() if self.token and self.token_expiration > now + timedelta(seconds=60): return self.token self.token = base64.b64encode(os.urandom(24)).decode('utf-8') self.token_expiration = now + timedelta(seconds=expires_in) db.session.add(self) return self.token def revoke_token(self): self.token_expiration = datetime.utcnow() - timedelta(seconds=1) @staticmethod def check_token(token): user = User.query.filter_by(token=token).first() if user is None or user.token_expiration < datetime.utcnow(): return None return user 

token , , . token_expiration , . , , .


. get_token() . , base64, . , , .


, . , . revoke_token() , , , .


check_token() , , . , None.


, , :


 (venv) $ flask db migrate -m "user tokens" (venv) $ flask db upgrade 

()


API, , -, -. API , , . API, , -.


, Flask Flask-HTTPAuth . Flask-HTTPAuth pip:


 (venv) $ pip install flask-httpauth 

Flask-HTTPAuth , API . HTTP Basic Authentication 11.1 , http . Flask-HTTPAuth : , , , , . Flask-HTTPAuth , . :


app/api/auth.py: .

 from flask import g from flask_httpauth import HTTPBasicAuth from app.models import User from app.api.errors import error_response basic_auth = HTTPBasicAuth() @basic_auth.verify_password def verify_password(username, password): user = User.query.filter_by(username=username).first() if user is None: return False g.current_user = user return user.check_password(password) @basic_auth.error_handler def basic_auth_error(): return error_response(401) 

HTTPBasicAuth Flask-HTTPAuth- , . verify_password error_handler .


, , True , , False , . check_password() User , Flask-Login -. g.current_user , API.


401, error_response() app/api/errors.py . 401 HTTP "Unauthorized" (" "). HTTP , .


, , , :


app/api/tokens.py: Generate user tokens.

 from flask import jsonify, g from app import db from app.api import bp from app.api.auth import basic_auth @bp.route('/tokens', methods=['POST']) @basic_auth.login_required def get_token(): token = g.current_user.get_token() db.session.commit() return jsonify({'token': token}) 

@basic_auth.login_required HTTPBasicAuth, Flask-HTTPAuth ( ) , . get_token() . , , .


POST API :


 (venv) $ http POST http://localhost:5000/api/tokens HTTP/1.0 401 UNAUTHORIZED Content-Length: 30 Content-Type: application/json Date: Mon, 27 Nov 2017 20:01:00 GMT Server: Werkzeug/0.12.2 Python/3.6.3 WWW-Authenticate: Basic realm="Authentication Required" { "error": "Unauthorized" } 

HTTP 401 , basic_auth_error() . , :


 (venv) $ http --auth <username>:<password> POST http://localhost:5000/api/tokens HTTP/1.0 200 OK Content-Length: 50 Content-Type: application/json Date: Mon, 27 Nov 2017 20:01:22 GMT Server: Werkzeug/0.12.2 Python/3.6.3 { "token": "pC1Nu9wwyNt8VCj1trWilFdFI276AcbS" } 

200, , . , <username>:<password> . .


API


API, . , Flask-HTTPAuth . HTTPTokenAuth :


app/api/auth.py: Token.

 # ... from flask_httpauth import HTTPTokenAuth # ... token_auth = HTTPTokenAuth() # ... @token_auth.verify_token def verify_token(token): g.current_user = User.check_token(token) if token else None return g.current_user is not None @token_auth.error_handler def token_auth_error(): return error_response(401) 

Flask-HTTPAuth verify_token , , . User.check_token() , , . , None . True False , Flask-HTTPAuth .


API , @token_auth.login_required :


app/api/users.py: Protect user routes with token authentication.

 from app.api.auth import token_auth @bp.route('/users/<int:id>', methods=['GET']) @token_auth.login_required def get_user(id): # ... @bp.route('/users', methods=['GET']) @token_auth.login_required def get_users(): # ... @bp.route('/users/<int:id>/followers', methods=['GET']) @token_auth.login_required def get_followers(id): # ... @bp.route('/users/<int:id>/followed', methods=['GET']) @token_auth.login_required def get_followed(id): # ... @bp.route('/users', methods=['POST']) def create_user(): # ... @bp.route('/users/<int:id>', methods=['PUT']) @token_auth.login_required def update_user(id): # ... 

, API, create_user() , , , , .


, , 401. , Authorization , /api/tokens . Flask-HTTPAuth , -, HTTPie. HTTPie --auth , . -:


 (venv) $ http GET http://localhost:5000/api/users/1 \ "Authorization:Bearer pC1Nu9wwyNt8VCj1trWilFdFI276AcbS" 


, , , — , :


app/api/tokens.py: Revoke tokens.

 from app.api.auth import token_auth @bp.route('/tokens', methods=['DELETE']) @token_auth.login_required def revoke_token(): g.current_user.revoke_token() db.session.commit() return '', 204 

DELETE URL- /tokens , . , , Authorization , . User , . , . , . return 204, , .


, HTTPie:


 (venv) $ http DELETE http://localhost:5000/api/tokens \ Authorization:"Bearer pC1Nu9wwyNt8VCj1trWilFdFI276AcbS" 

API


, , API URL- ? 404, 404 HTML. , API, JSON API, , Flask, - , , HTML.


HTTP , , . Accept , . , , , .


, HTML JSON . Flask request.accept_mimetypes :


app/errors/handlers.py: .

 from flask import render_template, request from app import db from app.errors import bp from app.api.errors import error_response as api_error_response def wants_json_response(): return request.accept_mimetypes['application/json'] >= \ request.accept_mimetypes['text/html'] @bp.app_errorhandler(404) def not_found_error(error): if wants_json_response(): return api_error_response(404) return render_template('errors/404.html'), 404 @bp.app_errorhandler(500) def internal_error(error): db.session.rollback() if wants_json_response(): return api_error_response(500) return render_template('errors/500.html'), 500 

ヘルパヌ関数wants_json_response()は、優先圢匏のリストでクラむアントが遞択したJSONたたはHTMLの蚭定を比范したす。JSONの速床がHTMLよりも速い堎合、JSON応答を返したす。それ以倖の堎合、元のテンプレヌトベヌスのHTML応答を返したす。JSONの回答に぀いおerror_responseは、API芁玠のスキヌマからヘルパヌ関数をむンポヌトしたすが、ここで名前を倉曎しおapi_error_response()、その機胜ず生成元が明確になるようにしたす。



Source: https://habr.com/ru/post/J358152/


All Articles