thuk 1.0.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.
thuk/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """Thuk: offline study documents with contents and keyword search."""
2
+ from .reference import SECTIONS, get_text, search, list_topics, get_topic, search_topics
3
+ __version__ = '1.0.1'
4
+ __all__ = ['SECTIONS', 'get_text', 'search', 'list_topics', 'get_topic', 'search_topics']
thuk/__main__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .cli import main
2
+ raise SystemExit(main())
thuk/cli.py ADDED
@@ -0,0 +1,152 @@
1
+ """Nested document browser and keyword search."""
2
+ import argparse
3
+ import pydoc
4
+ import re
5
+ import subprocess
6
+ import sys
7
+ from . import __version__
8
+ from .reference import SECTIONS, get_text, get_topic, list_topics, search_topics, export_pdf, open_pdf
9
+
10
+
11
+ def highlight(text, keyword, color=False):
12
+ if not keyword:
13
+ return text
14
+ pattern = re.compile(re.escape(keyword), re.IGNORECASE)
15
+ return pattern.sub(lambda m: ('\033[1;33m' + m[0] + '\033[0m') if color else '[[' + m[0] + ']]', text)
16
+
17
+
18
+ def display(section, topic=None, plain=False, keyword=None):
19
+ title = SECTIONS[section][0]
20
+ body = get_text(section) if topic is None else get_topic(section, topic)
21
+ if topic is not None:
22
+ title += ' / ' + list_topics(section)[topic - 1]['title']
23
+ # Markers remain visible even in Windows pagers that strip ANSI escapes.
24
+ text = title + '\n' + '=' * 60 + '\n\n' + highlight(body, keyword)
25
+ if keyword:
26
+ text = f'Search: {keyword} (matches marked with [[...]]).\n\n' + text
27
+ if plain or not sys.stdout.isatty():
28
+ print(text)
29
+ else:
30
+ pydoc.pager(text)
31
+
32
+
33
+ def ask(prompt):
34
+ return input(prompt).strip()
35
+
36
+
37
+ def search_menu(section, plain=False, query=None):
38
+ if query is None:
39
+ query = ask('Keyword (blank to cancel): ')
40
+ if not query.strip():
41
+ return
42
+ results = search_topics(query, section)
43
+ if not results:
44
+ print('No matches found in this document.')
45
+ return
46
+ while True:
47
+ print(f'\nSearch results in {SECTIONS[section][0]} for "{query}"')
48
+ for i, result in enumerate(results, 1):
49
+ print(f"{i}. {result['title']} ({len(result['matches'])} matching lines)")
50
+ for line, snippet in result['matches'][:2]:
51
+ print(f' Line {line}: {highlight(snippet, query)}')
52
+ print('0. Back to document contents')
53
+ choice = ask('Open result: ')
54
+ if choice == '0':
55
+ return
56
+ if choice.isdigit() and 1 <= int(choice) <= len(results):
57
+ display(section, results[int(choice) - 1]['topic'], plain, query)
58
+ return
59
+ else:
60
+ print('Choose a result number or 0.')
61
+
62
+
63
+ def document_menu(section, plain=False):
64
+ topics = list_topics(section)
65
+ while True:
66
+ print('\n' + SECTIONS[section][0] + ' - Contents\n' + '=' * 60)
67
+ for number, item in enumerate(topics, 1):
68
+ indent = ' ' if item['level'] == 2 else ''
69
+ print(f"{number}. {indent}{item['title']}")
70
+ print('S. Search this document')
71
+ print('0. Back to documents')
72
+ choice = ask('Select a section or S to search: ').lower()
73
+ if choice == '0':
74
+ return
75
+ if choice == 's':
76
+ search_menu(section, plain)
77
+ elif choice.isdigit() and 1 <= int(choice) <= len(topics):
78
+ display(section, int(choice), plain)
79
+ else:
80
+ print('Choose a section number, S, or 0.')
81
+
82
+
83
+ def menu(plain=False, pdf=False):
84
+ while True:
85
+ print('\nTHUK\n' + '=' * 42)
86
+ for number, (title, _) in SECTIONS.items():
87
+ print(f'{number}. {title}')
88
+ print('0. Exit\n' + '=' * 42)
89
+ choice = ask('Select a document (1-5): ')
90
+ if choice == '0':
91
+ return 0
92
+ if choice not in {str(n) for n in SECTIONS}:
93
+ print('Choose a document from 1 to 5, or 0 to exit.')
94
+ continue
95
+ if pdf:
96
+ try:
97
+ print(f'Opened: {open_pdf(int(choice))}')
98
+ except (ValueError, OSError, subprocess.CalledProcessError) as exc:
99
+ print(f'Cannot open PDF: {exc}')
100
+ else:
101
+ document_menu(int(choice), plain)
102
+
103
+
104
+ def main(argv=None):
105
+ parser = argparse.ArgumentParser(description='Thuk offline document browser')
106
+ parser.add_argument('section', nargs='?', type=int, choices=SECTIONS, help='document number (1-5)')
107
+ parser.add_argument('--topic', type=int, help='read one topic by its contents-menu number')
108
+ parser.add_argument('--version', action='version', version=f'Thuk {__version__}')
109
+ group = parser.add_mutually_exclusive_group()
110
+ group.add_argument('--pdf', action='store_true', help='open the original PDF')
111
+ group.add_argument('--export-pdf', metavar='PATH', help='save the original PDF')
112
+ group.add_argument('--search', metavar='TEXT', help='search the selected document, or all documents')
113
+ group.add_argument('--full', action='store_true', help='print the complete document')
114
+ parser.add_argument('--plain', action='store_true', help='disable the interactive pager')
115
+ args = parser.parse_args(argv)
116
+ if (args.topic is not None or args.export_pdf or args.full) and args.section is None:
117
+ parser.error('--topic, --export-pdf and --full require a document number')
118
+ if args.topic is not None and (args.pdf or args.export_pdf or args.full or args.search is not None):
119
+ parser.error('--topic cannot be combined with PDF, full-document or search options')
120
+ try:
121
+ if args.search is not None:
122
+ if not args.search.strip():
123
+ raise ValueError('Enter a non-empty search query.')
124
+ if args.section is not None and sys.stdin.isatty():
125
+ search_menu(args.section, args.plain, args.search)
126
+ document_menu(args.section, args.plain)
127
+ else:
128
+ found = False
129
+ for section in ([args.section] if args.section else SECTIONS):
130
+ for result in search_topics(args.search, section):
131
+ found = True
132
+ print(f"{SECTIONS[section][0]} / {result['title']} (topic {result['topic']})")
133
+ for line, snippet in result['matches']:
134
+ print(f' {line}: {highlight(snippet, args.search)}')
135
+ if not found:
136
+ print('No matches found.')
137
+ elif args.section is None:
138
+ return menu(args.plain, args.pdf)
139
+ elif args.export_pdf:
140
+ print(export_pdf(args.section, args.export_pdf))
141
+ elif args.pdf:
142
+ print(f'Opened: {open_pdf(args.section)}')
143
+ elif args.topic is not None or args.full:
144
+ display(args.section, args.topic, args.plain)
145
+ else:
146
+ document_menu(args.section, args.plain)
147
+ return 0
148
+ except (EOFError, KeyboardInterrupt, BrokenPipeError):
149
+ return 0
150
+ except (ValueError, OSError, subprocess.CalledProcessError) as exc:
151
+ print(f'Thuk: {exc}', file=sys.stderr)
152
+ return 1
@@ -0,0 +1,330 @@
1
+ {
2
+ "1": [
3
+ {
4
+ "title": "Document introduction",
5
+ "start": 0,
6
+ "end": 4,
7
+ "level": 1
8
+ },
9
+ {
10
+ "title": "1. Course Orientation",
11
+ "start": 4,
12
+ "end": 30,
13
+ "level": 1
14
+ },
15
+ {
16
+ "title": "2. Python Core",
17
+ "start": 30,
18
+ "end": 246,
19
+ "level": 1
20
+ },
21
+ {
22
+ "title": "3. Networking and Files",
23
+ "start": 246,
24
+ "end": 423,
25
+ "level": 1
26
+ },
27
+ {
28
+ "title": "4. Django MVT",
29
+ "start": 423,
30
+ "end": 685,
31
+ "level": 1
32
+ },
33
+ {
34
+ "title": "5. Auth and Sessions",
35
+ "start": 685,
36
+ "end": 773,
37
+ "level": 1
38
+ },
39
+ {
40
+ "title": "6. Database Connectivity",
41
+ "start": 773,
42
+ "end": 880,
43
+ "level": 1
44
+ },
45
+ {
46
+ "title": "7. APIs with Django REST Framework",
47
+ "start": 880,
48
+ "end": 1011,
49
+ "level": 1
50
+ },
51
+ {
52
+ "title": "8. Machine Learning Model Integration",
53
+ "start": 1011,
54
+ "end": 1120,
55
+ "level": 1
56
+ },
57
+ {
58
+ "title": "9. Quick Recap",
59
+ "start": 1120,
60
+ "end": 1161,
61
+ "level": 1
62
+ }
63
+ ],
64
+ "2": [
65
+ {
66
+ "title": "Document introduction",
67
+ "start": 0,
68
+ "end": 25,
69
+ "level": 1
70
+ },
71
+ {
72
+ "title": "1. Course Orientation",
73
+ "start": 25,
74
+ "end": 40,
75
+ "level": 1
76
+ },
77
+ {
78
+ "title": "2. Why Docker Exists",
79
+ "start": 40,
80
+ "end": 120,
81
+ "level": 1
82
+ },
83
+ {
84
+ "title": "3. Containers vs Virtual Machines",
85
+ "start": 120,
86
+ "end": 161,
87
+ "level": 1
88
+ },
89
+ {
90
+ "title": "4. Docker Architecture",
91
+ "start": 161,
92
+ "end": 211,
93
+ "level": 1
94
+ },
95
+ {
96
+ "title": "5. Images and the Dockerfile",
97
+ "start": 211,
98
+ "end": 308,
99
+ "level": 1
100
+ },
101
+ {
102
+ "title": "6. Containers, Storage and Networking",
103
+ "start": 308,
104
+ "end": 422,
105
+ "level": 1
106
+ },
107
+ {
108
+ "title": "7. Docker Compose",
109
+ "start": 422,
110
+ "end": 468,
111
+ "level": 1
112
+ },
113
+ {
114
+ "title": "8. Integrating Django with Docker",
115
+ "start": 468,
116
+ "end": 500,
117
+ "level": 1
118
+ },
119
+ {
120
+ "title": "9. Deployment and Security",
121
+ "start": 500,
122
+ "end": 595,
123
+ "level": 1
124
+ },
125
+ {
126
+ "title": "10. Best Practice and Debugging",
127
+ "start": 595,
128
+ "end": 661,
129
+ "level": 1
130
+ },
131
+ {
132
+ "title": "11. Quick Recap",
133
+ "start": 661,
134
+ "end": 691,
135
+ "level": 1
136
+ }
137
+ ],
138
+ "3": [
139
+ {
140
+ "title": "Document introduction",
141
+ "start": 0,
142
+ "end": 7,
143
+ "level": 1
144
+ },
145
+ {
146
+ "title": "1. Python Core",
147
+ "start": 7,
148
+ "end": 127,
149
+ "level": 1
150
+ },
151
+ {
152
+ "title": "2. Networking and Files",
153
+ "start": 127,
154
+ "end": 237,
155
+ "level": 1
156
+ },
157
+ {
158
+ "title": "3. Django Project Setup",
159
+ "start": 237,
160
+ "end": 264,
161
+ "level": 1
162
+ },
163
+ {
164
+ "title": "4. Django MVT",
165
+ "start": 264,
166
+ "end": 414,
167
+ "level": 1
168
+ },
169
+ {
170
+ "title": "5. Auth and Sessions",
171
+ "start": 414,
172
+ "end": 516,
173
+ "level": 1
174
+ },
175
+ {
176
+ "title": "6. Database Connectivity",
177
+ "start": 516,
178
+ "end": 589,
179
+ "level": 1
180
+ },
181
+ {
182
+ "title": "7. Django REST Framework",
183
+ "start": 589,
184
+ "end": 687,
185
+ "level": 1
186
+ },
187
+ {
188
+ "title": "8. Machine Learning Model Integration",
189
+ "start": 687,
190
+ "end": 782,
191
+ "level": 1
192
+ },
193
+ {
194
+ "title": "9. Common Errors and Fixes",
195
+ "start": 782,
196
+ "end": 828,
197
+ "level": 1
198
+ },
199
+ {
200
+ "title": "10. Command Sheet",
201
+ "start": 828,
202
+ "end": 847,
203
+ "level": 1
204
+ }
205
+ ],
206
+ "4": [
207
+ {
208
+ "title": "Document introduction",
209
+ "start": 0,
210
+ "end": 36,
211
+ "level": 1
212
+ },
213
+ {
214
+ "title": "1. Setup",
215
+ "start": 36,
216
+ "end": 65,
217
+ "level": 1
218
+ },
219
+ {
220
+ "title": "2. Images",
221
+ "start": 65,
222
+ "end": 107,
223
+ "level": 1
224
+ },
225
+ {
226
+ "title": "3. Containers",
227
+ "start": 107,
228
+ "end": 167,
229
+ "level": 1
230
+ },
231
+ {
232
+ "title": "4. The Dockerfile",
233
+ "start": 167,
234
+ "end": 293,
235
+ "level": 1
236
+ },
237
+ {
238
+ "title": "5. Preparing Django for Docker",
239
+ "start": 293,
240
+ "end": 350,
241
+ "level": 1
242
+ },
243
+ {
244
+ "title": "6. Volumes and Networks",
245
+ "start": 350,
246
+ "end": 394,
247
+ "level": 1
248
+ },
249
+ {
250
+ "title": "7. Docker Compose",
251
+ "start": 394,
252
+ "end": 529,
253
+ "level": 1
254
+ },
255
+ {
256
+ "title": "8. Registries and Deployment",
257
+ "start": 529,
258
+ "end": 596,
259
+ "level": 1
260
+ },
261
+ {
262
+ "title": "9. Debugging and Clean Up",
263
+ "start": 596,
264
+ "end": 660,
265
+ "level": 1
266
+ }
267
+ ],
268
+ "5": [
269
+ {
270
+ "title": "Document introduction",
271
+ "start": 0,
272
+ "end": 6,
273
+ "level": 1
274
+ },
275
+ {
276
+ "title": "SETUP",
277
+ "start": 6,
278
+ "end": 11,
279
+ "level": 1
280
+ },
281
+ {
282
+ "title": "START A REPOSITORY",
283
+ "start": 11,
284
+ "end": 15,
285
+ "level": 1
286
+ },
287
+ {
288
+ "title": "DAILY WORK",
289
+ "start": 15,
290
+ "end": 25,
291
+ "level": 1
292
+ },
293
+ {
294
+ "title": "BRANCHES",
295
+ "start": 25,
296
+ "end": 32,
297
+ "level": 1
298
+ },
299
+ {
300
+ "title": "REMOTES",
301
+ "start": 32,
302
+ "end": 40,
303
+ "level": 1
304
+ },
305
+ {
306
+ "title": "TEMPORARY WORK",
307
+ "start": 40,
308
+ "end": 47,
309
+ "level": 1
310
+ },
311
+ {
312
+ "title": "UNDO",
313
+ "start": 47,
314
+ "end": 53,
315
+ "level": 1
316
+ },
317
+ {
318
+ "title": "CONFLICTS",
319
+ "start": 53,
320
+ "end": 60,
321
+ "level": 1
322
+ },
323
+ {
324
+ "title": "TAGS AND IGNORE RULES",
325
+ "start": 60,
326
+ "end": 65,
327
+ "level": 1
328
+ }
329
+ ]
330
+ }
Binary file