AtCoderStudyBooster 0.2__py3-none-any.whl → 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- atcdr/download.py +251 -240
- atcdr/generate.py +184 -193
- atcdr/login.py +133 -0
- atcdr/logout.py +24 -0
- atcdr/main.py +24 -17
- atcdr/markdown.py +22 -22
- atcdr/open.py +30 -30
- atcdr/submit.py +297 -0
- atcdr/test.py +382 -244
- atcdr/util/execute.py +52 -52
- atcdr/util/filetype.py +81 -67
- atcdr/util/gpt.py +102 -96
- atcdr/util/parse.py +206 -0
- atcdr/util/problem.py +94 -91
- atcdr/util/session.py +140 -0
- atcoderstudybooster-0.3.1.dist-info/METADATA +205 -0
- atcoderstudybooster-0.3.1.dist-info/RECORD +21 -0
- {atcoderstudybooster-0.2.dist-info → atcoderstudybooster-0.3.1.dist-info}/WHEEL +1 -1
- atcdr/util/cost.py +0 -120
- atcoderstudybooster-0.2.dist-info/METADATA +0 -96
- atcoderstudybooster-0.2.dist-info/RECORD +0 -17
- {atcoderstudybooster-0.2.dist-info → atcoderstudybooster-0.3.1.dist-info}/entry_points.txt +0 -0
atcdr/generate.py
CHANGED
@@ -3,104 +3,92 @@ import os
|
|
3
3
|
import re
|
4
4
|
|
5
5
|
from rich.console import Console
|
6
|
+
from rich.panel import Panel
|
7
|
+
from rich.syntax import Syntax
|
6
8
|
|
7
|
-
from atcdr.test import
|
8
|
-
LabeledTestCaseResult,
|
9
|
-
ResultStatus,
|
10
|
-
create_testcases_from_html,
|
11
|
-
judge_code_from,
|
12
|
-
render_results,
|
13
|
-
)
|
9
|
+
from atcdr.test import ResultStatus, TestRunner, create_renderable_test_info
|
14
10
|
from atcdr.util.execute import execute_files
|
15
11
|
from atcdr.util.filetype import (
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
12
|
+
FILE_EXTENSIONS,
|
13
|
+
Filename,
|
14
|
+
Lang,
|
15
|
+
lang2str,
|
16
|
+
str2lang,
|
21
17
|
)
|
22
|
-
from atcdr.util.gpt import ChatGPT, set_api_key
|
23
|
-
from atcdr.util.
|
18
|
+
from atcdr.util.gpt import ChatGPT, Model, set_api_key
|
19
|
+
from atcdr.util.parse import ProblemHTML
|
24
20
|
|
25
21
|
|
26
22
|
def get_code_from_gpt_output(output: str) -> str:
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
def render_result_for_GPT(
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
)
|
81
|
-
with open(saved_filename, 'w') as f:
|
82
|
-
console.print(
|
83
|
-
f'[green][+][/green] {gpt.model.value} の出力したコードを保存しました:{f.name}'
|
84
|
-
)
|
85
|
-
f.write(code)
|
86
|
-
|
87
|
-
console.print(f'[info] AI利用にかかったAPIコスト: {gpt.sum_cost}')
|
23
|
+
pattern = re.compile(r'```(?:\w+)?\s*(.*?)\s*```', re.DOTALL)
|
24
|
+
match = pattern.search(output)
|
25
|
+
return match.group(1) if match else ''
|
26
|
+
|
27
|
+
|
28
|
+
def render_result_for_GPT(
|
29
|
+
test: TestRunner,
|
30
|
+
) -> tuple[str, bool]:
|
31
|
+
results = list(test)
|
32
|
+
|
33
|
+
match test.info.summary:
|
34
|
+
case ResultStatus.AC:
|
35
|
+
return 'Accepted', True
|
36
|
+
case ResultStatus.CE:
|
37
|
+
return f'Compile Error \n {test.info.compiler_message}', False
|
38
|
+
case _:
|
39
|
+
message_for_gpt = ''.join(
|
40
|
+
f'\n{result.label} => {result.result.passed.value}\nInput :\n{result.testcase.input}\nOutput :\n{result.result.output}\nExpected :\n{result.testcase.output}\n'
|
41
|
+
if result.result.passed == ResultStatus.WA
|
42
|
+
else f'\n{result.label} => {result.result.passed.value}\nInput :\n{result.testcase.input}\nOutput :\n{result.result.output}\n'
|
43
|
+
for result in results
|
44
|
+
)
|
45
|
+
return message_for_gpt, False
|
46
|
+
|
47
|
+
|
48
|
+
def generate_code(file: Filename, lang: Lang, model: Model) -> None:
|
49
|
+
console = Console()
|
50
|
+
with open(file, 'r') as f:
|
51
|
+
html_content = f.read()
|
52
|
+
md = ProblemHTML(html_content).make_problem_markdown('en')
|
53
|
+
|
54
|
+
if set_api_key() is None:
|
55
|
+
return
|
56
|
+
gpt = ChatGPT(
|
57
|
+
system_prompt=f"""You are an excellent programmer. You solve problems in competitive programming.When a user provides you with a problem from a programming contest called AtCoder, including the Problem,Constraints, Input, Output, Input Example, and Output Example, please carefully consider these and solve the problem.Make sure that your output code block contains no more than two blocks. Pay close attention to the Input, Input Example, Output, and Output Example.Create the solution in {lang2str(lang)}.""",
|
58
|
+
model=model,
|
59
|
+
)
|
60
|
+
with console.status(f'コード生成中 (by {gpt.model.value})'):
|
61
|
+
reply = gpt.tell(md)
|
62
|
+
|
63
|
+
code = get_code_from_gpt_output(reply)
|
64
|
+
console.print('[green][+][/green] コードの生成に成功しました. ')
|
65
|
+
console.rule(f'{gpt.model.value}による{lang2str(lang)}コード')
|
66
|
+
console.print(Syntax(code=code, lexer=lang2str(lang)))
|
67
|
+
|
68
|
+
saved_filename = (
|
69
|
+
os.path.splitext(file)[0] + f'_by_{gpt.model.value}' + FILE_EXTENSIONS[lang]
|
70
|
+
)
|
71
|
+
with open(saved_filename, 'w') as f:
|
72
|
+
console.print(
|
73
|
+
f'[green][+][/green] {gpt.model.value} の出力したコードを保存しました:{f.name}'
|
74
|
+
)
|
75
|
+
f.write(code)
|
88
76
|
|
89
77
|
|
90
78
|
def generate_template(file: Filename, lang: Lang) -> None:
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
79
|
+
console = Console()
|
80
|
+
with open(file, 'r') as f:
|
81
|
+
html_content = f.read()
|
82
|
+
md = ProblemHTML(html_content).make_problem_markdown('en')
|
83
|
+
|
84
|
+
if set_api_key() is None:
|
85
|
+
return
|
86
|
+
gpt = ChatGPT(
|
87
|
+
system_prompt='You are a highly skilled programmer. Your role is to create a template code for competitive programming.',
|
88
|
+
temperature=0.0,
|
89
|
+
)
|
90
|
+
|
91
|
+
propmpt = f"""
|
104
92
|
The user will provide a problem from a programming contest called AtCoder. This problem will include the Problem Statement, Constraints, Input, Output, Input Example, and Output Example. You should focus on the Constraints and Input sections to create the template in {lang2str(lang)}.
|
105
93
|
|
106
94
|
- First, create the part of the code that handles input. Then, you should read ###Input Block and ###Constraints Block.
|
@@ -109,112 +97,115 @@ The user will provide a problem from a programming contest called AtCoder. This
|
|
109
97
|
|
110
98
|
You must not solve the problem. Please faithfully reproduce the variable names defined in the problem.
|
111
99
|
"""
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
100
|
+
with console.status(f'{lang2str(lang)}のテンプレートを生成しています...'):
|
101
|
+
reply = gpt.tell(md + propmpt)
|
102
|
+
code = get_code_from_gpt_output(reply)
|
103
|
+
|
104
|
+
savaed_filename = os.path.splitext(file)[0] + FILE_EXTENSIONS[lang]
|
105
|
+
with open(savaed_filename, 'x') as f:
|
106
|
+
console.print(
|
107
|
+
f'[green][+][/green] テンプレートファイルを作成 :{savaed_filename}'
|
108
|
+
)
|
109
|
+
f.write(code)
|
110
|
+
|
111
|
+
|
112
|
+
def solve_problem(file: Filename, lang: Lang, model: Model) -> None:
|
113
|
+
console = Console()
|
114
|
+
with open(file, 'r') as f:
|
115
|
+
html = ProblemHTML(f.read())
|
116
|
+
|
117
|
+
md = html.make_problem_markdown('en')
|
118
|
+
labeled_cases = html.load_labeled_testcase()
|
119
|
+
|
120
|
+
if set_api_key() is None:
|
121
|
+
return
|
122
|
+
gpt = ChatGPT(
|
123
|
+
system_prompt=f"""You are a brilliant programmer. Your task is to solve an AtCoder problem. AtCoder is a platform that hosts programming competitions where participants write programs to solve algorithmic challenges.Please solve the problem in {lang2str(lang)}.""",
|
124
|
+
model=model,
|
125
|
+
)
|
126
|
+
|
127
|
+
file_without_ext = os.path.splitext(file)[0]
|
128
|
+
|
129
|
+
for i in range(1, 4):
|
130
|
+
with console.status(f'{i}回目のコード生成中 (by {gpt.model.value})'):
|
131
|
+
if i == 1:
|
132
|
+
test_report = ''
|
133
|
+
reply = gpt.tell(md)
|
134
|
+
else:
|
135
|
+
prompt = f"""The following is the test report for the code you provided:
|
136
|
+
{test_report}
|
137
|
+
Please provide an updated version of the code in {lang2str(lang)}."""
|
138
|
+
console.print(
|
139
|
+
f'[green][+][/] 次のプロンプトを{gpt.model.value}に与え,再生成します'
|
140
|
+
)
|
141
|
+
console.print(Panel(prompt))
|
142
|
+
reply = gpt.tell(prompt)
|
143
|
+
|
144
|
+
code = get_code_from_gpt_output(reply)
|
145
|
+
|
146
|
+
saved_filename = (
|
147
|
+
f'{i}_'
|
148
|
+
+ file_without_ext
|
149
|
+
+ f'_by_{gpt.model.value}'
|
150
|
+
+ FILE_EXTENSIONS[lang]
|
151
|
+
)
|
152
|
+
with open(saved_filename, 'w') as f:
|
153
|
+
console.print(f'[green][+][/] コードの生成に成功しました!:{f.name}')
|
154
|
+
f.write(code)
|
155
|
+
|
156
|
+
with console.status(
|
157
|
+
f'{gpt.model.value}が生成したコードをテスト中', spinner='circleHalves'
|
158
|
+
):
|
159
|
+
test = TestRunner(saved_filename, labeled_cases)
|
160
|
+
test_report, is_ac = render_result_for_GPT(test)
|
161
|
+
|
162
|
+
console.print(create_renderable_test_info(test.info))
|
163
|
+
|
164
|
+
if is_ac:
|
165
|
+
console.print('[green][+][/] コードのテストに成功!')
|
166
|
+
break
|
167
|
+
else:
|
168
|
+
console.print('[red][-][/] コードのテストに失敗!')
|
169
|
+
|
170
|
+
with open(
|
171
|
+
'log_'
|
172
|
+
+ file_without_ext
|
173
|
+
+ f'_by_{gpt.model.value}'
|
174
|
+
+ FILE_EXTENSIONS[Lang.JSON],
|
175
|
+
'w',
|
176
|
+
) as f:
|
177
|
+
console.print(
|
178
|
+
f'[green][+][/] {gpt.model.value}の出力のログを保存しました:{f.name}'
|
179
|
+
)
|
180
|
+
f.write(json.dumps(gpt.messages, indent=2))
|
181
|
+
return
|
193
182
|
|
194
183
|
|
195
184
|
def generate(
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
185
|
+
*source: str,
|
186
|
+
lang: str = 'Python',
|
187
|
+
model: str = Model.GPT41_MINI.value,
|
188
|
+
without_test: bool = False,
|
189
|
+
template: bool = False,
|
200
190
|
) -> None:
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
191
|
+
la = str2lang(lang)
|
192
|
+
model_enum = Model(model)
|
193
|
+
|
194
|
+
if template:
|
195
|
+
execute_files(
|
196
|
+
*source,
|
197
|
+
func=lambda file: generate_template(file, la),
|
198
|
+
target_filetypes=[Lang.HTML],
|
199
|
+
)
|
200
|
+
elif without_test:
|
201
|
+
execute_files(
|
202
|
+
*source,
|
203
|
+
func=lambda file: generate_code(file, la, model_enum),
|
204
|
+
target_filetypes=[Lang.HTML],
|
205
|
+
)
|
206
|
+
else:
|
207
|
+
execute_files(
|
208
|
+
*source,
|
209
|
+
func=lambda file: solve_problem(file, la, model_enum),
|
210
|
+
target_filetypes=[Lang.HTML],
|
211
|
+
)
|
atcdr/login.py
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
import threading
|
2
|
+
import time
|
3
|
+
|
4
|
+
import webview
|
5
|
+
from requests import Session
|
6
|
+
from rich.console import Console
|
7
|
+
|
8
|
+
from atcdr.util.session import load_session, save_session, validate_session
|
9
|
+
|
10
|
+
ATCODER_LOGIN_URL = 'https://atcoder.jp/login'
|
11
|
+
ATCODER_HOME_URL = 'https://atcoder.jp/home'
|
12
|
+
|
13
|
+
console = Console()
|
14
|
+
|
15
|
+
|
16
|
+
def login() -> None:
|
17
|
+
session = load_session()
|
18
|
+
if validate_session(session):
|
19
|
+
console.print('[green][+][/] すでにログインしています. ')
|
20
|
+
return
|
21
|
+
|
22
|
+
# Prompt in CLI
|
23
|
+
username = console.input('[cyan]ユーザー名: [/]').strip()
|
24
|
+
password = console.input('[cyan]パスワード: [/]').strip()
|
25
|
+
|
26
|
+
window = webview.create_window('AtCoder Login', ATCODER_LOGIN_URL, hidden=False)
|
27
|
+
|
28
|
+
def on_start():
|
29
|
+
js_fill = f"""
|
30
|
+
document.getElementById('username').value = '{username}';
|
31
|
+
document.getElementById('password').value = '{password}';
|
32
|
+
"""
|
33
|
+
window.evaluate_js(js_fill)
|
34
|
+
|
35
|
+
def poll_and_submit():
|
36
|
+
with console.status(
|
37
|
+
'キャプチャー認証を解決してください', spinner='circleHalves'
|
38
|
+
):
|
39
|
+
while True:
|
40
|
+
try:
|
41
|
+
token = window.evaluate_js(
|
42
|
+
'document.querySelector(\'input[name=\\"cf-turnstile-response\\"]\').value'
|
43
|
+
)
|
44
|
+
if token:
|
45
|
+
console.print('[green][+][/] ログインします')
|
46
|
+
window.evaluate_js(
|
47
|
+
"document.getElementById('submit').click();"
|
48
|
+
)
|
49
|
+
break
|
50
|
+
except Exception:
|
51
|
+
pass
|
52
|
+
|
53
|
+
time.sleep(0.5)
|
54
|
+
|
55
|
+
with console.status('ログインの結果の待機中...', spinner='circleHalves'):
|
56
|
+
while True:
|
57
|
+
try:
|
58
|
+
current_url = window.get_current_url()
|
59
|
+
except Exception:
|
60
|
+
current_url = None
|
61
|
+
|
62
|
+
if current_url and current_url.startswith(ATCODER_HOME_URL):
|
63
|
+
console.print('[green][+][/] ログイン成功!')
|
64
|
+
|
65
|
+
session = Session()
|
66
|
+
session = move_cookies_from_webview_to_session(window, session)
|
67
|
+
save_session(session)
|
68
|
+
window.destroy()
|
69
|
+
break
|
70
|
+
|
71
|
+
try:
|
72
|
+
err = window.evaluate_js(
|
73
|
+
'Array.from(document.querySelectorAll('
|
74
|
+
'\'div.alert.alert-danger[role="alert"]\'))'
|
75
|
+
".map(e=>e.textContent.trim()).filter(t=>t).join(' ')"
|
76
|
+
)
|
77
|
+
err = err.replace('\n', '').replace('\r', '').replace('\t', '')
|
78
|
+
except Exception:
|
79
|
+
err = ''
|
80
|
+
|
81
|
+
if err:
|
82
|
+
console.print(f'[red][-][/] エラー: {err}')
|
83
|
+
session = Session()
|
84
|
+
session = move_cookies_from_webview_to_session(window, session)
|
85
|
+
save_session(session)
|
86
|
+
window.destroy()
|
87
|
+
return
|
88
|
+
|
89
|
+
time.sleep(0.5)
|
90
|
+
|
91
|
+
t = threading.Thread(target=poll_and_submit, daemon=True)
|
92
|
+
t.start()
|
93
|
+
|
94
|
+
webview.start(on_start, private_mode=False)
|
95
|
+
|
96
|
+
|
97
|
+
def move_cookies_from_webview_to_session(
|
98
|
+
window: webview.Window, session: Session
|
99
|
+
) -> Session:
|
100
|
+
cookie_list = window.get_cookies()
|
101
|
+
for cookie_obj in cookie_list:
|
102
|
+
for cookie_name, morsel in cookie_obj.items():
|
103
|
+
# morselからデータを取得
|
104
|
+
value = morsel.value
|
105
|
+
|
106
|
+
domain = morsel.get('domain')
|
107
|
+
if domain is None:
|
108
|
+
domain = '.atcoder.jp'
|
109
|
+
|
110
|
+
path = morsel.get('path', '/')
|
111
|
+
secure = 'secure' in morsel
|
112
|
+
|
113
|
+
expires = None # __NSTaggedDateオブジェクトを回避
|
114
|
+
|
115
|
+
http_only = 'httponly' in morsel
|
116
|
+
|
117
|
+
# HttpOnlyをrestに含める
|
118
|
+
rest = {}
|
119
|
+
if http_only:
|
120
|
+
rest['HttpOnly'] = True
|
121
|
+
|
122
|
+
# セッションにクッキーを設定
|
123
|
+
session.cookies.set(
|
124
|
+
name=cookie_name,
|
125
|
+
value=value,
|
126
|
+
domain=domain,
|
127
|
+
path=path,
|
128
|
+
secure=secure,
|
129
|
+
expires=expires, # Noneを渡す
|
130
|
+
rest=rest,
|
131
|
+
)
|
132
|
+
|
133
|
+
return session
|
atcdr/logout.py
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
import webview
|
2
|
+
from rich import print
|
3
|
+
|
4
|
+
from atcdr.util.session import delete_session, load_session, validate_session
|
5
|
+
|
6
|
+
ATCODER_LOGIN_URL = 'https://atcoder.jp/login'
|
7
|
+
|
8
|
+
|
9
|
+
def logout() -> None:
|
10
|
+
session = load_session()
|
11
|
+
if not validate_session(session):
|
12
|
+
print('[red][-][/] ログインしていません.')
|
13
|
+
return
|
14
|
+
|
15
|
+
delete_session()
|
16
|
+
print('[green][+][/] ログアウトしました.')
|
17
|
+
|
18
|
+
window = webview.create_window('AtCoder Logout', ATCODER_LOGIN_URL, hidden=True)
|
19
|
+
|
20
|
+
def on_start():
|
21
|
+
window.clear_cookies()
|
22
|
+
window.destroy()
|
23
|
+
|
24
|
+
webview.start(on_start, private_mode=False)
|
atcdr/main.py
CHANGED
@@ -5,36 +5,43 @@ from rich.traceback import install
|
|
5
5
|
|
6
6
|
from atcdr.download import download
|
7
7
|
from atcdr.generate import generate
|
8
|
+
from atcdr.login import login
|
9
|
+
from atcdr.logout import logout
|
8
10
|
from atcdr.markdown import markdown
|
9
11
|
from atcdr.open import open_files
|
12
|
+
from atcdr.submit import submit
|
10
13
|
from atcdr.test import test
|
11
14
|
|
12
15
|
|
13
16
|
def get_version() -> None:
|
14
|
-
|
15
|
-
|
17
|
+
meta = metadata('AtCoderStudyBooster')
|
18
|
+
print(meta['Name'], meta['Version'])
|
16
19
|
|
17
20
|
|
18
21
|
MAP_COMMANDS: dict = {
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
22
|
+
'test': test,
|
23
|
+
't': test,
|
24
|
+
'download': download,
|
25
|
+
'd': download,
|
26
|
+
'open': open_files,
|
27
|
+
'o': open_files,
|
28
|
+
'generate': generate,
|
29
|
+
'g': generate,
|
30
|
+
'markdown': markdown,
|
31
|
+
'md': markdown,
|
32
|
+
'login': login,
|
33
|
+
'logout': logout,
|
34
|
+
'submit': submit,
|
35
|
+
's': submit,
|
36
|
+
'--version': get_version,
|
37
|
+
'-v': get_version,
|
31
38
|
}
|
32
39
|
|
33
40
|
|
34
41
|
def main():
|
35
|
-
|
36
|
-
|
42
|
+
install()
|
43
|
+
fire.Fire(MAP_COMMANDS)
|
37
44
|
|
38
45
|
|
39
46
|
if __name__ == '__main__':
|
40
|
-
|
47
|
+
main()
|