gramcheck 0.1.0__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.
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: gramcheck
3
+ Version: 0.1.0
4
+ Summary: Use LLMs to check grammar.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/degD/gramcheck
7
+ Project-URL: Issues, https://github.com/degD/gramcheck/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: colored>=2.3.2
14
+ Requires-Dist: python-dotenv>=1.0.1
15
+ Requires-Dist: google-genai>=2.8.0
16
+ Dynamic: license-file
17
+
18
+ # GRAMCHECK
19
+ This is a Python tool to check text for grammar mistakes using online LLMs. It uses [Google AI Studio](https://aistudio.google.com/) under the hood. You must provide your own API key. Because of the probabilistic nature of LLMs, the results may or may not be correct. Take them with a grain of salt. However, results are generated using a predefined seed to keep them consistent.
20
+
21
+ ## Install
22
+ 1. Install from PyPI: `pipx install gramcheck`.
23
+ 2. Set your API key: `gramcheck --set-api-key <YOUR_API_KEY_HERE>` (writes to `.env` in the project directory).
24
+
25
+ ## Usage
26
+ 1. Check a file: `gramcheck example.txt`.
27
+ 2. Check a single text: `gramcheck -t "Your text here"`.
28
+ 3. Check the Nth line in a file: `gramcheck example.txt -n 0`.
29
+ 4. Check a file as a whole: `gramcheck example.txt -a`.
30
+ 5. Show help: `gramcheck --help`.
31
+
32
+ ## Development
33
+ 1. Install the [`uv`](https://docs.astral.sh/uv/) project manager.
34
+ 2. Clone the project and run `uv sync`.
35
+ 3. Set your API key in `.env` as shown above.
36
+ 4. Run locally with `python gramcheck.py` or `./gramcheck`.
@@ -0,0 +1,7 @@
1
+ gramcheck.py,sha256=gNoOge1p0fM6X_Pa92dXIn5uzsZTZEgkKHnpNsYyUUg,6558
2
+ gramcheck-0.1.0.dist-info/licenses/LICENSE,sha256=LheyDQWAAO2gfWxIDAIIjJAbhkKDyX-ZMULGOfCtdwg,1061
3
+ gramcheck-0.1.0.dist-info/METADATA,sha256=02wA16zd-fF5CvfuEHBG_2frJBUOAE05KARhmUAVIxA,1592
4
+ gramcheck-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ gramcheck-0.1.0.dist-info/entry_points.txt,sha256=CHeTUGfxpfWclKLn_b7nvAga-usrmqB77SNCxnT2A3g,44
6
+ gramcheck-0.1.0.dist-info/top_level.txt,sha256=aIJdy0dEvH-EwoQMp_eNW8z-pjfUgb0Ovn_kydXiNtU,10
7
+ gramcheck-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gramcheck = gramcheck:cli
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 degD
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ gramcheck
gramcheck.py ADDED
@@ -0,0 +1,225 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+
5
+ import dotenv
6
+ from colored import Fore
7
+ from google import genai
8
+ from google.genai import types
9
+
10
+
11
+ def get_config_env_path() -> str:
12
+ project_root = os.path.dirname(os.path.abspath(__file__))
13
+ return os.path.join(project_root, ".env")
14
+
15
+
16
+ SEED = 4224442
17
+ API_KEY_PROVIDED = True
18
+ CONFIG_ENV_PATH = get_config_env_path()
19
+ dotenv.load_dotenv(CONFIG_ENV_PATH)
20
+ dotenv.load_dotenv()
21
+
22
+ try:
23
+ client = genai.Client()
24
+ except ValueError:
25
+ API_KEY_PROVIDED = False
26
+
27
+ file_read_error = "Unable to read from file. Please try again."
28
+
29
+ number_error = 'Text number "-n" is not an integer.'
30
+
31
+ number_range_error = "The number specified is not in the range of number of texts in file (counting starts from 0)."
32
+
33
+
34
+ def set_api_key(api_key: str) -> str:
35
+ env_path = CONFIG_ENV_PATH
36
+ os.makedirs(os.path.dirname(env_path), exist_ok=True)
37
+
38
+ lines = []
39
+ if os.path.exists(env_path):
40
+ with open(env_path) as fp:
41
+ lines = fp.read().splitlines()
42
+
43
+ updated = False
44
+ for index, line in enumerate(lines):
45
+ if line.startswith("GEMINI_API_KEY="):
46
+ lines[index] = f"GEMINI_API_KEY={api_key}"
47
+ updated = True
48
+ break
49
+ if not updated:
50
+ lines.append(f"GEMINI_API_KEY={api_key}")
51
+
52
+ with open(env_path, "w") as fp:
53
+ fp.write("\n".join(lines).rstrip("\n") + "\n")
54
+
55
+ return env_path
56
+
57
+
58
+ def build_arg_parser() -> argparse.ArgumentParser:
59
+ return argparse.ArgumentParser(
60
+ prog="gramcheck",
61
+ description=(
62
+ "Read lines from FILE and check them for grammatical errors using LLMs.\n"
63
+ 'Or grammar check TEXT with "-t" option.\n'
64
+ 'To check Nth text in FILE, use the "-n" option.\n'
65
+ 'To check FILE as a whole, use option "-a".'
66
+ ),
67
+ formatter_class=argparse.RawDescriptionHelpFormatter,
68
+ )
69
+
70
+
71
+ def parse_text_number(value: str) -> int:
72
+ try:
73
+ return int(value)
74
+ except ValueError as exc:
75
+ raise argparse.ArgumentTypeError(number_error) from exc
76
+
77
+
78
+ def text_grammar_check(text: str):
79
+ response = client.models.generate_content(
80
+ model="gemini-3.1-flash-lite",
81
+ contents=text,
82
+ config=types.GenerateContentConfig(
83
+ system_instruction="You are a tool made for language teaching. Check the text given by the user sentence by sentence for syntactic and semantic errors. Strictly avoid any greetings or filler.",
84
+ seed=SEED,
85
+ ),
86
+ )
87
+ return response.text
88
+
89
+
90
+ def flatten_list(l: list):
91
+ l_flat = []
92
+ count = 0
93
+ for l_inner in l:
94
+ if isinstance(l_inner, list):
95
+ count += 1
96
+ l_flat.extend(l_inner)
97
+ if count == 0:
98
+ return l
99
+ else:
100
+ return flatten_list(l_flat)
101
+
102
+
103
+ def parse_long_text(text: str):
104
+ word_count = len(text.split())
105
+ if word_count < 40_000:
106
+ return text
107
+ else:
108
+ text_first_half = " ".join(text.split()[: word_count // 2])
109
+ text_second_half = " ".join(text.split()[word_count // 2 :])
110
+ return flatten_list(
111
+ [parse_long_text(text_first_half), parse_long_text(text_second_half)]
112
+ )
113
+
114
+
115
+ def parse_file_text(file_text: str):
116
+ texts = []
117
+ for text in file_text.splitlines():
118
+ if text:
119
+ texts.append(parse_long_text(text))
120
+ return texts
121
+
122
+
123
+ def parse_only_file_text(file_text: str):
124
+ texts = []
125
+ for text in file_text.splitlines():
126
+ if text:
127
+ texts.append(text)
128
+ return texts
129
+
130
+
131
+ def read_from_file(path: str):
132
+ with open(path) as fp:
133
+ file_text = "".join(fp.readlines())
134
+ return file_text
135
+
136
+
137
+ def main(texts: list[str]):
138
+ responses = []
139
+ for text in texts:
140
+ responses.append(text_grammar_check(text))
141
+
142
+ for i in range(len(texts)):
143
+ print("\n" + "#" * os.get_terminal_size().columns + "\n")
144
+ print(f"{Fore.red}{texts[i]}\n\n{Fore.green}{responses[i]}{Fore.white}")
145
+ print("\n" + "#" * os.get_terminal_size().columns + "\n")
146
+
147
+
148
+ def cli():
149
+ parser = build_arg_parser()
150
+ parser.add_argument("file", nargs="?", help="File to read")
151
+ parser.add_argument("-t", "--text", help="Text to check")
152
+ parser.add_argument(
153
+ "-n",
154
+ "--number",
155
+ type=parse_text_number,
156
+ help="Check Nth text in FILE (0-based)",
157
+ )
158
+ parser.add_argument(
159
+ "-a",
160
+ "--all",
161
+ action="store_true",
162
+ help="Check FILE as a whole",
163
+ )
164
+ parser.add_argument(
165
+ "--set-api-key",
166
+ metavar="KEY",
167
+ help="Store GEMINI_API_KEY in the user config",
168
+ )
169
+
170
+ args = parser.parse_args()
171
+
172
+ if args.set_api_key:
173
+ if args.text or args.file or args.number is not None or args.all:
174
+ parser.error("--set-api-key cannot be combined with other options")
175
+ env_path = set_api_key(args.set_api_key)
176
+ print(f"Saved GEMINI_API_KEY to {env_path}")
177
+ sys.exit(0)
178
+
179
+ if API_KEY_PROVIDED:
180
+ if not args.text and not args.file:
181
+ parser.print_help()
182
+ sys.exit(0)
183
+ if args.text and args.file:
184
+ parser.error("FILE and -t/--text cannot be used together")
185
+ if args.text and args.number is not None:
186
+ parser.error("-n/--number cannot be used with -t/--text")
187
+ if args.number is not None and not args.file:
188
+ parser.error("-n/--number requires FILE")
189
+ if args.all and not args.file:
190
+ parser.error("-a/--all requires FILE")
191
+ if args.all and args.text:
192
+ parser.error("-a/--all cannot be used with -t/--text")
193
+ else:
194
+ parser.error(
195
+ "No API key was provided. Please pass a valid API key using --set-api-key"
196
+ )
197
+
198
+ if args.text:
199
+ texts = parse_file_text(args.text)
200
+ else:
201
+ try:
202
+ file_text = read_from_file(args.file)
203
+ except:
204
+ sys.stderr.write(file_read_error + "\n")
205
+ sys.exit(1)
206
+
207
+ if args.number is not None:
208
+ file_texts = parse_only_file_text(file_text)
209
+ if args.number < 0 or args.number >= len(file_texts):
210
+ sys.stderr.write(number_range_error + "\n")
211
+ sys.exit(3)
212
+ file_text = file_texts[args.number]
213
+ texts = parse_file_text(file_text)
214
+ elif args.all:
215
+ text_or_texts = parse_long_text(file_text)
216
+ texts = (
217
+ text_or_texts if isinstance(text_or_texts, list) else [text_or_texts]
218
+ )
219
+ else:
220
+ texts = parse_file_text(file_text)
221
+ main(texts)
222
+
223
+
224
+ if __name__ == "__main__":
225
+ cli()