VocabMaster 0.3.0__tar.gz → 0.3.2__tar.gz
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.
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/PKG-INFO +4 -5
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/VocabMaster.egg-info/PKG-INFO +4 -5
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/VocabMaster.egg-info/SOURCES.txt +1 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/VocabMaster.egg-info/requires.txt +2 -2
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/pyproject.toml +6 -7
- vocabmaster-0.3.2/tests/test_gpt_integration_sdk_boundary.py +158 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/vocabmaster/gpt_integration.py +15 -37
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/LICENSE +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/README.md +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/VocabMaster.egg-info/dependency_links.txt +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/VocabMaster.egg-info/entry_points.txt +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/VocabMaster.egg-info/top_level.txt +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/setup.cfg +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/tests/test_cli.py +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/tests/test_config_handler.py +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/tests/test_csv_handler.py +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/tests/test_gpt_integration.py +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/tests/test_path_traversal_prevention.py +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/tests/test_recovery.py +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/tests/test_same_language_integration.py +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/tests/test_utils.py +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/vocabmaster/__init__.py +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/vocabmaster/cli.py +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/vocabmaster/config_handler.py +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/vocabmaster/csv_handler.py +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/vocabmaster/recovery.py +0 -0
- {vocabmaster-0.3.0 → vocabmaster-0.3.2}/vocabmaster/utils.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: VocabMaster
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Master new languages with this CLI tool, designed to help you record vocabulary and create Anki flashcards without the need to manually input translations or example sentences.
|
|
5
5
|
Author: Sébastien De Revière
|
|
6
|
-
License: Apache-2.0
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
7
|
Project-URL: Homepage, https://github.com/sderev/vocabmaster
|
|
8
8
|
Project-URL: Repository, https://github.com/sderev/vocabmaster
|
|
9
9
|
Project-URL: Issues, https://github.com/sderev/vocabmaster/issues
|
|
@@ -11,7 +11,6 @@ Keywords: vocabulary,language-learning,anki,flashcards,cli,openai,gpt
|
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Intended Audience :: Education
|
|
13
13
|
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
14
|
Classifier: Operating System :: OS Independent
|
|
16
15
|
Classifier: Programming Language :: Python :: 3
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -24,9 +23,9 @@ Description-Content-Type: text/markdown
|
|
|
24
23
|
License-File: LICENSE
|
|
25
24
|
Requires-Dist: click~=8.3
|
|
26
25
|
Requires-Dist: httpx>=0.28.1
|
|
27
|
-
Requires-Dist: openai<
|
|
26
|
+
Requires-Dist: openai<3.0,>=1.66.0
|
|
28
27
|
Requires-Dist: tiktoken~=0.12
|
|
29
|
-
Requires-Dist: urllib3>=2.
|
|
28
|
+
Requires-Dist: urllib3>=2.7.0
|
|
30
29
|
Dynamic: license-file
|
|
31
30
|
|
|
32
31
|
# VocabMaster
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: VocabMaster
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Master new languages with this CLI tool, designed to help you record vocabulary and create Anki flashcards without the need to manually input translations or example sentences.
|
|
5
5
|
Author: Sébastien De Revière
|
|
6
|
-
License: Apache-2.0
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
7
|
Project-URL: Homepage, https://github.com/sderev/vocabmaster
|
|
8
8
|
Project-URL: Repository, https://github.com/sderev/vocabmaster
|
|
9
9
|
Project-URL: Issues, https://github.com/sderev/vocabmaster/issues
|
|
@@ -11,7 +11,6 @@ Keywords: vocabulary,language-learning,anki,flashcards,cli,openai,gpt
|
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Intended Audience :: Education
|
|
13
13
|
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
14
|
Classifier: Operating System :: OS Independent
|
|
16
15
|
Classifier: Programming Language :: Python :: 3
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -24,9 +23,9 @@ Description-Content-Type: text/markdown
|
|
|
24
23
|
License-File: LICENSE
|
|
25
24
|
Requires-Dist: click~=8.3
|
|
26
25
|
Requires-Dist: httpx>=0.28.1
|
|
27
|
-
Requires-Dist: openai<
|
|
26
|
+
Requires-Dist: openai<3.0,>=1.66.0
|
|
28
27
|
Requires-Dist: tiktoken~=0.12
|
|
29
|
-
Requires-Dist: urllib3>=2.
|
|
28
|
+
Requires-Dist: urllib3>=2.7.0
|
|
30
29
|
Dynamic: license-file
|
|
31
30
|
|
|
32
31
|
# VocabMaster
|
|
@@ -11,6 +11,7 @@ tests/test_cli.py
|
|
|
11
11
|
tests/test_config_handler.py
|
|
12
12
|
tests/test_csv_handler.py
|
|
13
13
|
tests/test_gpt_integration.py
|
|
14
|
+
tests/test_gpt_integration_sdk_boundary.py
|
|
14
15
|
tests/test_path_traversal_prevention.py
|
|
15
16
|
tests/test_recovery.py
|
|
16
17
|
tests/test_same_language_integration.py
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "VocabMaster"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Master new languages with this CLI tool, designed to help you record vocabulary and create Anki flashcards without the need to manually input translations or example sentences."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
|
-
license =
|
|
11
|
+
license = "Apache-2.0"
|
|
12
12
|
authors = [
|
|
13
13
|
{name = "Sébastien De Revière"}
|
|
14
14
|
]
|
|
@@ -17,7 +17,6 @@ classifiers = [
|
|
|
17
17
|
"Development Status :: 4 - Beta",
|
|
18
18
|
"Intended Audience :: Education",
|
|
19
19
|
"Intended Audience :: End Users/Desktop",
|
|
20
|
-
"License :: OSI Approved :: Apache Software License",
|
|
21
20
|
"Operating System :: OS Independent",
|
|
22
21
|
"Programming Language :: Python :: 3",
|
|
23
22
|
"Programming Language :: Python :: 3.10",
|
|
@@ -30,9 +29,9 @@ classifiers = [
|
|
|
30
29
|
dependencies = [
|
|
31
30
|
"click ~= 8.3",
|
|
32
31
|
"httpx>=0.28.1",
|
|
33
|
-
"openai >= 1.66.0, <
|
|
32
|
+
"openai >= 1.66.0, <3.0",
|
|
34
33
|
"tiktoken ~= 0.12",
|
|
35
|
-
"urllib3 >= 2.
|
|
34
|
+
"urllib3 >= 2.7.0",
|
|
36
35
|
]
|
|
37
36
|
|
|
38
37
|
[project.scripts]
|
|
@@ -40,9 +39,9 @@ vocabmaster = "vocabmaster.cli:vocabmaster"
|
|
|
40
39
|
|
|
41
40
|
[dependency-groups]
|
|
42
41
|
dev = [
|
|
43
|
-
"pytest ~=
|
|
42
|
+
"pytest ~= 9.0.3",
|
|
44
43
|
"scriv[toml]>=1.5.0",
|
|
45
|
-
"urllib3>=2.
|
|
44
|
+
"urllib3>=2.7.0", # urllib3 is a transitive dependency of scriv (via requests).
|
|
46
45
|
]
|
|
47
46
|
|
|
48
47
|
[project.urls]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import pytest
|
|
5
|
+
from openai import RateLimitError
|
|
6
|
+
|
|
7
|
+
from vocabmaster import gpt_integration
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FakeStreamingResponse:
|
|
11
|
+
def __init__(self, events, final_response):
|
|
12
|
+
self._events = list(events)
|
|
13
|
+
self._final_response = final_response
|
|
14
|
+
|
|
15
|
+
def __iter__(self):
|
|
16
|
+
return iter(self._events)
|
|
17
|
+
|
|
18
|
+
def get_final_response(self):
|
|
19
|
+
return self._final_response
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FakeStreamingContext:
|
|
23
|
+
def __init__(self, stream_response=None, enter_error=None):
|
|
24
|
+
self._stream_response = stream_response
|
|
25
|
+
self._enter_error = enter_error
|
|
26
|
+
|
|
27
|
+
def __enter__(self):
|
|
28
|
+
if self._enter_error is not None:
|
|
29
|
+
raise self._enter_error
|
|
30
|
+
return self._stream_response
|
|
31
|
+
|
|
32
|
+
def __exit__(self, exc_type, exc, exc_tb):
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_chatgpt_request_non_streaming_uses_response_output_text():
|
|
37
|
+
create_calls = []
|
|
38
|
+
response = SimpleNamespace(output_text="bonjour\thello\texample")
|
|
39
|
+
|
|
40
|
+
class FakeResponses:
|
|
41
|
+
def create(self, **kwargs):
|
|
42
|
+
create_calls.append(kwargs)
|
|
43
|
+
return response
|
|
44
|
+
|
|
45
|
+
client = SimpleNamespace(responses=FakeResponses())
|
|
46
|
+
|
|
47
|
+
generated_text, response_time, raw_response = gpt_integration.chatgpt_request(
|
|
48
|
+
prompt=[{"role": "user", "content": "Translate bonjour"}],
|
|
49
|
+
stream=False,
|
|
50
|
+
client=client,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
assert generated_text == response.output_text
|
|
54
|
+
assert response_time >= 0
|
|
55
|
+
assert raw_response is response
|
|
56
|
+
assert create_calls == [
|
|
57
|
+
{
|
|
58
|
+
"input": [{"role": "user", "content": "Translate bonjour"}],
|
|
59
|
+
"model": gpt_integration.DEFAULT_MODEL,
|
|
60
|
+
"temperature": 0.7,
|
|
61
|
+
"stream": False,
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_chatgpt_request_streaming_uses_public_stream_api_and_filters_output(capsys):
|
|
67
|
+
stream_calls = []
|
|
68
|
+
final_response = SimpleNamespace(
|
|
69
|
+
output_text="bonjour\tbon jour\thello\texample\nsalut\tsalut\thi\texample two\n"
|
|
70
|
+
)
|
|
71
|
+
stream_response = FakeStreamingResponse(
|
|
72
|
+
events=[
|
|
73
|
+
SimpleNamespace(type="response.created"),
|
|
74
|
+
SimpleNamespace(type="response.output_text.delta", delta="bonjour\tbon jour\t"),
|
|
75
|
+
SimpleNamespace(type="response.output_text.delta", delta="hello\texample\n"),
|
|
76
|
+
SimpleNamespace(type="response.output_text.delta", delta="salut\tsalut\thi\t"),
|
|
77
|
+
SimpleNamespace(type="response.output_text.delta", delta="example two\n"),
|
|
78
|
+
],
|
|
79
|
+
final_response=final_response,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
class FakeResponses:
|
|
83
|
+
def stream(self, **kwargs):
|
|
84
|
+
stream_calls.append(kwargs)
|
|
85
|
+
return FakeStreamingContext(stream_response=stream_response)
|
|
86
|
+
|
|
87
|
+
client = SimpleNamespace(responses=FakeResponses())
|
|
88
|
+
|
|
89
|
+
generated_text, response_time, raw_response = gpt_integration.chatgpt_request(
|
|
90
|
+
prompt=[{"role": "user", "content": "Translate bonjour and salut"}],
|
|
91
|
+
stream=True,
|
|
92
|
+
client=client,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
captured = capsys.readouterr()
|
|
96
|
+
|
|
97
|
+
assert generated_text == final_response.output_text
|
|
98
|
+
assert response_time >= 0
|
|
99
|
+
assert raw_response is final_response
|
|
100
|
+
assert captured.out == "bonjour\thello\texample\nsalut\thi\texample two\n\n"
|
|
101
|
+
assert stream_calls == [
|
|
102
|
+
{
|
|
103
|
+
"input": [{"role": "user", "content": "Translate bonjour and salut"}],
|
|
104
|
+
"model": gpt_integration.DEFAULT_MODEL,
|
|
105
|
+
"temperature": 0.7,
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_chatgpt_request_streaming_falls_back_to_final_output_text(capsys):
|
|
111
|
+
final_response = SimpleNamespace(output_text="bonjour\tbon jour\thello\texample\n")
|
|
112
|
+
stream_response = FakeStreamingResponse(
|
|
113
|
+
events=[SimpleNamespace(type="response.completed")],
|
|
114
|
+
final_response=final_response,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
class FakeResponses:
|
|
118
|
+
def stream(self, **kwargs):
|
|
119
|
+
return FakeStreamingContext(stream_response=stream_response)
|
|
120
|
+
|
|
121
|
+
client = SimpleNamespace(responses=FakeResponses())
|
|
122
|
+
|
|
123
|
+
generated_text, _response_time, raw_response = gpt_integration.chatgpt_request(
|
|
124
|
+
prompt=[{"role": "user", "content": "Translate bonjour"}],
|
|
125
|
+
stream=True,
|
|
126
|
+
client=client,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
captured = capsys.readouterr()
|
|
130
|
+
|
|
131
|
+
assert generated_text == final_response.output_text
|
|
132
|
+
assert raw_response is final_response
|
|
133
|
+
assert captured.out == "bonjour\thello\texample\n\n"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_chatgpt_request_streaming_propagates_public_status_errors():
|
|
137
|
+
request = httpx.Request("POST", "https://api.openai.com/v1/responses")
|
|
138
|
+
response = httpx.Response(429, request=request)
|
|
139
|
+
rate_limit_error = RateLimitError("Too many requests", response=response, body=None)
|
|
140
|
+
|
|
141
|
+
class FakeResponses:
|
|
142
|
+
def stream(self, **kwargs):
|
|
143
|
+
return FakeStreamingContext(enter_error=rate_limit_error)
|
|
144
|
+
|
|
145
|
+
def forbidden_private_helper(_response):
|
|
146
|
+
raise AssertionError("private OpenAI helper should not be used")
|
|
147
|
+
|
|
148
|
+
client = SimpleNamespace(
|
|
149
|
+
responses=FakeResponses(),
|
|
150
|
+
_make_status_error_from_response=forbidden_private_helper,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
with pytest.raises(RateLimitError):
|
|
154
|
+
gpt_integration.chatgpt_request(
|
|
155
|
+
prompt=[{"role": "user", "content": "Translate bonjour"}],
|
|
156
|
+
stream=True,
|
|
157
|
+
client=client,
|
|
158
|
+
)
|
|
@@ -3,7 +3,6 @@ import json
|
|
|
3
3
|
import os
|
|
4
4
|
import time
|
|
5
5
|
|
|
6
|
-
import httpx
|
|
7
6
|
import tiktoken
|
|
8
7
|
from openai import OpenAI
|
|
9
8
|
|
|
@@ -167,51 +166,31 @@ def chatgpt_request(
|
|
|
167
166
|
stream_enabled = bool(stream)
|
|
168
167
|
|
|
169
168
|
if stream_enabled:
|
|
170
|
-
collected_chunks = []
|
|
171
169
|
collected_messages = []
|
|
172
170
|
streaming_state = {"line_buffer": ""}
|
|
171
|
+
final_response = None
|
|
173
172
|
|
|
174
|
-
with client.responses.
|
|
173
|
+
with client.responses.stream(
|
|
175
174
|
input=prompt,
|
|
176
175
|
model=model,
|
|
177
176
|
# max_output_tokens=max_tokens,
|
|
178
177
|
temperature=temperature,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
except httpx.HTTPStatusError as error:
|
|
184
|
-
if not error.response.is_closed:
|
|
185
|
-
error.response.read()
|
|
186
|
-
raise client._make_status_error_from_response(error.response) from None
|
|
187
|
-
for line in response.iter_lines():
|
|
188
|
-
if not line:
|
|
189
|
-
continue
|
|
190
|
-
if isinstance(line, bytes):
|
|
191
|
-
line = line.decode("utf-8", errors="replace")
|
|
192
|
-
if not line.startswith("data:"):
|
|
193
|
-
continue
|
|
194
|
-
payload = line[len("data:") :].strip()
|
|
195
|
-
if payload == "[DONE]":
|
|
196
|
-
break
|
|
197
|
-
try:
|
|
198
|
-
event = json.loads(payload)
|
|
199
|
-
except json.JSONDecodeError:
|
|
200
|
-
continue
|
|
201
|
-
collected_chunks.append(event)
|
|
202
|
-
event_type = event.get("type")
|
|
203
|
-
if event_type == "response.output_text.delta":
|
|
204
|
-
delta = event.get("delta", "")
|
|
178
|
+
) as stream_response:
|
|
179
|
+
for event in stream_response:
|
|
180
|
+
if getattr(event, "type", None) == "response.output_text.delta":
|
|
181
|
+
delta = getattr(event, "delta", "")
|
|
205
182
|
if delta:
|
|
206
183
|
collected_messages.append(delta)
|
|
207
184
|
filtered_content = filter_streaming_tsv(delta, streaming_state)
|
|
208
185
|
print(filtered_content, end="")
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
186
|
+
final_response = stream_response.get_final_response()
|
|
187
|
+
|
|
188
|
+
generated_text = "".join(collected_messages)
|
|
189
|
+
if not generated_text:
|
|
190
|
+
generated_text = final_response.output_text
|
|
191
|
+
if generated_text:
|
|
192
|
+
filtered_content = filter_streaming_tsv(generated_text, streaming_state)
|
|
193
|
+
print(filtered_content, end="")
|
|
215
194
|
tail = filter_streaming_tsv("", streaming_state, final=True)
|
|
216
195
|
if tail:
|
|
217
196
|
print(tail, end="")
|
|
@@ -219,8 +198,7 @@ def chatgpt_request(
|
|
|
219
198
|
|
|
220
199
|
# Save the time delay and text received
|
|
221
200
|
response_time = (time.monotonic_ns() - start_time) / 1e9
|
|
222
|
-
|
|
223
|
-
response = collected_chunks
|
|
201
|
+
response = final_response
|
|
224
202
|
|
|
225
203
|
else:
|
|
226
204
|
# Extract and save the generated response
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|