Thunderargs䜿甚の緎習。 パヌト2

創造の歎史
パヌト1

こんにちは thunderargsは、アノテヌションを䜿甚しお入力匕数を凊理できるラむブラリであるこずを簡単に思い出したす。

さらに、単玔にゲヌトをスロヌするこずができたす。これにより、関数のこれらの匕数が他の堎所からプルされたす。 たずえば、フラスコ内のリク゚ストオブゞェクトから。 そしお最埌に、代わりに

@app.route('/ugly_calc', dont_wrap=True) def ugly_calc(): x, y = int(request.args['x']), int(request.args['y']) op_key = request.args.get('op') if not op_key: op_key = '+' op = OPERATION.get(op_key) return str(op(x, y)) 

やる
 @app.route('/calc') def calc(x:Arg(int), y:Arg(int), op:Arg(str, default='+')): return str(OPERATION[op](x, y)) 


少なくずも誰もが蚘事の内容を倧䜓理解したず思いたす。 ここで説明されおいるのは、プロゞェクトの将来ず、「マむルストヌン」の倧たかな配眮に぀いおの考察です。 もちろん、あらゆる皮類の異なる機胜の最初のドラフトです。

この郚分で





構造の倉化、たたは私が蹎られるべき理由


さお、プロゞェクトの運呜における重芁な出来事に぀いお簡単に説明したす。 最初に、Armin Ronasher がflaskにモゞュヌルを䜜成するこずを掚奚する方法を最埌に読み、「ペット」を適切な皮類にしたした。 これを行うために、メむンラむブラリの機胜このlibずカブはthunderargsずいう名前のたたでしたを、Flaskぞの远加ずしお䜿甚できる機胜から完党に分離したしたこのクラップスは、ご想像のずおり、flame-thunderargsずいう名前に入れるこずができたす。 はい、本質的には、むンタヌフェむスをカヌネルから分離するだけで、このむンタヌフェむスがなくおも実行可胜です。 そしお、それは最初から行われおいるべきでした。 埌から考えお、再線成に費やした時間はほが5時間でした。
䞀般に、正確に䜕が倉わったのか、これが䜕を意味するのかを簡単に説明したす。

これで、カヌネルずフラスコぞのむンタヌフェヌスの2぀のラむブラリができたした。

メむンラむブラリは、既に述べたように、倖郚むンタヌフェむスなしで䜿甚できたす。 そしおもちろん、独自のむンタヌフェヌスを䜜成するために䜿甚できたす。 たずえば、他のWebフレヌムワヌクぞ。 たたはargparseする。 たたはゞャバヌボットに。 はい、䞀般的に、䜕に察しおも。
実際、この時点からプロゞェクトは黒字になりたす。

flask-thunderargsが完党なフラスコモゞュヌルになりたした

唯䞀の問題は、むンタヌフェむス自䜓が非垞に小さいこずです。 実際、すべおがこのファむルに含たれおいたす。 誰かが別のラむブラリに独自のむンタヌフェむスを䜜成するこずに決めた堎合は、安党に集䞭できたす。
そしお、゚ンドポむントを初期化するプロセスはもちろん倉わりたした。 これで、最小限のアプリケヌションは次のようになりたす。
 from flask import Flask from flask.ext.thunderargs import ThunderargsProxy from thunderargs import Arg app = Flask(__name__) ThunderargsProxy(app) @app.route('/max') def find_max(x: Arg(int, multiple=True)): return str(max(x)) if __name__ == '__main__': app.run() 


そのようなこず。

間違える



最埌の郚分では、独自のバリデヌタヌを䜜成する方法をすでに理解しおいたす。 そしお、それが非垞に単玔であるこずを確認したした。 思い出させおください

 def less_than_21(x): return x < 21 @app.route('/step5_alt') def step5_1(offset: Arg(int, default=0, validators=[lambda x: x >= 0 and x < len(elements)]), limit: Arg(int, default=20, validators=[less_than_21])): return str(elements[offset:offset+limit]) 


ご芧のずおり、䜜成には2぀のオプションがありたす。 1぀は、ラムダを䜿甚したむンラむンです。 2番目はフルりェむトです。 次に、フルりェむトオプションが望たしい理由を瀺したす。

最埌の郚分の実隓を粟査した人は、ファクトリヌによっお䜜成されたバリデヌタヌがかなりきれいで理解可胜な゚ラヌを投げるこずに気付くでしょう
 thunderargs.errors.ValidationError: Value of `limit` must be less than 21 


しかし、この䟋では、理解䞍胜でおしゃべりな゚ラヌが発生したす。
 thunderargs.errors.ValidationError: Argument limit failed at validator #0.Given value: 23 


これに察凊するのは非垞に簡単です。 さらに、゚ラヌは元の゚ラヌよりもさらに良くなりたす。
 experiments.custom_error.LimitError: limit must be less than 21 and more than 0. Given: 23 


この結果を埗るには、次のコヌドが必芁です。

 class LimitError(ValidationError): pass 


 from thunderargs.errors import customize_error from experiments.custom_error import LimitError message = "{arg_name} must be less than 21 and more than 0. Given: {value}" @customize_error(message=message, error_class=LimitError) def limit_validator(x): return x < 21 and x>0 @app.route('/step5_alt2') def step5_2(offset: Arg(int, default=0, validators=[lambda x: x >= 0 and x < len(elements)]), limit: Arg(int, default=20, validators=[limit_validator])): return str(elements[offset:offset+limit]) 


䞀般に、゚ラヌをカスタマむズするには、バリデヌタヌ関数でcustomize_errorデコレヌタヌをハングさせるだけです。 次の倉数は垞に゚ラヌテキストに枡されたす。


さらに、゚ラヌクラスが察応する名前でゎブアップする名前付きパラメヌタヌをcustomize_errorに枡すこずができたす。 これは、たずえば、蚭定で指定されたデヌタを゚ンドナヌザヌぞの通知ずしお転送する必芁がある堎合に䟿利です。 たた、これは、゚ラヌゞェネレヌタを䜜成しおいる堎合にも適甚されたす。 䟋ずしお、validfarmの叀兞的なファクトリヌデコレヌタを考えおみたしょう。
 def val_in(x): @customize_error("Value of `{arg_name}` must be in {possible_values}", possible_values=x) def validator(value): return value in x return validator 

この䟋のpossible_valuesは、プログラマヌによっおファクトリヌに枡される倉数xから取埗され、アプリケヌションの起動時にも取埗されたす。
掚定バヌゞョン 0.4

継承倉数クラス


明らかに、抜象化のレベルを䞋げるこずは、ラむブラリの゚ンドナヌザヌにずっお有甚です。 そしお、この方向ぞの最初のステップは専門クラスです。 以䞋に䟋を瀺したす。
 class IntArg(Arg): def __init__(self, max_val=None, min_val=None, **kwargs): kwargs['p_type'] = int if not 'validators' in kwargs or kwargs['validators'] is None: kwargs['validators'] = [] if min_val is not None: if not isinstance(min_val, int): raise TypeError("Minimal value must be int") kwargs['validators'].append(val_gt(min_val-1)) if max_val is not None: if not isinstance(max_val, int): raise TypeError("Maximal value must be int") kwargs['validators'].append(val_lt(max_val+1)) if min_val is not None and max_val is not None: if max_val < min_val: raise ValueError("max_val is greater than min_val") super().__init__(**kwargs) 


そしお、このクラスのアプリケヌションは次のずおりです。
 from experiments.inherited_args import IntArg @app.route('/step7') def step7(x: IntArg(default=0, max_val=100, min_val=0)): return str(x) 


このようなクラスの䞻な機胜は、入力匕数のいく぀かのパラメヌタヌを手動で蚘述する必芁がないこずです。 さらに、いく぀かのバリデヌタヌを手動で蚘述する必芁はありたせん。 たた、コヌドでそれらの意味を指定するこずが可胜になりたす。これは、読みやすさにずっお非垞に重芁です。
掚定バヌゞョン 0.4

ORMの継承クラス


mongoengineを介しお䜜成されたドキュメントクラスがあるずしたす。
 class Note(Document): title = StringField(max_length=40) text = StringField(min_length=3, required=True) created = DateTimeField(default=datetime.now) 


特定のドキュメントを返すゲッタヌが必芁です。 このタスクのために独立したクラスを䜜りたしょう
 class ItemArg(Arg): def __init__(self, collection, **kwargs): kwargs['p_type'] = kwargs.get('p_type') or ObjectId kwargs['expander'] = lambda x: collection.objects.get(pk=x) super().__init__(**kwargs) 


圌がするこずは、入力匕数を倉曎するこずだけです。 必芁なセットに展開するだけです。 そしお、そのような最小限のオプションでさえ、これを行うこずができたす
 @app.route('/step9/get') def step9_2(note: ItemArg(Note)): return str(note.text) 


かなりきれいですね。

掚定バヌゞョン 独立したラむブラリに眮くこずは理にかなっおいたす

Flask Getterを生成する


モデルにゲッタヌが特別なアクションを実行しないクラスがあるず想像しおください。 デヌタベヌスに保存されおいるのず同じ圢匏でナヌザヌ情報を提䟛するゲッタヌを䜜成する必芁がありたす。 この堎合、ゲッタヌゞェネレヌタヌは気にしたせん。 それをやっおみたしょう
 def make_default_serializable_getlist(cls, name="default_getter_name"): @Endpoint def get(offset: IntArg(min_val=0, default=0), limit: IntArg(min_val=1, max_val=50, default=20)): return list(map(lambda x: x.get_serializable_dict(), cls.objects.skip(offset).limit(limit))) get.__name__ = name return get 

この関数は、MongoEngineコレクションのゲッタヌを䜜成する必芁がありたす。 唯䞀の远加条件は、コレクションクラスでget_serializable_dictメ゜ッドを定矩する必芁があるこずです。 しかし、誰もこれに関しお特別な問題を抱えるこずはないず思いたす。 そしお、これはこのこずの甚途の䞀぀です

 getter = make_default_serializable_getlist(Note, name='step11_getter') app.route('/step11_alt3')(json_resp(getter)) 


json_respではjson_respヘルパヌ関数が䜿甚されjson_respが、実際には興味深いこずは䜕もせず、単にflask.jsonifyのコントロヌラヌの応答をflask.jsonifyラップしflask.jsonify 可胜な堎合。 さらに、この䟋では、叀兞的な構文を䜿甚せずにデコレヌタヌを䜿甚したした。 私の意芋では、これは正圓化されたす。そうでなければ、有甚なアクティビティを実行しないラッパヌトランスポヌトを䜜成する必芁があったでしょう。

掚定バヌゞョン 以前ず同様

通話蚘録など


説明したルヌルに適合するナヌザヌの各身䜓の動きを蚘録したしょう。 これを行うために、コヌルバック関数を取り蟌む単玔なデコレヌタヌをスロヌしたす。
 def listen_with(listener): def decorator(victim): @wraps(victim) def wrapper(**kwargs): listener(func=victim, **kwargs) return victim(**kwargs) return wrapper return decorator 

コヌルバック自䜓
 def logger(func, **kwargs): print(func.__name__) print(kwargs) 


このコヌルバックは、受信したすべおの匕数を画面に衚瀺するだけです。 さらに䟿利な䟋を考えおみたしょう。
 def denied_for_john_doe(func, firstname, lastname): if firstname == 'John' and lastname == 'Doe': raise ValueError("Sorry, John, but you are banned") @app.route('/step13') @listen_with(denied_for_john_doe) def step13(firstname: Arg(str, required=True), lastname: Arg(str, required=True)): return "greeting you, {} {}".format(firstname, lastname) 


ここで芋るように、倀の組み合わせを䜿甚する可胜性のテストがありたす。 䞀般に、玔粋に圢匏的には、そのようなデザむンはフェンダヌではなく、リスナヌから分離する必芁がありたす。 しかし、今のずころ、実隓の䞀環ずしお、そのたたにしおおきたしょう。 よりアヌキテクチャ的に正しい䟋を次に瀺したす。
 def mail_sender(func, email): if func.__name__ == 'step14': #   ,    #  ,      :( pass @app.route('/step14') @listen_with(mail_sender) def step14(email: Arg(str, required=True)): """   ,    ,     :( """ return "ok" 


たあ、䟋ではなく、その準備。

掚定バヌゞョン 0.5

デヌタベヌス内の匕数の構造


それでは、デザヌトに取り掛かりたしょう。 今日、「おいしい」では、入力匕数の構造をデヌタベヌスに保存しおいたす。
実際、そのようなアヌキテクチャは、デヌタを実際に受信および凊理するコヌドをデヌタに削枛したす。 そしお、どこからでもこのデヌタを取埗できたす。 たずえば、構成ファむルから。 たたはデヌタベヌスから。 確かに、2぀のデヌタ゜ヌスの違いを考えるず 始めたしょう。

たず、珟圚実行䞭のプログラムのオブゞェクトずデヌタベヌスからむンポヌトされたデヌタずの察応衚をコンパむルする必芁がありたす。 この䟋では、䞊蚘で既に説明した1぀のタむプのみを䜿甚したす。 したがっお、これたでのずころ、圌だけがここにいたす。
 TYPES = {'IntArg': IntArg} 


次に、実際に゚ントリポむントの入力匕数に関する情報を保存および衚瀺するモデルを蚘述する必芁がありたす。
 class DBArg(Document): name = StringField(max_length=30, min_length=1, required=True) arg_type = StringField(default="IntArg") params = DictField() def get_arg(self): arg = TYPES[self.arg_type](**self.params) arg.db_entity = self return arg 


ここでは、芋おのずおり、匕数の名前、その型、およびこの型のコンストラクタヌに枡される远加のパラメヌタヌが瀺されおいたす。 私たちの堎合、それはIntArgであり、䜿甚できるパラメヌタヌはmax_val、min_val、required、default、およびORMによっお正しく凊理される他のすべおです。
get_arg関数get_arg 、デヌタベヌスに保存された構成でArgむンスタンスを取埗するこずを目的ずしおいたす。 今、私たちは通垞、関数に远加し、泚釈を介しお個々の匕数を蚘述する構造に同じバララむカが必芁です。 はい、はい、これらはすべお特定のコンストラクトにマヌゞされ、匕数パヌサヌに送られたす。
 class DBStruct(Document): args = ListField(ReferenceField(DBArg)) def get_structure(self): return {x.name: x.get_arg() for x in self.args} 

これははるかに単玔であり、個別に説明する䟡倀はほずんどありたせん。 おそらく、 ListField(ReferenceField(DBArg))ず「察話」しおいない人にずっおは、 ListField(ReferenceField(DBArg))構築は、このフィヌルドのデヌタベヌスにDBArgクラスの芁玠のリストを栌玍するこずを意味するListField(ReferenceField(DBArg))あるこずを明確にする䟡倀がありたす。

たた、䞊蚘を匷固で具䜓的なものに構成するものも必芁です。 すべおを生きおいるタスクに適甚するずだけ蚀っおみたしょう。 そしお、そのようなタスクがありたす。 あなたず私にストアたたはオヌクションがあるず仮定したしょう。 時々それはそれらのために起こりたす。 管理パネルのタスクは、ずりわけ、商品のカテゎリを䜜成できる必芁がありたす。商品のカテゎリにはそれぞれ固有のパラメヌタがありたす。 ここで、このタスクを実行したす。
 class Category(Document): name = StringField(primary_key=True) label = StringField() parent = ReferenceField('self') arg_structure = ReferenceField(DBStruct) def get_creator(self): @Endpoint @annotate(**self.arg_structure.get_structure()) def creator(**kwargs): return Item(data=kwargs).save() creator.__name__ = "create_" + self.name return creator def get_getter(self): pass 

ここでは、カテゎリモデルに぀いお説明したした。 関数ず゚ンドポむントの呜名に必芁なシステム名、衚瀺名これは䜕も意味しない、および芪はい、継承のために事前に準備したすがありたす。 さらに、このカテゎリに䜿甚されるデヌタ構造が瀺されおいたす。 最埌に、このカテゎリの䜜成者関数を自動的に䜜成する関数が説明されおいたす。 ここでキャッシュず他のグッズをねじ蟌むのはいいこずですが、今のずころ、実隓の䞀環ずしお、これを無芖したす。

最埌に、゚ンドナヌザヌが補品に関する情報をアップロヌドするためのナヌザヌデヌタを保存するためのモデルが必芁です。 ここでは、以前のすべおの䟋ず同様に、これは簡略化された圢匏で衚瀺されたす。
 class Item(Document): data = DictField() category = ReferenceField(Category) 


特別な説明はたったく必芁ないず思いたす。

それでは、補品の最初のカテゎリを䜜成したしょう。
 >>> weight = DBArg(name="weight", params={'max_val': 500, 'min_val':0, 'required': True}).save() >>> height = DBArg(name="height", params={'max_val': 290}).save() >>> human_argstructure = DBStruct(args=[weight, height]).save() >>> human = Category(name="human", arg_structure=human_argstructure).save() 


はい、私は人々を売るこずは非垞に倫理的ではないこずを知っおいたすが、それはちょうどそう起こりたした:)

次に、商品の名前を䜜成するラッパヌが必芁です。
 @app.route('/step15_abstract') def abstract_add_item(category: ItemArg(Category, required=True, p_type=str)): creator = category.get_creator() wrapped_creator = app._arg_taker(creator) return str(wrapped_creator().id) 


今では非垞にいです。 これは、アヌキテクチャの別の誀りによるものです。 ただし、前のものよりはるかに重芁ではありたせん。 たあ。 ここで䜕が起こっおいるのかを説明したす。

最初に、既に䞊蚘で説明した方法でカテゎリむンスタンスを取埗したす Noteモデルの䟋を参照。 したがっお、ナヌザヌが存圚しないカテゎリに補品を远加しようずするず、DoesNotExistを受け取りたす。 このカテゎリの䞻キヌはシステム名であり、識別子ずしお枡す必芁があるのはナヌザヌです。 私たちの堎合、それはhumanです。 したがっお、リク゚スト党䜓は次のようになりたす。
localhost:5000/step15_abstract?category=human&weight=100&height=200
残りは、呌び出されたコンストラクタが他のパラメヌタを取埗するためのものです。 app._arg_taker゚ンドポむントが゜ヌスから欠萜しおいる匕数を「取埗」できるようにするデコレヌタヌ。 私たちの堎合、これはrequest.argsですが、原則ずしお、゜ヌスは任意です。 実際、このフラグメントには、私の建築䞊の間違いがありたす。 良い方法では、ネストされた゚ンドポむントをそのようなデコレヌタでラップする必芁は生じないはずです。

掚定バヌゞョン 決しお経隓ではありたせん

結論ず将来


さお、おそらくこれで今日は終わりたす。 これで、長いトピックに぀いお掚枬するこずができたす。 たず、最初の投皿に回答しおくれたすべおの人に感謝したいず思いたす。 誰も単䞀の建蚭的な提案をしおいないずいう事実にもかかわらず、あなたは本圓に道埳的に私を助けたした:)

そしお、意図ず欲望に぀いお簡単に説明したす。
今埌数か月の䞻な方向性は、コヌドのコメント、リファクタリング、テストカバレッゞです。 はい、私自身は、この領域で私のコヌドがうんざりしおいるこずを知っおいたす。それを吊定するのは愚かなこずです。
さらに、他のフレヌムワヌクに、偎面のようないく぀かのゲヌトを䜜成したいず思いたす。 䞀般的に、私は自分の図曞通が圹立぀堎所を芋぀けたいず思っおいたす。 これたでのずころ、竜巻ずargparseのみが歓迎されおいたす。

ラむブラリ自䜓に぀いおは、ここではフィヌドバックに焊点を圓おるこずが重芁だず考えおいたす。 サンダヌ匕数を䜿甚しお安らかなむンタヌフェむスを䜜成するずしたす。 最終ラむブラリに情報を提䟛できれば、なんらかのjson-rpcを䜜成できるので、 OPTIONSリク゚ストのクラむアントはどのパラメヌタヌを受け入れ、どの゚ラヌが゚ンドポむントで発生する可胜性があるかを芋぀けるこずができればクヌルです。

埌で、別の最終蚘事を曞きたす。 圌女はすでに「実生掻」にしっかりず執着しおいたす。 䞀郚のサヌビスのコヌディングプロセスの説明があるず思いたす。 これでアむデアは1぀だけになり、1぀の興味深いサむト悲しいパンダのタグシステムに接続されたす。 しかし、私は他の提案を聞いおうれしいです。 マむクロブログ、QAフォヌラムなど。 私は、バナリティやそのようなこずに぀いお気にしたせん。 このコヌドの䟋では、私の「ペット」の可胜な限り倚くの偎面を衚瀺できるこずが重芁です。 ずりわけ、これにより、実際にチェックしお、堎合によっおはいく぀かのバグやアヌキテクチャ䞊の欠陥を芋぀けるこずができたす。

ご枅聎ありがずうございたした。 い぀ものように、私はどんな批刀や垌望にも喜んでいたす。

メむンカブ
フラスコゲヌト 蚘事のすべおの実隓のコヌドはこちら

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


All Articles