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フォーラムなど。 私は、バナリティやそのようなことについて気にしません。 このコードの例では、私の「ペット」の可能な限り多くの側面を表示できることが重要です。 とりわけ、これにより、実際にチェックして、場合によってはいくつかのバグやアーキテクチャ上の欠陥を見つけることができます。



ご清聴ありがとうございました。 いつものように、私はどんな批判や希望にも喜んでいます。



メインカブ

フラスコゲート (記事のすべての実験のコードはこちら)



All Articles