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.
Files changed (27) hide show
  1. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/PKG-INFO +4 -5
  2. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/VocabMaster.egg-info/PKG-INFO +4 -5
  3. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/VocabMaster.egg-info/SOURCES.txt +1 -0
  4. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/VocabMaster.egg-info/requires.txt +2 -2
  5. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/pyproject.toml +6 -7
  6. vocabmaster-0.3.2/tests/test_gpt_integration_sdk_boundary.py +158 -0
  7. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/vocabmaster/gpt_integration.py +15 -37
  8. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/LICENSE +0 -0
  9. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/README.md +0 -0
  10. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/VocabMaster.egg-info/dependency_links.txt +0 -0
  11. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/VocabMaster.egg-info/entry_points.txt +0 -0
  12. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/VocabMaster.egg-info/top_level.txt +0 -0
  13. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/setup.cfg +0 -0
  14. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/tests/test_cli.py +0 -0
  15. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/tests/test_config_handler.py +0 -0
  16. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/tests/test_csv_handler.py +0 -0
  17. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/tests/test_gpt_integration.py +0 -0
  18. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/tests/test_path_traversal_prevention.py +0 -0
  19. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/tests/test_recovery.py +0 -0
  20. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/tests/test_same_language_integration.py +0 -0
  21. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/tests/test_utils.py +0 -0
  22. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/vocabmaster/__init__.py +0 -0
  23. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/vocabmaster/cli.py +0 -0
  24. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/vocabmaster/config_handler.py +0 -0
  25. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/vocabmaster/csv_handler.py +0 -0
  26. {vocabmaster-0.3.0 → vocabmaster-0.3.2}/vocabmaster/recovery.py +0 -0
  27. {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.0
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<2.0,>=1.66.0
26
+ Requires-Dist: openai<3.0,>=1.66.0
28
27
  Requires-Dist: tiktoken~=0.12
29
- Requires-Dist: urllib3>=2.6.3
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.0
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<2.0,>=1.66.0
26
+ Requires-Dist: openai<3.0,>=1.66.0
28
27
  Requires-Dist: tiktoken~=0.12
29
- Requires-Dist: urllib3>=2.6.3
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
@@ -1,5 +1,5 @@
1
1
  click~=8.3
2
2
  httpx>=0.28.1
3
- openai<2.0,>=1.66.0
3
+ openai<3.0,>=1.66.0
4
4
  tiktoken~=0.12
5
- urllib3>=2.6.3
5
+ urllib3>=2.7.0
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "VocabMaster"
7
- version = "0.3.0"
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 = {text = "Apache-2.0"}
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, <2.0",
32
+ "openai >= 1.66.0, <3.0",
34
33
  "tiktoken ~= 0.12",
35
- "urllib3 >= 2.6.3",
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 ~= 8.4",
42
+ "pytest ~= 9.0.3",
44
43
  "scriv[toml]>=1.5.0",
45
- "urllib3>=2.6.3", # urllib3 is a transitive dependency of scriv (via requests). Pinned to fix CVE-2026-21441.
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.with_streaming_response.create(
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
- stream=True,
180
- ) as response:
181
- try:
182
- response.http_response.raise_for_status()
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
- elif event_type == "response.output_text.done" and not collected_messages:
210
- text = event.get("text", "")
211
- if text:
212
- collected_messages.append(text)
213
- filtered_content = filter_streaming_tsv(text, streaming_state)
214
- print(filtered_content, end="")
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
- generated_text = "".join(collected_messages)
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