vondb 3.0.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.
von/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ __author__ = "Evan Chen"
2
+ __version__ = "1.0.0"
3
+ __date__ = "August 3, 2016"
von/__main__.py ADDED
@@ -0,0 +1,15 @@
1
+ import sys
2
+
3
+ from . import term
4
+
5
+
6
+ def main():
7
+ cmd = term.VonTerminal()
8
+ if len(sys.argv) == 1:
9
+ cmd.run()
10
+ else:
11
+ cmd.direct(sys.argv[1:]) # to preserve command line quotes etc
12
+
13
+
14
+ if __name__ == "__main__":
15
+ main()
von/api.py ADDED
@@ -0,0 +1,67 @@
1
+ # For importing von externally
2
+ import sys
3
+
4
+ from . import model
5
+ from .puid import inferPUID
6
+
7
+ sys.modules["model"] = model
8
+
9
+ index = model.VonIndex().store # get the underlying dict
10
+
11
+ source_to_puid_lookup = {inferPUID(source): source for source in index}
12
+
13
+
14
+ def has(source: str) -> bool:
15
+ """Checks whether a given source exists in database"""
16
+ return source in index
17
+
18
+
19
+ def has_solution(source: str) -> bool:
20
+ """Checks whether a given source exists in database AND has a solution"""
21
+ if not has(source):
22
+ return False
23
+ entry = index[source]
24
+ return len(entry.full.bodies) > 1
25
+
26
+
27
+ def get_index(source: str, brave=False) -> model.PickleMappingEntry | None:
28
+ """Returns the index entry for a given source, or None"""
29
+ entry = index.get(source)
30
+
31
+ if entry is None:
32
+ puid = source.upper()
33
+ for index_source, index_entry in index.items():
34
+ if puid == inferPUID(index_source):
35
+ entry = index_entry
36
+ break
37
+
38
+ if entry is not None and entry.secret:
39
+ assert brave, f"{entry} requires security clearance"
40
+ return entry
41
+
42
+
43
+ def get(source: str, brave=False) -> model.Problem:
44
+ """Returns the full data for a given source"""
45
+ entry = get_index(source, brave)
46
+ assert entry is not None, f"{source} is None"
47
+ return entry.full
48
+
49
+
50
+ def get_statement(source: str, brave=False) -> str:
51
+ """Returns just the problem statement for a given source"""
52
+ return get(source, brave).bodies[0]
53
+
54
+
55
+ def get_solution(source: str, brave=False) -> str:
56
+ """Returns just the solution for a given source (asserts existence)"""
57
+ bodies = get(source, brave).bodies
58
+ assert len(bodies) > 1, f"{source} has no solution"
59
+ return bodies[1]
60
+
61
+
62
+ def get_puid(source: str) -> str:
63
+ return inferPUID(source)
64
+
65
+
66
+ def get_source(puid: str) -> str | None:
67
+ return source_to_puid_lookup.get(puid)
von/clipboard.py ADDED
@@ -0,0 +1,21 @@
1
+ try:
2
+ import pyperclip
3
+ except ModuleNotFoundError:
4
+ PYPERCLIP_AVAILABLE = False
5
+ pyperclip = None
6
+ else:
7
+ PYPERCLIP_AVAILABLE = True
8
+
9
+
10
+ def get_clipboard() -> str:
11
+ if PYPERCLIP_AVAILABLE:
12
+ assert pyperclip is not None
13
+ return pyperclip.paste().strip()
14
+ else:
15
+ return ""
16
+
17
+
18
+ def set_clipboard(s: str):
19
+ if PYPERCLIP_AVAILABLE:
20
+ assert pyperclip is not None
21
+ pyperclip.copy(s)
@@ -0,0 +1,46 @@
1
+ from . import ( # NOQA
2
+ add,
3
+ asy,
4
+ br,
5
+ cd,
6
+ clear,
7
+ edit,
8
+ index,
9
+ link,
10
+ markup,
11
+ nuke,
12
+ paths,
13
+ po,
14
+ search,
15
+ show,
16
+ solve,
17
+ status,
18
+ )
19
+
20
+
21
+ class VonController:
22
+ def do_ls(self, argv: list[str]):
23
+ self.do_cd(["."] + argv)
24
+
25
+ do_add = add.main
26
+ do_asy = asy.main
27
+ do_br = br.main
28
+ do_cd = cd.main
29
+ do_clear = clear.main
30
+ do_cs = do_cd
31
+ do_edit = edit.main
32
+ do_f = show.main # alias find
33
+ do_link = link.main
34
+ do_m = markup.main
35
+ do_markup = markup.main
36
+ do_index = index.main
37
+ do_nuke = nuke.main
38
+ do_paths = paths.main
39
+ do_po = po.main
40
+ do_s = search.main
41
+ do_search = search.main
42
+ do_show = show.main
43
+ do_solve = solve.main
44
+ do_solve = solve.main
45
+ do_ss = status.main
46
+ do_status = status.main
von/controller/add.py ADDED
@@ -0,0 +1,224 @@
1
+ import datetime
2
+ import logging
3
+ import os
4
+ import re
5
+ import subprocess
6
+ import tempfile
7
+ import traceback
8
+ from argparse import Namespace
9
+ from typing import Any, Callable
10
+
11
+ import yaml
12
+ from yaml.scanner import ScannerError
13
+
14
+ from .. import model, view
15
+ from ..clipboard import get_clipboard
16
+ from ..puid import inferPUID
17
+ from ..rc import EDITOR, NSEPARATOR, SEPARATOR, TAG_HINT_TEXT
18
+ from . import preview
19
+
20
+ # https://urlregex.com/
21
+ RE_URL = re.compile(
22
+ r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"
23
+ )
24
+
25
+
26
+ def user_file_input(
27
+ initial="",
28
+ extension=".tmp",
29
+ pre_hook: Callable[[str], None] = None,
30
+ delete: bool = False,
31
+ ):
32
+ """Opens in $EDITOR a file with content 'initial'
33
+ and 'extension', and returns edited file.
34
+ If pre_hook is not None, runs pre_hook(tf.name) before opening EDITOR.
35
+ If delete is True, delete the file afterwards.
36
+ """
37
+
38
+ with tempfile.NamedTemporaryFile(suffix=extension, delete=False) as tf:
39
+ tf.write(initial.encode())
40
+ filename = tf.name
41
+ if pre_hook is not None:
42
+ pre_hook(filename)
43
+ subprocess.run([EDITOR, filename])
44
+
45
+ with open(filename, "r") as tf:
46
+ edited_message = "".join(_ for _ in tf.readlines())
47
+ if delete:
48
+ os.unlink(filename)
49
+ return edited_message
50
+
51
+
52
+ def alert_error_tryagain(message=""):
53
+ """Prints an error message and waits for user to confirm."""
54
+ logging.error(message)
55
+ return input("** Press enter to continue: ")
56
+
57
+
58
+ PS_INSTRUCT = r"""% Input your problem and solution below.
59
+ % Three dashes on a newline indicate the breaking points.
60
+ % URL detected: {url}
61
+ % TEX root = {preview_path}
62
+ """
63
+
64
+
65
+ def solicit_user_for_content(raw_text: str, url: str, opts: Namespace):
66
+ del opts
67
+ initial = (
68
+ PS_INSTRUCT.format(
69
+ url=url if url != "<++>" else "None",
70
+ preview_path=preview.VON_PREVIEW_PATH,
71
+ )
72
+ + NSEPARATOR
73
+ + raw_text
74
+ )
75
+
76
+ def pre_hook(tempfile_name: str):
77
+ preview.make_preview(tempfile_name)
78
+
79
+ while True:
80
+ # TODO maybe give user instructions
81
+ raw_ps = user_file_input(
82
+ initial=initial, extension="von.tex", pre_hook=pre_hook
83
+ )
84
+
85
+ if raw_ps.count(SEPARATOR) >= 1:
86
+ bodies = [_.strip() for _ in raw_ps.split(SEPARATOR)[1:]]
87
+ if bodies[0] == "":
88
+ return None
89
+ return bodies
90
+ elif raw_ps.strip() == "":
91
+ return None
92
+ else:
93
+ alert_error_tryagain("Bad format: can't find separator. Try again.")
94
+ initial = raw_ps
95
+
96
+
97
+ DEFAULT_PATH = model.getcwd()
98
+ YAML_DATA_FILE = """# Input your problem metadata here
99
+
100
+ source: {source} # e.g. USAMO 2000/6. This must be unique
101
+ desc: <++> # e.g. Fiendish inequality
102
+ path: {path}<++>
103
+ tags: [{now.year}-{now.month:02d}, <++>]
104
+ url: {url}
105
+
106
+ {hint}"""
107
+
108
+
109
+ def solicit_user_for_yaml(opts: Namespace, url: str) -> None | tuple[str, Any]:
110
+ if clipboard_text := get_clipboard():
111
+ if RE_URL.fullmatch(clipboard_text) is not None:
112
+ url = clipboard_text
113
+
114
+ initial = YAML_DATA_FILE.format(
115
+ path=model.completePath(DEFAULT_PATH),
116
+ now=datetime.datetime.now(),
117
+ source="<++>" if opts.source is None else opts.source,
118
+ hint=TAG_HINT_TEXT,
119
+ url=url,
120
+ )
121
+ while True:
122
+ raw_yaml = user_file_input(initial=initial, extension="von.yaml", delete=True)
123
+ try:
124
+ d = yaml.safe_load(raw_yaml)
125
+ if d is None:
126
+ return None
127
+ assert "path" in d, "Path is mandatory"
128
+ assert "source" in d, "Source is mandatory"
129
+ if d["path"][-1] != "/":
130
+ d["path"] += "/"
131
+ assert os.path.isdir(d["path"]), d["path"] + " directory non-existent"
132
+ target = d["path"] + inferPUID(d["source"]) + ".tex"
133
+ assert not os.path.isfile(target), target + " already taken"
134
+ assert model.getEntryByKey(d["source"]) is None, (
135
+ d["source"] + " is already an existing problem source"
136
+ )
137
+ except AssertionError:
138
+ traceback.print_exc()
139
+ alert_error_tryagain("Assertions failed, please try again.")
140
+ initial = raw_yaml
141
+ except ScannerError:
142
+ traceback.print_exc()
143
+ alert_error_tryagain("Could not parse YAML, please try again.")
144
+ initial = raw_yaml
145
+ else:
146
+ del d["path"]
147
+ # darn PyYAML used to do this fine -_-
148
+ tags = d.pop("tags")
149
+ output = (
150
+ yaml.dump(d, default_flow_style=False).strip()
151
+ + "\n"
152
+ + "tags: ["
153
+ + ", ".join(tags)
154
+ + "]"
155
+ )
156
+ return (target, output)
157
+
158
+
159
+ def do_add_problem(raw_text: str, url: str, opts: Namespace):
160
+ """Core procedure. Opens two instances of editors to solicit user input
161
+ on problem and produce a problem instance."""
162
+
163
+ # Get problem and solution
164
+ bodies = solicit_user_for_content(raw_text, url, opts)
165
+ if bodies is None:
166
+ logging.warning("Aborting due to empty input...")
167
+ return
168
+
169
+ yaml_info = solicit_user_for_yaml(opts, url)
170
+ if yaml_info is None:
171
+ logging.warning("Aborting due to empty input...")
172
+ return
173
+
174
+ target, out_yaml = yaml_info
175
+ out_text = NSEPARATOR.join([out_yaml] + bodies)
176
+ p = model.addProblemByFileContents(target, out_text)
177
+ assert p is not None
178
+ model.augmentCache(p.entry)
179
+ view.printEntry(p.entry)
180
+
181
+
182
+ parser = view.Parser(prog="add", description="Adds a problem to VON.")
183
+ parser.add_argument(
184
+ "source",
185
+ default=None,
186
+ nargs="?",
187
+ help="If specified, sets the source for the new problem.",
188
+ )
189
+ parser.add_argument(
190
+ "-f",
191
+ "--file",
192
+ dest="filename",
193
+ default=None,
194
+ help="If specified, uses contents of file as body",
195
+ )
196
+ parser.add_argument(
197
+ "-u",
198
+ "--url",
199
+ default=None,
200
+ help="Specify a URL on the command line",
201
+ )
202
+
203
+
204
+ def main(self: object, argv: list[str]):
205
+ del self
206
+ opts = parser.process(argv)
207
+ opts.verbose = True
208
+ url = opts.url if opts.url is not None else "<++>"
209
+ if opts.filename is not None:
210
+ if not os.path.isfile(opts.filename):
211
+ logging.error("The file " + opts.filename + " doesn't exist")
212
+ return
213
+ with open(opts.filename) as f:
214
+ initial_text = "".join(f.readlines())
215
+ else:
216
+ if clipboard_text := get_clipboard():
217
+ if RE_URL.fullmatch(clipboard_text) is not None and opts.url is not None:
218
+ initial_text = "<++>"
219
+ url = clipboard_text
220
+ else:
221
+ initial_text = clipboard_text
222
+ else:
223
+ initial_text = "<++>"
224
+ do_add_problem(initial_text, url, opts)
von/controller/asy.py ADDED
@@ -0,0 +1,46 @@
1
+ import logging
2
+ import re
3
+
4
+ from .. import model, view
5
+
6
+ parser = view.Parser(
7
+ prog="asy", description="Prints the diagram from the solution to a problem"
8
+ )
9
+ parser.add_argument("key", help="The key of the problem to open")
10
+ parser.add_argument(
11
+ "-n",
12
+ "--number",
13
+ default=1,
14
+ type=int,
15
+ help="Takes the n-th diagram, one-indexed. Defaults to 1.",
16
+ )
17
+ parser.add_argument(
18
+ "-b",
19
+ "--body",
20
+ default=1,
21
+ type=int,
22
+ help="Takes the diagram from the b'th body. Defaults to 1.",
23
+ )
24
+ parser.add_argument(
25
+ "-c",
26
+ "--comments",
27
+ action="store_true",
28
+ help="Decides whether or not commented lines should be displayed",
29
+ )
30
+
31
+
32
+ def main(self: object, argv: list[str]):
33
+ opts = parser.process(argv)
34
+ entry = model.getEntryByKey(opts.key)
35
+ if entry is None:
36
+ logging.error(opts.key + " not found")
37
+ else:
38
+ problem = entry.full
39
+ soln = problem.bodies[opts.body]
40
+ asys = re.findall(r"\\begin\{asy\}(.+?)\\end\{asy\}", soln, flags=re.DOTALL)
41
+ assert opts.number >= 1
42
+ diagram = asys[opts.number - 1]
43
+
44
+ if not opts.comments:
45
+ diagram = re.sub(r"/\*.*?\*/", "", diagram, flags=re.DOTALL)
46
+ print("\n".join(line.strip() for line in diagram.strip().split("\n")))
von/controller/br.py ADDED
@@ -0,0 +1,22 @@
1
+ import logging
2
+ import sys
3
+ import webbrowser
4
+
5
+ from .. import model, view
6
+
7
+ parser = view.Parser(prog="br", description="Browse a problem on AoPS")
8
+ parser.add_argument("key", help="The key of the problem to open")
9
+
10
+
11
+ def main(self: object, argv: list[str]):
12
+ opts = parser.process(argv)
13
+ entry = model.getEntryByKey(opts.key)
14
+ if entry is None:
15
+ logging.error(opts.key + " not found")
16
+ else:
17
+ url = entry.url
18
+ if url is None:
19
+ print("No URL is provided for this problem")
20
+ sys.exit(1)
21
+ print(url)
22
+ webbrowser.open(url)
von/controller/cd.py ADDED
@@ -0,0 +1,18 @@
1
+ import os
2
+
3
+ from .. import model, view
4
+
5
+ parser = view.Parser(prog="cd", description="Changes the working directory.")
6
+ parser.add_argument("path", help="The path to change to")
7
+
8
+
9
+ def main(self: object, argv: list[str]):
10
+ opts = parser.process(argv)
11
+ os.chdir(opts.path)
12
+
13
+ if not opts.quiet:
14
+ entries, dirs = model.viewDirectory(model.getCompleteCwd())
15
+ for d in dirs:
16
+ view.printDir(d)
17
+ for e in entries:
18
+ view.printEntry(e)
@@ -0,0 +1,7 @@
1
+ from .. import model, view
2
+
3
+ parser = view.Parser(prog="clear", description="Clears the Cache.")
4
+
5
+
6
+ def main(self: object, argv: list[str]):
7
+ model.clearCache()
von/controller/edit.py ADDED
@@ -0,0 +1,45 @@
1
+ import logging
2
+ import subprocess
3
+
4
+ from .. import model, view
5
+ from ..fzf import fzf_choose
6
+ from ..rc import EDITOR
7
+ from . import preview
8
+
9
+ parser = view.Parser(prog="edit", description="Opens problem(s) by source name.")
10
+ parser.add_argument(
11
+ "-a",
12
+ "--all",
13
+ action="store_true",
14
+ dest="all",
15
+ help="Edit all problems in cache",
16
+ )
17
+ parser.add_argument(
18
+ "keys",
19
+ nargs="*",
20
+ help="The key of the problem to open (either source or cache index).",
21
+ )
22
+
23
+
24
+ def main(self: object, argv: list[str]):
25
+ opts = parser.process(argv)
26
+ opts.verbose = True
27
+
28
+ if opts.all is True:
29
+ entries = model.readCache()
30
+ elif len(opts.keys) == 0:
31
+ entries = [model.getEntryByKey(fzf_choose())]
32
+ else:
33
+ entries = [model.getEntryByKey(key) for key in opts.keys]
34
+ for entry in entries:
35
+ if entry is None:
36
+ logging.error("Not found")
37
+ else:
38
+ full_path = model.completePath(entry.path)
39
+ preview.make_preview(full_path)
40
+ subprocess.call([EDITOR, full_path]) # do editing
41
+
42
+ problem = model.makeProblemFromPath(entry.path)
43
+ new_entry = model.updateEntryByProblem(old_entry=entry, new_problem=problem)
44
+ # update cache after editing problem
45
+ view.printEntry(new_entry)
@@ -0,0 +1,9 @@
1
+ from .. import model, view
2
+
3
+ parser = view.Parser(
4
+ prog="index", description="Rebuilds the problem index. No arguments."
5
+ )
6
+
7
+
8
+ def main(self: object, argv: list[str]):
9
+ model.rebuildIndex()
von/controller/link.py ADDED
@@ -0,0 +1,22 @@
1
+ import logging
2
+ import sys
3
+
4
+ from .. import model, view
5
+ from ..clipboard import set_clipboard
6
+
7
+ parser = view.Parser(prog="link", description="Pastes the URL to clipboard")
8
+ parser.add_argument("key", help="The key of the problem to open")
9
+
10
+
11
+ def main(self: object, argv: list[str]):
12
+ opts = parser.process(argv)
13
+ entry = model.getEntryByKey(opts.key)
14
+ if entry is None:
15
+ logging.error(opts.key + " not found")
16
+ else:
17
+ url = entry.url
18
+ if url is None:
19
+ print("No URL is provided for this problem")
20
+ sys.exit(1)
21
+ print(url)
22
+ set_clipboard(url)
@@ -0,0 +1,91 @@
1
+ import logging
2
+
3
+ from .. import model, strparse, view
4
+ from ..fzf import fzf_choose
5
+
6
+ parser = view.Parser(
7
+ prog="markup",
8
+ description="Gives a Markdown version of problem statement for copy-pasting in email etc.",
9
+ )
10
+ parser.add_argument(
11
+ "keys",
12
+ nargs="*",
13
+ help="The key(S) of the problem to open.",
14
+ )
15
+ parser.add_argument(
16
+ "-p",
17
+ "--preserve",
18
+ action="store_true",
19
+ help="Suppress macro expansion from body.",
20
+ )
21
+ parser_display_modes = parser.add_mutually_exclusive_group()
22
+
23
+ parser_display_modes.add_argument(
24
+ "-a",
25
+ "--aops",
26
+ action="store_true",
27
+ help="Returns in AoPS mode.",
28
+ )
29
+ parser_display_modes.add_argument(
30
+ "-d",
31
+ "--discord",
32
+ action="store_true",
33
+ help="Returns in Discord mode.",
34
+ )
35
+
36
+
37
+ def main(self: object, argv: list[str]):
38
+ opts = parser.process(argv)
39
+ keys = opts.keys
40
+ if len(keys) == 0:
41
+ keys = [fzf_choose()]
42
+
43
+ for i, key in enumerate(keys):
44
+ entry = model.getEntryByKey(key)
45
+
46
+ if entry is None:
47
+ logging.error(opts.key + " not found")
48
+ return
49
+ elif entry.secret and not opts.brave:
50
+ logging.error("Problem can't be shown without --brave option")
51
+ return
52
+
53
+ problem = entry.full
54
+ statement = problem.bodies[0]
55
+
56
+ if opts.aops is True:
57
+ header = "[b]"
58
+ if problem.url is not None:
59
+ header += f"[url={problem.url}]{entry.source}[/url]"
60
+ else:
61
+ header += entry.source
62
+ if problem.author is not None:
63
+ header += f", proposed by {problem.author}.[/b]"
64
+ else:
65
+ header += ".[/b]"
66
+ view.out(header)
67
+ view.out(strparse.toAOPS(statement))
68
+ if i != len(keys) - 1:
69
+ view.out("\n--------\n")
70
+ else:
71
+ header = "## "
72
+ if problem.url is not None:
73
+ header += f"[{problem.source}]({problem.url})"
74
+ else:
75
+ header += entry.source
76
+ if problem.author is not None:
77
+ header += f", proposed by {problem.author}."
78
+ else:
79
+ header += "."
80
+ view.out(header)
81
+ if opts.discord is True:
82
+ view.out(r"```latex")
83
+ else:
84
+ view.out("")
85
+ view.out(
86
+ statement if opts.preserve is True else strparse.demacro(statement)
87
+ )
88
+ if opts.discord is True:
89
+ view.out(r"```")
90
+ elif i != len(keys) - 1:
91
+ view.out("\n")
von/controller/nuke.py ADDED
@@ -0,0 +1,18 @@
1
+ from pathlib import Path
2
+
3
+ from .. import model, view
4
+ from ..puid import inferPUID
5
+ from ..rc import VON_BASE_PATH
6
+
7
+ parser = view.Parser(prog="nuke", description="Fixes all the filenames to match PUID")
8
+
9
+
10
+ def main(self: object, argv: list[str]):
11
+ parser.process(argv)
12
+ for p in model.getAllProblems():
13
+ puid = inferPUID(p.source)
14
+ src = VON_BASE_PATH / Path(p.path)
15
+ target = VON_BASE_PATH / src.parent / f"{puid}.tex"
16
+ if not target.exists():
17
+ src.rename(target)
18
+ model.rebuildIndex()
@@ -0,0 +1,10 @@
1
+ from .. import model, view
2
+
3
+ parser = view.Parser(
4
+ prog="paths", description="Prints the paths of all files in cache."
5
+ )
6
+
7
+
8
+ def main(self: object, argv: list[str]):
9
+ for entry in model.readCache():
10
+ print(entry.path)