Skip to content

Latest commit

 

History

History
553 lines (444 loc) · 29.3 KB

README.md

File metadata and controls

553 lines (444 loc) · 29.3 KB

Build Status codecov PyPI

【重要】2.13.0以前のバージョンを利用するとルール違反になってしまうケースに関する注意喚起 (2024/6/7)

2024/6/7にAtCoderがatcoder-toolsを含むコード自動生成ツールを特定のコンテストで禁止するルール変更を行いました。詳しくは『生成AIの台頭に伴うABCにおけるルール変更について』を御覧ください。

atcoder-toolsの入力解析を伴うコード自動生成機能をAtCoder Beginer Contestのコンテスト開催期間中に利用してコード生成を行うことはUnrated, Ratedでの参加を問わず禁止となります。

これに伴い、バージョン2.14.0にて、開催中のABCコンテストにおいては入力解析部分の実行をスキップするような仕様変更を行いました。

挙動としましては、コンテストの種類がABCでありかつ開催中に限り、

Format prediction is skipped because it's not allowed by AtCoder rules

という警告と共に入力解析分をスキップするようになっています。入力部分のコードは生成されませんが、コード生成失敗時に生成されるテンプレートは生成され、MOD値やYES/NO等の定数抽出は行われます。また、問題文に存在するテストケースも生成されます。

2.13.0以前のatcoder-tools にはコンテストの種類を検知して生成をスキップする機能はありませんので、2.14.0にアップデートして頂くか、各自の責任において利用を避けて頂くようお願い申し上げます。

ライセンスにもありますように、利用しているバージョンに関わらずこのツールを利用したことで生じるあらゆる損害に対し、atcoder-tools管理者は何の責任も負う必要がないことを明記しておきます。

AtCoder Tools

Python 3.6 以降で動作する AtCoder からサンプル入力をダウンロードしたりする際に便利なツールです。

このツールには次のような機能があります。

  • AtCoderへのログイン,入出力例データなどの抽出
  • 枝刈り探索による高精度・高速な入力フォーマット解析 (ARC、ABC、AGCについては約9割ほど)
  • 問題文中に含まれるMOD値、YES/NO文字列、誤差ジャッジのための誤差値等の定数値抽出
  • サンプルのローカルテスト機能
    • 誤差ジャッジに対応 by @chaemon
  • コード提出機能
  • 入力フォーマット解析結果や抽出した定数値を用いたテンプレートからのコード自動生成(以下の表に記載されている言語をサポートしています)
    • カスタムテンプレートに対応
    • 他言語対応のためのコントリビューション(≒中間形式からコードに変換する部分のPR)を募集中です!
対応言語 Contributor 1 Contributor 2
C++ @kyuridenamida (generator, template) @asi1024 (template)
Java @kyuridenamida (generator, template)
Rust @fukatani (generator, template) @koba-e964 (template, CR)
Python3 @kmyk (generator, template) @penpenpng (generator)
D @penpenpng (generator, template)
Nim @chaemon (generator, template)
C# @chaemon (generator, template)
Swift @firewood (generator, template)
Go @nu50218 (generator, template) @chaemon (generator, template)
Julia @yatra9 (generator, template) @chaemon (generator, template)

Demo

How to install

pip3 install atcoder-tools

ただのpipだとPython 2系を使ってインストールされる可能性があるためうまくいかないかもしれません。

Userscript by @kmyk (NEW! 2019/03/06)

Tampermonkey(各種ブラウザで動作)でインストールすることが可能なUserscriptです。公開されている過去問を対象として、atcoder-toolsで自動生成されたコードをそのままAtCoderのスニペット上で利用できます。

  1. Tampermonkey をインストールする (Chrome, FireFox)
  2. https://kyuridenamida.github.io/atcoder-tools/index.user.js にアクセスしてUserscriptをインストール
  3. ログインした状態で適当な問題ページに行く(e.g. https://atcoder.jp/contests/abc120/tasks/abc120_d)
  4. 正しくインストールされている場合、ページ下部のコードスニペットにコードが標準で埋め込まれている (atcoder-toolsの対応言語のみ)

Analysis

https://kyuridenamida.github.io/atcoder-tools/

各問題ごとの解析結果などが載っています。

Usage

重要: かつてパスワード入力なしでログインを実現するためにAccountInformation.pyにログイン情報を書き込むことを要求していましたが、セキュリティリスクが高すぎるため、セッション情報のみを保持する方針に切り替えました。 今後はできるだけ保持されているセッション情報を利用してAtCoderにアクセスし、必要に応じて再入力を要求します。 過去のユーザーの皆様にはAccountInformation.pyを削除して頂くようお願い申し上げます。

  • atcoder-tools gen {contest_id} コンテスト環境を用意します。
  • atcoder-tools test カレント・ディレクトリ上に実行ファイルと入出力(in_*.txt, out_*.txt)がある状態で実行するとローカルテストを行います。
  • atcoder-tools submit カレント・ディレクトリ上で実行すると対応する問題がサンプルに通る場合ソースコードを提出します。既にAtCoder上にその問題に対する提出がある場合、-uを指定しないと提出できないようになっています。
  • atcoder-tools version 現在の atcoder-tools のバージョンを出力します。

atcoder-tools gen --helpatcoder-tools genの引数の詳細について確認することができます。

例:

atcoder-tools gen agc001
cd ~/atcoder-workspace/agc001/A
g++ main.cpp
atcoder-tools test

--without-login 引数を指定するとログインなしでデータをダウンロードできます(一般公開されているコンテストのみ)。

$ atcoder-tools gen  [contest_id] --without-login

gen の詳細

usage: atcoder-tools gen
       [-h] [--without-login] [--workspace WORKSPACE] [--lang LANG]
       [--template TEMPLATE] [--parallel] [--save-no-session-cache]
       [--skip-existing-problems] [--config CONFIG]
       contest_id

positional arguments:
  contest_id            Contest ID (e.g. arc001)

optional arguments:
  -h, --help            show this help message and exit
  --without-login       Download data without login
  --workspace WORKSPACE
                        Path to workspace's root directory. This script will create files in {WORKSPACE}/{contest_name}/{alphabet}/ e.g. ./your-workspace/arc001/A/
                        [Default] /home/kyuridenamida/atcoder-workspace
  --lang LANG           Programming language of your template code, cpp or java or rust or python or nim or d or cs or julia.
                        [Default] cpp
  --template TEMPLATE   File path to your template code
                        [Default (C++)] /atcodertools/tools/templates/default_template.cpp
                        [Default (Java)] /atcodertools/tools/templates/default_template.java
                        [Default (Rust)] /atcodertools/tools/templates/default_template.rs
                        [Default (Python3)] /atcodertools/tools/templates/default_template.py
                        [Default (NIM)] /atcodertools/tools/templates/default_template.nim
                        [Default (D)] /atcodertools/tools/templates/default_template.d
                        [Default (C#)] /atcodertools/tools/templates/default_template.cs
  --parallel            Prepare problem directories asynchronously using multi processors.
  --save-no-session-cache
                        Save no session cache to avoid security risk
  --skip-existing-problems
                        Skip processing every problem for which a directory already exists
  --config CONFIG       File path to your config file
                        [Default (Primary)] /home/kyuridenamida/.atcodertools.toml
                        [Default (Secondary)] /atcoder-tools/atcodertools/tools/atcodertools-default.toml

test の詳細

usage: atcoder-tools test [-h] [--exec EXEC] [--num NUM]
                                         [--dir DIR] [--timeout TIMEOUT]
                                         [--knock-out]
                                         [--skip-almost-ac-feedback]
                                         [--judge-type JUDGE_TYPE]
                                         [--error-value ERROR_VALUE]

optional arguments:
  -h, --help            show this help message and exit
  --exec EXEC, -e EXEC  File path to the execution target. [Default] Automatically detected exec file
  --num NUM, -n NUM     The case number to test (1-origin). All cases are tested if not specified.
  --dir DIR, -d DIR     Target directory to test. [Default] Current directory
  --timeout TIMEOUT, -t TIMEOUT
                        Timeout for each test cases (sec) [Default] 1
  --knock-out, -k       Stop execution immediately after any example's failure [Default] False
  --skip-almost-ac-feedback, -s
                        Hide inputs and expected/actual outputs if result is correct and there are error outputs [Default] False,
  --judge-type JUDGE_TYPE, -j JUDGE_TYPE
                        error type must be one of [normal, absolute, relative, absolute_or_relative]
  --error-value ERROR_VALUE, -v ERROR_VALUE
                        error value for decimal number judge: [Default] 0.000000001

submit の詳細

usage: atcoder-tools submit [-h] [--exec EXEC] [--dir DIR]
                                           [--timeout TIMEOUT] [--code CODE]
                                           [--force] [--save-no-session-cache]
                                           [--unlock-safety]
                                           [--judge-type JUDGE_TYPE]
                                           [--error-value ERROR_VALUE]

optional arguments:
  -h, --help            show this help message and exit
  --exec EXEC, -e EXEC  File path to the execution target. [Default] Automatically detected exec file
  --dir DIR, -d DIR     Target directory to test. [Default] Current directory
  --timeout TIMEOUT, -t TIMEOUT
                        Timeout for each test cases (sec) [Default] 1
  --code CODE, -c CODE  Path to the source code to submit [Default] Code path written in metadata.json
  --force, -f           Submit the code regardless of the local test result [Default] False
  --save-no-session-cache
                        Save no session cache to avoid security risk
  --unlock-safety, -u   By default, this script only submits the first code per problem. However, you can remove the safety by this option in order to submit codes twice or more.
  --judge-type JUDGE_TYPE, -j JUDGE_TYPE
                        error type must be one of [normal, absolute, relative, absolute_or_relative]
  --error-value ERROR_VALUE, -v ERROR_VALUE
                        error value for decimal number judge: [Default] 1e-09

codegen の詳細

usage: ./atcoder-tools codegen [-h] [--without-login] [--lang LANG]
                               [--template TEMPLATE] [--save-no-session-cache]
                               [--config CONFIG]
                               url

positional arguments:
  url                   URL (e.g. https://atcoder.jp/contests/abc012/tasks/abc012_3)

optional arguments:
  -h, --help            show this help message and exit
  --without-login       Download data without login
  --lang LANG           Programming language of your template code, cpp or java or rust.
                        [Default] cpp
  --template TEMPLATE   File path to your template code
                        [Default (C++)] /home/user/GitHub/atcoder-tools/atcodertools/tools/templates/default_template.cpp
                        [Default (Java)] /home/user/GitHub/atcoder-tools/atcodertools/tools/templates/default_template.java
                        [Default (Rust)] /home/user/GitHub/atcoder-tools/atcodertools/tools/templates/default_template.rs
  --save-no-session-cache
                        Save no session cache to avoid security risk
  --config CONFIG       File path to your config file
                        [Default (Primary)] /home/user/.atcodertools.toml
                        [Default (Secondary)] /home/user/GitHub/atcoder-tools/atcodertools/tools/atcodertools-default.toml

設定ファイルの例

~/.atcodertools.tomlに以下の設定を保存すると、コードスタイルや、コード生成後に実行するコマンドを指定できます。 設定ファイルはcodestyle, postprocess, tester, submit, etcのテーブルに分かれていて、codestyle.nimというようにテーブル名の後に.[言語名]で指定するとその言語のみに適用されます。

以下は、次の挙動を期待する場合の~/.atcodertools.tomlの例です。

  • indent_type='space' スペースがインデントに使われる('tab'を指定した場合はタブが使われる)

  • indent_width=4 インデント幅は4である (indent_widthが無指定の場合4(nim言語以外), 2(nim言語)が規定値として使われます。)

  • template_file='~/my_template.cpp' コード生成テンプレートとして~/my_template.cppを使う

  • workspace_dir='~/atcoder-workspace/' ワークスペースのルートは ~/atcoder-workspace/

  • lang='cpp' 言語設定は cpp (提出時もしくはデフォルトのコードジェネレーター生成時に使われます)

  • code_generator_file="~/custom_code_generator.py" カスタムコードジェネレーター ~/custom_code_generator.pyを指定する

  • code_generator_toml="~/universal_code_generator.toml" ユニバーサルコードジェネレーター ~/universal_code_generator.tomlを指定する

  • exec_on_each_problem_dir='clang-format -i ./*.cpp' exec_on_contest_dir='touch CMakeLists.txt'

    • 問題用ディレクトリ内で毎回clang-formatを実行して、最後にCMakeLists.txt(空)をコンテスト用ディレクトリに生成する
  • compile_before_testing テスト前にコンパイルを実行するか否かをTrue/Falseで指定。何も指定しないとFalseとなります。

  • compile_only_when_diff_detected テスト前のコンパイルの際、元のソースに変更があった場合のみ実行するかをTrue/Falseで指定。何も指定しないとFalseとなります。

  • timeout_adjustment=1.2 問題文に記載された実行時間制限にこの値をかけた秒数がローカルテストの際の実行時間制限になります。例えばatcoderで制限時間2秒の問題は2x1.2=2.4秒となります。atcoderとローカルの実行環境が異なる場合の調整に使用してください。

なお、compile_before_testing, compile_only_when_diff_detectedはいずれもtesterの引数で指定することも可能で、指定した場合はそちらが優先されます。

  • download_without_login=false AtCoderにログインせずにダウンロードを行う機能を使わない (公開コンテストに対してのみ可能)
  • parallel_download=false データの並列ダウンロードを無効にする
  • save_no_session_cache=false ログイン情報のクッキーを保存する
  • skip_existing_problems=false ディレクトリが既に存在する問題の処理をスキップする
  • in_example_format="in_{}.txt" テストケース(input)のフォーマットをin_1.txt, in_2.txt, ...とする
  • out_example_format="out_{}.txt" テストケース(output)のフォーマットをout_1.txt, out_2.txt, ...とする
[codestyle]
indent_type='space' # 'tab' or 'space'
indent_width=4
template_file='~/my_template.cpp'
workspace_dir='~/atcoder-workspace/'
lang='cpp' # Check README.md for the supported languages.
code_generator_file="~/custom_code_generator.py"
[postprocess]
exec_on_each_problem_dir='clang-format -i ./*.cpp'
exec_on_contest_dir='touch CMakeLists.txt'
[compiler]
compile_command='g++ main.cpp -o main -std=c++17'
compile_only_when_diff_detected=true
[tester]
compile_before_testing=true
compile_only_when_diff_detected=true
timeout_adjustment=1.2
[etc]
download_without_login=false
parallel_download=false
save_no_session_cache=false
skip_existing_problems=false
in_example_format="in_{}.txt"
out_example_format="out_{}.txt"

また、以下のように提出時にコマンドを実行してその結果を提出することが可能です。C++以外のAC-libraryを自動に展開するような用途で用いることができます。下記の例はNim言語でACLのexpanderを実行しその出力ファイルを提出し、その後ローカルの出力ファイルを削除するという設定です。 なお、C++に関してはAC-libraryがatcoderのジャッジにも搭載されているためこのような設定は不要です。

  • exec_before_submit 提出前に実行するコマンド
  • submit_filename exec_before_submitを実行した結果提出するファイルが変わる場合に指定
  • exec_after_submit 提出後に行う処理
[submit.nim]
exec_before_submit='rm ./combined.nim | python3 ~/git/Nim-ACL/expander.py main.nim --lib /home/chaemon/git/Nim-ACL/ -s'
exec_after_submit='rm ./combined.nim'
submit_filename='./combined.nim'

カスタムコードジェネレーター

標準のC++コードジェネレーターに倣って、 (CogeGenArgs) -> str(ソースコード)が型であるようなmain関数を定義した.pyファイルをcode_generator_fileで指定すると、コード生成時にカスタムコードジェネレーターを利用できます。

ユニバーサルコードジェネレーター

ユニバーサルコードジェネレーターはループ・配列アクセス方法等のいくつかの言語仕様を記述するだけでカスタムコードジェネレーターよりも簡単にコード生成することを意図して作成したジェネレーターです。設定ファイルのcode_generator_tomlで指定します。書き方は以下です。

  • base_indent 入力部分のインデント数
  • insert_space_around_operators 入力部分の変数や演算子の間にスペースを入れるかどうかをtrue/falseで指定
  • newline_after_input 入力部分で入力ごとに空行を入れるかどうかをtrue/falseで指定
  • global_prefix グローバル変数の宣言時に入れる接頭辞(Javaなどでstaticを指定したりできます)

以下のようにテーブルを定義します。各項目はダブルコーテーションあるいはシングルコーテーションを用いた文字列で指定します。Pythonのformatメソッドに渡されるため、波括弧等の文字を直に書きたい場合はエスケープする必要があります。 テーブルのキーは整数(int), 浮動小数(float), 文字列(str), およびこれら3つを使った1次元配列(seq), 2次元配列(2d_seq)となっています。

  • [index] ループインデックスの名称を指定します。1重目をi, 2重目をjで指定してください。省略可能で省略した場合はi, jが指定されます。Perl, PHPなどの言語で$i, $jなどとi, j以外の名前を指定しなければならないとき用のつもりです。

  • [loop] ループに関することを記述します

    • header ループの最初に記述する内容。ループを回すための変数は{loop_var}, 回す回数は{length}を用いてください。
    • footer ループの最後に記述する内容。C++, Javaでは閉じカッコになります。波括弧の場合は}}とエスケープする必要があることに注意してください。
  • [type] タイプ(int, float, str)のタイプについて記述します。例を参照してください。

  • [default] デフォルトの値について記述します。例を参照してください。注意: TOMLの表記に癖があるようで、ダブルコーテーション2つ(空の文字列)を表記する際にはstr='""'とするとよいようです。""""だとエラーになるようです。

  • [input_func] int, float, strについて入力時に呼び出す関数を記述します。

  • [arg] solve関数の引数の記述方法について指定します。int, float, str, seq, 2d_seqについて記述してください。{name}が変数名, {type}seq, 2d_seqについてベースとなる型です。

  • [actual_arg] seq, 2d_seqについてsolve関数を呼び出す際の引数の渡し方について記述します。C++などでmoveをつかってメモリを節約したいときなどに指定できます。省略可能で、省略した場合はそのまま渡されます。

  • [access] 配列のアクセス方法について記述します。seq, 2d_seqについて指定してください。{name}で変数名, {index_i}, {index_j}でインデックス名を指定します。

以下は宣言・確保・入力を行うためのコードを記述します。いくつかを同時に行う方法も指定できます。いずれも一行または複数行に渡る指定が可能でセミコロン等の終端子も(必要な言語では)記述してください。 キーワードとして{name}, {type}はそれぞれ対象となる変数名、タイプ名で、上記で指定した{default}が使えます。また、指定していれば{input_func}も使えます。seq, 2d_seqの場合は{type}はベースとなる型名になります(vector<int>におけるint)のでご注意ください。また、seqの長さは{length}, 2d_seqの長さは{length_i}, {length_j}となっています。

  • [declare] int, float, str, seq, 2d_seqの宣言方法について記述します。
  • [allocate] seq, 2d_seqの確保の方法を記述します。
  • [declare_and_allocate] seq, 2d_seqについて宣言と確保を同時に行う方法について記述します。
  • [input] int, float, strの入力方法について記述します。

以下は入力コードの冗長性を下げる目的で指定するテーブルで省略可能なものです。指定方法についてはPythonの設定を参照してください。

  • [allocate_and_input] seq, 2d_seqについて確保と入力をまとめて行うことができる場合に記述します。省略した場合、上記で指定した確保と入力の方式を複合したものが挿入されます
  • [declare_and_allocate_and_input] seq, 2d_seqについて宣言・確保・入力をまとめて行うことができる場合に記述します。省略した場合、上記で指定した宣言と確保と入力の方式を複合したものが挿入されます

例えばC++での設定方法は以下です。

base_indent = 1
insert_space_around_operators = false

# global変数宣言時の接頭辞
global_prefix = ""

# ループ
[loop]
header = "for(int {loop_var} = 0 ; {loop_var} < {length} ; {loop_var}++){{"
footer = "}}"

# タイプ
[type]
int = "long long"
float = "long double"
str = "std::string"

# デフォルト値
[default]
int = "0"
float = "0.0"
str = '""'

# 引数
[arg]
int = "long long {name}"
float = "long double {name}"
str = "std::string {name}"
seq = "std::vector<{type}> {name}"
2d_seq = "std::vector<std::vector<{type}>> {name}"

# 引数への渡し方
[actual_arg]
seq = "std::move({name})"
2d_seq = "std::move({name})"

# 配列アクセス
[access]
seq = "{name}[{index}]"
2d_seq = "{name}[{index_i}][{index_j}]"

# 宣言
[declare]
int = "long long {name};"
float = "long double {name};"
str = "std::string {name};"
seq = "std::vector<{type}> {name};"
2d_seq = "std::vector<std::vector<{type}>> {name};"

# 確保
[allocate]
seq = "{name}.assign({length}, {default});"
2d_seq = "{name}.assign({length_i}, std::vector<{type}>({length_j}));"

# 宣言と確保
[declare_and_allocate]
seq = "std::vector<{type}> {name}({length});"
2d_seq = "std::vector<std::vector<{type}>> {name}({length_i}, std::vector<{type}>({length_j}));"

# 入力
[input]
#int = "std::cin >> {name};"
int = "std::scanf(\"%lld\", &{name});"
#float = "std::cin >> {name};"
float = "std::scanf(\"%Lf\", &{name});"
str = "std::cin >> {name};"

例えばPythonでの設定方法は以下です。

base_indent = 1
insert_space_around_operators = true

# global変数宣言時の接頭辞
global_prefix = ""

# インデックス
[index]
i = "i"
j = "j"

# ループ
[loop]
header = "for {loop_var} in range({length}):"
footer = ""

# タイプ
[type]
int = "int"
float = "float"
str = "str"

# デフォルト値
[default]
int = "int()"
float = "float()"
str = "str()"

# 宣言
[declare]
int = ""
float = ""
str = ""
seq = ""
2d_seq = ""

# 確保
[allocate]
seq = "{name} = [{default}] * ({length})"
2d_seq = "{name} = [[{default}] * ({length_j}) for _ in {length_i}]"

# 宣言と確保
[declare_and_allocate]
seq = "{name} = [{default}] * ({length})  # type: \"List[{type}]\""
self.declare_and_allocate_2d_seq = "{name} = [[{default}] * ({length_j}) for _ in {length_i}]  # type: \"List[List[{type}]]\""

# 入力関数
[input_func]
int = "int(next(tokens))"
float = "float(next(tokens))"
str = "next(tokens)"

# 入力
[input]
int = "{name} = {input_func}"
float = "{name} = {input_func}"
str = "{name} = {input_func}"

# 宣言と入力
[declare_and_input]
int = "{name} = {input_func}  # type: int"
float = "{name} = {input_func}  # type: float"
str = "{name} = {input_func}  # type: str"

# 確保と入力
[allocate_and_input]
seq = "{name} = [{input_func} for _ in range({length})]"
2d_seq = "{name} = [[{input_func} for _ in range({length_j})] for _ in range({length_i})]"

# 宣言と確保と入力
[declare_and_allocate_and_input]
seq = "{name} = [{input_func} for _ in range({length})]  # type: \"List[{type}]\""
2d_seq = "{name} = [[{input_func} for _ in range({length_j})] for _ in range({length_i})]  # type: \"List[List[{type}]]\""

# 引数
[arg]
int = "{name}: int"
float = "{name}: float"
str = "{name}: str"
seq = "{name}: \"List[{type}]\""
2d_seq = "{name}: \"List[List[{type}]]\""

# 配列アクセス
[access]
seq = "{name}[{index}]"
2d_seq = "{name}[{index_i}][{index_j}]"

テンプレートの例

atcoder-tools genコマンドに対し--templateでテンプレートソースコードを指定できます。 テンプレートエンジンの仕様についてはjinja2 の公式ドキュメントを参照してください。

テンプレートに渡される変数は以下の通りです。

  • prediction_success 入力形式の推論に成功したとき True、 失敗したとき Falseが格納されている。この値がTrueのとき次の3種類の変数も存在することが保証される。

    • input_part input用のコード
    • formal_arguments 型つき引数列
    • actual_arguments 型なし引数列
  • mod 問題文中に存在するmodの整数値

  • yes_str 問題文中に存在する yes や possible などの真を表しそうな文字列値

  • no_str 問題文中に存在する no や impossible などの偽を表しそうな文字列値

#include <bits/stdc++.h>
using namespace std;

{% if mod %}
const long long MOD = {{ mod }};
{% endif %}
{% if yes_str %}
const string YES = "{{ yes_str }}";
{% endif %}
{% if no_str %}
const string NO = "{{ no_str }}";
{% endif %}

{% if prediction_success %}
void solve({{ formal_arguments }}){

}
{% endif %}

int main(){
    {% if prediction_success %}
    {{input_part}}
    solve({{ actual_arguments }});
    {% else %}
    // Failed to predict input format
    {% endif %}
    return 0;
}

Contribution

CONTRIBUTING.md を参照してください。

Licence

MIT

Author

kyuridenamida (@kyuridenamida)