in2lambda 0.3.3__tar.gz → 1.0.0__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 (42) hide show
  1. {in2lambda-0.3.3 → in2lambda-1.0.0}/PKG-INFO +5 -3
  2. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/api/question.py +1 -1
  3. in2lambda-1.0.0/in2lambda/api/set.py +185 -0
  4. in2lambda-1.0.0/in2lambda/api/visibility_status.py +76 -0
  5. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/filters/PartPartSolSol/filter.py +7 -7
  6. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/filters/PartSolPartSol/filter.py +8 -10
  7. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/filters/PartsOneSol/filter.py +6 -6
  8. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/filters/PartsSepSol/filter.py +8 -8
  9. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/filters/markdown.py +8 -8
  10. in2lambda-1.0.0/in2lambda/json_convert/__init__.py +1 -0
  11. in2lambda-1.0.0/in2lambda/json_convert/json_convert.py +144 -0
  12. in2lambda-0.3.3/in2lambda/json_convert/minimal_template.json → in2lambda-1.0.0/in2lambda/json_convert/minimal_template_question.json +10 -10
  13. in2lambda-1.0.0/in2lambda/json_convert/minimal_template_set.json +9 -0
  14. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/main.py +9 -9
  15. {in2lambda-0.3.3 → in2lambda-1.0.0}/pyproject.toml +5 -1
  16. in2lambda-0.3.3/in2lambda/api/module.py +0 -125
  17. in2lambda-0.3.3/in2lambda/json_convert/__init__.py +0 -1
  18. in2lambda-0.3.3/in2lambda/json_convert/json_convert.py +0 -99
  19. {in2lambda-0.3.3 → in2lambda-1.0.0}/LICENSE +0 -0
  20. {in2lambda-0.3.3 → in2lambda-1.0.0}/README.md +0 -0
  21. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/__init__.py +0 -0
  22. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/api/__init__.py +0 -0
  23. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/api/part.py +0 -0
  24. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/example_questions.tex +0 -0
  25. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/example_solutions.tex +0 -0
  26. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/figures/O2_potential.png +0 -0
  27. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/figures/activated_complex.png +0 -0
  28. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/figures/rocket-momentum.png +0 -0
  29. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/filters/PartPartSolSol/__init__.py +0 -0
  30. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/filters/PartPartSolSol/example.tex +0 -0
  31. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/filters/PartSolPartSol/__init__.py +0 -0
  32. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/filters/PartSolPartSol/example.tex +0 -0
  33. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/filters/PartsOneSol/__init__.py +0 -0
  34. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/filters/PartsOneSol/example.tex +0 -0
  35. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/filters/PartsSepSol/__init__.py +0 -0
  36. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/filters/PartsSepSol/example.tex +0 -0
  37. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/filters/__init__.py +0 -0
  38. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/katex_convert/__init__.py +0 -0
  39. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/katex_convert/delete_list.txt +0 -0
  40. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/katex_convert/katex_convert.py +0 -0
  41. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/katex_convert/replace_list.txt +0 -0
  42. {in2lambda-0.3.3 → in2lambda-1.0.0}/in2lambda/py.typed +0 -0
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: in2lambda
3
- Version: 0.3.3
3
+ Version: 1.0.0
4
4
  Summary: Converts content ready for import into Lambda Feedback
5
- Home-page: https://lambda-feedback.github.io/in2lambda
6
5
  License: MIT
7
6
  Requires-Python: >=3.10,<4.0
8
7
  Classifier: Development Status :: 3 - Alpha
@@ -13,13 +12,16 @@ Classifier: Programming Language :: Python :: 3
13
12
  Classifier: Programming Language :: Python :: 3.10
14
13
  Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Programming Language :: Python :: 3 :: Only
17
17
  Classifier: Typing :: Typed
18
18
  Requires-Dist: beartype (>=0.17.2,<0.18.0)
19
19
  Requires-Dist: panflute (>=2.3.1,<3.0.0)
20
+ Requires-Dist: requests (<2.32.0)
20
21
  Requires-Dist: rich-click (>=1.7.4,<2.0.0)
21
22
  Project-URL: Bug Tracker, https://github.com/lambda-feedback/in2lambda/issues
22
23
  Project-URL: Documentation, https://lambda-feedback.github.io/in2lambda
24
+ Project-URL: Homepage, https://lambda-feedback.github.io/in2lambda
23
25
  Project-URL: Repository, https://github.com/lambda-feedback/in2lambda
24
26
  Description-Content-Type: text/markdown
25
27
 
@@ -1,4 +1,4 @@
1
- """A full question with optional parts that's contained in a module."""
1
+ """A full question with optional parts that's contained in a set."""
2
2
 
3
3
  from dataclasses import dataclass, field
4
4
  from typing import Union
@@ -0,0 +1,185 @@
1
+ """Represents a list of questions."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Union
5
+
6
+ import panflute as pf
7
+
8
+ from in2lambda.api.question import Question
9
+ from in2lambda.api.visibility_status import VisibilityController, VisibilityStatus
10
+
11
+
12
+ @dataclass
13
+ class Set:
14
+ """Represents a list of questions."""
15
+
16
+ _name: str = field(default="set")
17
+ _description: str = field(default="")
18
+ _finalAnswerVisibility: VisibilityController = field(
19
+ default_factory=lambda: VisibilityController(
20
+ VisibilityStatus.OPEN_WITH_WARNINGS
21
+ )
22
+ )
23
+ _workedSolutionVisibility: VisibilityController = field(
24
+ default_factory=lambda: VisibilityController(
25
+ VisibilityStatus.OPEN_WITH_WARNINGS
26
+ )
27
+ )
28
+ _structuredTutorialVisibility: VisibilityController = field(
29
+ default_factory=lambda: VisibilityController(VisibilityStatus.OPEN)
30
+ )
31
+
32
+ questions: list[Question] = field(default_factory=list)
33
+ _current_question_index = -1
34
+
35
+ @property
36
+ def current_question(self) -> Question:
37
+ """The current question being modified, or Question("INVALID") if there are no questions.
38
+
39
+ The reasoning behind returning Question("INVALID") is in case filter logic is being applied
40
+ on text before the first question (e.g. intro paragraphs). In that case, there is no effect.
41
+
42
+ Returns:
43
+ The current question or Question("INVALID") if there are no questions.
44
+
45
+ Examples:
46
+ >>> from in2lambda.api.set import Set
47
+ >>> Set().current_question
48
+ Question(title='INVALID', parts=[], images=[], main_text='')
49
+ >>> s = Set()
50
+ >>> s.add_question()
51
+ >>> s.current_question
52
+ Question(title='', parts=[], images=[], main_text='')
53
+ """
54
+ return (
55
+ self.questions[self._current_question_index]
56
+ if self.questions
57
+ else Question("INVALID")
58
+ )
59
+
60
+ def add_question(
61
+ self, title: str = "", main_text: Union[pf.Element, str] = pf.Str("")
62
+ ) -> None:
63
+ """Inserts a new question into the set.
64
+
65
+ Args:
66
+ title: An optional string for the title of the question. If no title
67
+ is provided, the question title auto-increments i.e. Question 1, 2, etc.
68
+ main_text: An optional string or panflute element for the main question text.
69
+
70
+ Examples:
71
+ >>> from in2lambda.api.set import Set
72
+ >>> import panflute as pf
73
+ >>> s = Set()
74
+ >>> s.add_question("Some title", pf.Para(pf.Str("hello"), pf.Space, pf.Str("there")))
75
+ >>> s.questions
76
+ [Question(title='Some title', parts=[], images=[], main_text='hello there')]
77
+ >>> s.add_question(main_text="Normal string text")
78
+ >>> s.questions[1].main_text
79
+ 'Normal string text'
80
+ """
81
+ question = Question(title=title)
82
+ question.main_text = main_text
83
+ self.questions.append(question)
84
+
85
+ def increment_current_question(self) -> None:
86
+ """Manually overrides the current question being modified.
87
+
88
+ The default (-1) indicates the last question added. Incrementing for the
89
+ first time sets to 0 i.e. the first question.
90
+
91
+ The is useful if adding question text first and answers later.
92
+
93
+ Examples:
94
+ >>> from in2lambda.api.set import Set
95
+ >>> s = Set()
96
+ >>> # Imagine adding the questions from a question file first...
97
+ >>> s.add_question("Question 1")
98
+ >>> s.add_question("Question 2")
99
+ >>> # ...and then adding solutions from an answer file later
100
+ >>> s.increment_current_question() # Loop back to question 1
101
+ >>> s.current_question.add_solution("Question 1 answer")
102
+ >>> s.increment_current_question()
103
+ >>> s.current_question.add_solution("Question 2 answer")
104
+ >>> s.questions
105
+ [Question(title='Question 1', parts=[Part(text='', worked_solution='Question 1 answer')], images=[], main_text=''),\
106
+ Question(title='Question 2', parts=[Part(text='', worked_solution='Question 2 answer')], images=[], main_text='')]
107
+ """
108
+ self._current_question_index += 1
109
+
110
+ def to_json(self, output_dir: str) -> None:
111
+ """Turns this set into Lambda Feedback JSON/ZIP files.
112
+
113
+ WARNING: This will overwrite any existing files in the directory.
114
+
115
+ Args:
116
+ output_dir: Where to output the final Lambda Feedback JSON/ZIP files.
117
+
118
+ Examples:
119
+ >>> import tempfile
120
+ >>> import os
121
+ >>> import json
122
+ >>> # Create a set with two questions
123
+ >>> s = Set()
124
+ >>> s.add_question("Question 1")
125
+ >>> s.add_question("Question 2")
126
+ >>> with tempfile.TemporaryDirectory() as temp_dir:
127
+ ... # Write the JSON files to the temporary directory
128
+ ... s.to_json(temp_dir)
129
+ ... # Check the contents of the directory
130
+ ... sorted(os.listdir(temp_dir))
131
+ ... # Check the contents of the set directory
132
+ ... sorted(os.listdir(f"{temp_dir}/set"))
133
+ ... # Check the title of the first question
134
+ ... with open(f"{temp_dir}/set/question_000_Question_1.json") as file:
135
+ ... print(f"Question 1's title: {json.load(file)['title']}")
136
+ ['set', 'set.zip']
137
+ ['question_000_Question_1.json', 'question_001_Question_2.json', 'set_set.json']
138
+ Question 1's title: Question 1
139
+ """
140
+ from in2lambda.json_convert import json_convert
141
+
142
+ json_convert.main(self, output_dir)
143
+
144
+ def set_name(self, name: str) -> None:
145
+ """Sets the name of the set.
146
+
147
+ Args:
148
+ name: The name to set for the set.
149
+
150
+ Examples:
151
+ >>> from in2lambda.api.set import Set
152
+ >>> s = Set()
153
+ >>> s.set_name("My Question Set")
154
+ >>> s._name
155
+ 'My Question Set'
156
+ """
157
+ self._name = name
158
+
159
+ def set_description(self, description: str) -> None:
160
+ """Sets the description of the set.
161
+
162
+ Args:
163
+ description: The description to set for the set.
164
+
165
+ Examples:
166
+ >>> from in2lambda.api.set import Set
167
+ >>> s = Set()
168
+ >>> s.set_description("This is my question set.")
169
+ >>> s._description
170
+ 'This is my question set.'
171
+ """
172
+ self._description = description
173
+
174
+ def __repr__(self):
175
+ """Custom representation showing visibility status values instead of memory addresses."""
176
+ return (
177
+ f"Set("
178
+ f"_name={self._name!r}, "
179
+ f"_description={self._description!r}, "
180
+ f"_finalAnswerVisibility={str(self._finalAnswerVisibility)!r}, "
181
+ f"_workedSolutionVisibility={str(self._workedSolutionVisibility)!r}, "
182
+ f"_structuredTutorialVisibility={str(self._structuredTutorialVisibility)!r}, "
183
+ f"questions={self.questions!r}"
184
+ f")"
185
+ )
@@ -0,0 +1,76 @@
1
+ """Module for managing visibility status of questions and sets."""
2
+
3
+ from enum import Enum
4
+
5
+ class VisibilityStatus(Enum):
6
+ """Enum representing the visibility status of a question or set."""
7
+
8
+ OPEN = "OPEN"
9
+ HIDE = "HIDE"
10
+ OPEN_WITH_WARNINGS = "OPEN_WITH_WARNINGS"
11
+
12
+ def __str__(self):
13
+ """Return the string representation of the visibility status."""
14
+ return self.value
15
+
16
+ def __repr__(self) -> str:
17
+ """Return a string representation for debugging."""
18
+ return str(self)
19
+
20
+
21
+ class VisibilityController:
22
+ """Controller for managing visibility status with easy-to-use methods."""
23
+
24
+ def __init__(self, initial_status: VisibilityStatus = VisibilityStatus.OPEN):
25
+ """Initialize the VisibilityController with a specific status."""
26
+ self._status = initial_status
27
+
28
+ @property
29
+ def status(self) -> VisibilityStatus:
30
+ """Return the current visibility status."""
31
+ return self._status
32
+
33
+ def to_open(self):
34
+ """Change status to OPEN.
35
+
36
+ Example:
37
+ >>> vc = VisibilityController()
38
+ >>> vc.to_open()
39
+ >>> vc.status
40
+ OPEN
41
+ """
42
+ self._status = VisibilityStatus.OPEN
43
+
44
+ def to_hide(self):
45
+ """Change status to HIDE.
46
+
47
+ Example:
48
+ >>> vc = VisibilityController()
49
+ >>> vc.to_hide()
50
+ >>> vc.status
51
+ HIDE
52
+ """
53
+ self._status = VisibilityStatus.HIDE
54
+
55
+ def to_open_with_warnings(self):
56
+ """Change status to OPEN_WITH_WARNINGS.
57
+
58
+ Example:
59
+ >>> vc = VisibilityController()
60
+ >>> vc.to_open_with_warnings()
61
+ >>> vc.status
62
+ OPEN_WITH_WARNINGS
63
+ """
64
+ self._status = VisibilityStatus.OPEN_WITH_WARNINGS
65
+
66
+ def __str__(self):
67
+ """Return the string representation of the visibility status."""
68
+ return str(self._status)
69
+
70
+ def __repr__(self) -> str:
71
+ """Return a string representation for debugging."""
72
+ return str(self)
73
+
74
+ def to_dict(self):
75
+ """Convert VisibilityController to dictionary for JSON serialization."""
76
+ return {"status": str(self._status)}
@@ -6,7 +6,7 @@ from typing import Optional
6
6
 
7
7
  import panflute as pf
8
8
 
9
- from in2lambda.api.module import Module
9
+ from in2lambda.api.set import Set
10
10
  from in2lambda.filters.markdown import filter
11
11
 
12
12
 
@@ -14,7 +14,7 @@ from in2lambda.filters.markdown import filter
14
14
  def pandoc_filter(
15
15
  elem: pf.Element,
16
16
  doc: pf.elements.Doc,
17
- module: Module,
17
+ set: Set,
18
18
  parsing_answers: bool,
19
19
  ) -> Optional[pf.Str]:
20
20
  """A Pandoc filter that parses and translates various TeX elements.
@@ -23,7 +23,7 @@ def pandoc_filter(
23
23
  elem: The current TeX element being processed. This could be a paragraph,
24
24
  ordered list, etc.
25
25
  doc: A Pandoc document container - essentially the Pandoc AST.
26
- module: The Python API that is used to store the result after processing
26
+ set: The Python API that is used to store the result after processing
27
27
  the TeX file.
28
28
  parsing_answers: Whether an answers-only document is currently being parsed.
29
29
 
@@ -46,10 +46,10 @@ def pandoc_filter(
46
46
 
47
47
  # Solutions are in a Div
48
48
  if isinstance(elem, pf.Div):
49
- module.add_question(main_text="\n".join(pandoc_filter.question))
49
+ set.add_question(main_text="\n".join(pandoc_filter.question))
50
50
  if hasattr(pandoc_filter, "parts"):
51
51
  for part in pandoc_filter.parts:
52
- module.current_question.add_part_text(part)
52
+ set.current_question.add_part_text(part)
53
53
  pandoc_filter.question = []
54
54
  pandoc_filter.parts = []
55
55
 
@@ -58,8 +58,8 @@ def pandoc_filter(
58
58
  match type(first_answer_part := elem.content[0]):
59
59
  case pf.OrderedList:
60
60
  for item in first_answer_part.content:
61
- module.current_question.add_solution(pf.stringify(item))
61
+ set.current_question.add_solution(pf.stringify(item))
62
62
  case pf.Para:
63
- module.current_question.add_solution(pf.stringify(elem))
63
+ set.current_question.add_solution(pf.stringify(elem))
64
64
 
65
65
  return None
@@ -7,7 +7,7 @@ from typing import Optional
7
7
 
8
8
  import panflute as pf
9
9
 
10
- from in2lambda.api.module import Module
10
+ from in2lambda.api.set import Set
11
11
  from in2lambda.filters.markdown import filter
12
12
 
13
13
 
@@ -15,7 +15,7 @@ from in2lambda.filters.markdown import filter
15
15
  def pandoc_filter(
16
16
  elem: pf.Element,
17
17
  doc: pf.elements.Doc,
18
- module: Module,
18
+ set: Set,
19
19
  parsing_answers: bool,
20
20
  ) -> Optional[pf.Str]:
21
21
  """A Pandoc filter that parses and translates various TeX elements.
@@ -24,7 +24,7 @@ def pandoc_filter(
24
24
  elem: The current TeX element being processed. This could be a paragraph,
25
25
  ordered list, etc.
26
26
  doc: A Pandoc document container - essentially the Pandoc AST.
27
- module: The Python API that is used to store the result after processing
27
+ set: The Python API that is used to store the result after processing
28
28
  the TeX file.
29
29
  parsing_answers: Whether an answers-only document is currently being parsed.
30
30
 
@@ -48,16 +48,14 @@ def pandoc_filter(
48
48
  for item in listItem.content
49
49
  if not isinstance(item, pf.Div)
50
50
  ]
51
- module.current_question.add_part_text("\n".join(part))
52
- module.current_question.add_solution(
53
- pandoc_filter.solutions.popleft()
54
- )
51
+ set.current_question.add_part_text("\n".join(part))
52
+ set.current_question.add_solution(pandoc_filter.solutions.popleft())
55
53
 
56
54
  if isinstance(elem, pf.Div):
57
55
  pandoc_filter.solutions.append(pf.stringify(elem))
58
56
  if pandoc_filter.question:
59
- module.add_question(main_text="\n".join(pandoc_filter.question))
60
- module.current_question.add_solution(pf.stringify(elem))
61
- module.current_question._last_part["solution"] -= 1
57
+ set.add_question(main_text="\n".join(pandoc_filter.question))
58
+ set.current_question.add_solution(pf.stringify(elem))
59
+ set.current_question._last_part["solution"] -= 1
62
60
  pandoc_filter.question = []
63
61
  return None
@@ -9,7 +9,7 @@ from typing import Optional
9
9
 
10
10
  import panflute as pf
11
11
 
12
- from in2lambda.api.module import Module
12
+ from in2lambda.api.set import Set
13
13
  from in2lambda.filters.markdown import filter
14
14
 
15
15
 
@@ -17,7 +17,7 @@ from in2lambda.filters.markdown import filter
17
17
  def pandoc_filter(
18
18
  elem: pf.Element,
19
19
  doc: pf.elements.Doc,
20
- module: Module,
20
+ set: Set,
21
21
  parsing_answers: bool,
22
22
  ) -> Optional[pf.Str]:
23
23
  """A Pandoc filter that parses and translates various TeX elements.
@@ -26,7 +26,7 @@ def pandoc_filter(
26
26
  elem: The current TeX element being processed. This could be a paragraph,
27
27
  ordered list, etc.
28
28
  doc: A Pandoc document container - essentially the Pandoc AST.
29
- module: The Python API that is used to store the result after processing
29
+ set: The Python API that is used to store the result after processing
30
30
  the TeX file.
31
31
  parsing_answers: Whether an answers-only document is currently being parsed.
32
32
 
@@ -42,17 +42,17 @@ def pandoc_filter(
42
42
  isinstance(elem.prev, pf.Header)
43
43
  and pf.stringify(elem.prev) != "Solution"
44
44
  ):
45
- module.add_question(main_text=elem)
45
+ set.add_question(main_text=elem)
46
46
 
47
47
  # Parts are denoted via ordered lists
48
48
  case pf.OrderedList:
49
49
  for item in elem.content:
50
- module.current_question.add_part_text(item)
50
+ set.current_question.add_part_text(item)
51
51
 
52
52
  # Solution is in a Div with nested content being "Solution"
53
53
  case pf.Div:
54
54
  if pf.stringify(elem.content[0].content) == "Solution":
55
- module.current_question.add_solution(
55
+ set.current_question.add_solution(
56
56
  pf.stringify(elem) # [len("Solution") :] - For Jon Rackham
57
57
  )
58
58
 
@@ -9,7 +9,7 @@ from typing import Optional
9
9
 
10
10
  import panflute as pf
11
11
 
12
- from in2lambda.api.module import Module
12
+ from in2lambda.api.set import Set
13
13
  from in2lambda.filters.markdown import filter
14
14
 
15
15
 
@@ -17,7 +17,7 @@ from in2lambda.filters.markdown import filter
17
17
  def pandoc_filter(
18
18
  elem: pf.Element,
19
19
  doc: pf.elements.Doc,
20
- module: Module,
20
+ set: Set,
21
21
  parsing_answers: bool,
22
22
  ) -> Optional[pf.Str]:
23
23
  """A Pandoc filter that parses and translates various TeX elements.
@@ -26,7 +26,7 @@ def pandoc_filter(
26
26
  elem: The current TeX element being processed. This could be a paragraph,
27
27
  ordered list, etc.
28
28
  doc: A Pandoc document container - essentially the Pandoc AST.
29
- module: The Python API that is used to store the result after processing
29
+ set: The Python API that is used to store the result after processing
30
30
  the TeX file.
31
31
  parsing_answers: Whether an answers-only document is currently being parsed.
32
32
 
@@ -39,7 +39,7 @@ def pandoc_filter(
39
39
  for numbered_part in elem.content:
40
40
  if parsing_answers:
41
41
  # Denotes that we've reached the answer for a new question
42
- module.increment_current_question()
42
+ set.increment_current_question()
43
43
  # For each numbered question, extract blurb and parts
44
44
  blurb: list[str] = []
45
45
  lettered_parts: list[str] = []
@@ -64,17 +64,17 @@ def pandoc_filter(
64
64
 
65
65
  # Answers for questions with no parts
66
66
  if parsing_answers and not lettered_parts:
67
- module.current_question.add_solution(spaced_blurb)
67
+ set.current_question.add_solution(spaced_blurb)
68
68
  # Add the main question text as the Lambda Feedback blurb
69
69
  elif not parsing_answers:
70
- module.add_question(main_text=spaced_blurb)
70
+ set.add_question(main_text=spaced_blurb)
71
71
 
72
72
  # Add each part solution/text
73
73
  # For the solution, prepend any top level answer text to each part answer
74
74
  for part in lettered_parts:
75
75
  (
76
- module.current_question.add_solution(spaced_blurb + part)
76
+ set.current_question.add_solution(spaced_blurb + part)
77
77
  if parsing_answers
78
- else module.current_question.add_part_text(part)
78
+ else set.current_question.add_part_text(part)
79
79
  )
80
80
  return None
@@ -9,7 +9,7 @@ import panflute as pf
9
9
  from beartype.typing import Callable, Optional
10
10
  from rich_click import echo
11
11
 
12
- from in2lambda.api.module import Module
12
+ from in2lambda.api.set import Set
13
13
  from in2lambda.katex_convert.katex_convert import latex_to_katex
14
14
 
15
15
 
@@ -32,7 +32,7 @@ def image_directories(tex_file: str) -> list[str]:
32
32
  >>> temp_dir = tempfile.mkdtemp()
33
33
  >>> tex_file = os.path.join(temp_dir, 'test.tex')
34
34
  >>> with open(tex_file, 'w') as f:
35
- ... f.write("\\graphicspath{{subdir1/}{subdir2/}{subdir3/}}")
35
+ ... f.write("\\graphicspath{{subdir1/}{subdir2/}{subdir3/}}")
36
36
  45
37
37
  >>> image_directories(tex_file)
38
38
  ['subdir1/', 'subdir2/', 'subdir3/']
@@ -71,7 +71,7 @@ def image_path(image_name: str, tex_file: str) -> Optional[str]:
71
71
  The absolute path to the image if it can be found. If not, it returns None.
72
72
 
73
73
  Examples:
74
- >>> from in2lambda.filters.markdown import image_path
74
+ set.current_question.images.append(path)
75
75
  >>> import tempfile
76
76
  >>> import os
77
77
  >>> # Example TeX file with a subdirectory
@@ -117,11 +117,11 @@ def image_path(image_name: str, tex_file: str) -> Optional[str]:
117
117
 
118
118
  def filter(
119
119
  func: Callable[
120
- [pf.Element, pf.elements.Doc, Module, bool],
120
+ [pf.Element, pf.elements.Doc, Set, bool],
121
121
  Optional[pf.Str],
122
122
  ]
123
123
  ) -> Callable[
124
- [pf.Element, pf.elements.Doc, Module, str, bool],
124
+ [pf.Element, pf.elements.Doc, Set, str, bool],
125
125
  Optional[pf.Str],
126
126
  ]:
127
127
  """Python decorator to make generic LaTeX elements markdown readable.
@@ -136,7 +136,7 @@ def filter(
136
136
  def markdown_converter(
137
137
  elem: pf.Element,
138
138
  doc: pf.elements.Doc,
139
- module: Module,
139
+ set: Set,
140
140
  tex_file: str,
141
141
  parsing_answers: bool,
142
142
  ) -> Optional[pf.Str]:
@@ -175,7 +175,7 @@ def filter(
175
175
  if path is None:
176
176
  echo(f"Warning: Couldn't find {elem.url}")
177
177
  else:
178
- module.current_question.images.append(path)
178
+ set.current_question.images.append(path)
179
179
  return pf.Str(f"![pictureTag]({elem.url})")
180
180
 
181
181
  case pf.Strong:
@@ -189,6 +189,6 @@ def filter(
189
189
  case pf.Str:
190
190
  return pf.Str(elem.text.replace("\u00a0", "\u202f"))
191
191
 
192
- return func(elem, doc, module, parsing_answers)
192
+ return func(elem, doc, set, parsing_answers)
193
193
 
194
194
  return markdown_converter
@@ -0,0 +1 @@
1
+ """Files relating to translating the questions of a Python set object into JSON."""
@@ -0,0 +1,144 @@
1
+ """Converts questions from a Python set object into Lambda Feedback JSON."""
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import shutil
7
+ import zipfile
8
+ from copy import deepcopy
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from in2lambda.api.set import Set
13
+
14
+ MINIMAL_QUESTION_TEMPLATE = "minimal_template_question.json"
15
+ MINIMAL_SET_TEMPLATE = "minimal_template_set.json"
16
+
17
+
18
+ def _zip_sorted_folder(folder_path, zip_path):
19
+ """Zips the contents of a folder, preserving the directory structure.
20
+
21
+ Args:
22
+ folder_path: The path to the folder to zip.
23
+ zip_path: The path where the zip file will be created.
24
+ """
25
+ with zipfile.ZipFile(zip_path, "w") as zf:
26
+ for root, dirs, files in os.walk(folder_path):
27
+ # Sort files for deterministic, alphabetical order
28
+ for file in sorted(files):
29
+ abs_path = os.path.join(root, file)
30
+ rel_path = os.path.relpath(abs_path, folder_path)
31
+ zf.write(abs_path, arcname=rel_path)
32
+
33
+
34
+ def converter(
35
+ question_template: dict[str, Any],
36
+ set_template: dict[str, Any],
37
+ SetQuestions: Set,
38
+ output_dir: str,
39
+ ) -> None:
40
+ """Turns a set of question objects into Lambda Feedback JSON.
41
+
42
+ Args:
43
+ question_template: The loaded JSON from the minimal question template (it needs to be in sync).
44
+ set_template: The loaded JSON from the minimal set template (it needs to be in sync).
45
+ SetQuestions: A Set object containing questions.
46
+ output_dir: The absolute path for where to produced the final JSON/zip files.
47
+ """
48
+ ListQuestions = SetQuestions.questions
49
+ set_name = SetQuestions._name
50
+ set_description = SetQuestions._description
51
+
52
+ # create directory to put the questions
53
+ os.makedirs(output_dir, exist_ok=True)
54
+ output_question = os.path.join(output_dir, set_name)
55
+ os.makedirs(output_question, exist_ok=True)
56
+
57
+ set_template["name"] = set_name
58
+ set_template["description"] = set_description
59
+ set_template["finalAnswerVisibility"] = str(
60
+ SetQuestions._finalAnswerVisibility.status
61
+ )
62
+ set_template["workedSolutionVisibility"] = str(
63
+ SetQuestions._workedSolutionVisibility.status
64
+ )
65
+ set_template["structuredTutorialVisibility"] = str(
66
+ SetQuestions._structuredTutorialVisibility.status
67
+ )
68
+ # create the set file
69
+ with open(f"{output_question}/set_{set_name}.json", "w") as file:
70
+ json.dump(set_template, file)
71
+
72
+ for i in range(len(ListQuestions)):
73
+ output = deepcopy(question_template)
74
+
75
+ output["orderNumber"] = i # order number starts at 0
76
+ # add title to the question file
77
+ if ListQuestions[i].title != "":
78
+ output["title"] = ListQuestions[i].title
79
+ else:
80
+ output["title"] = "Question " + str(i + 1)
81
+
82
+ # add main text to the question file
83
+ output["masterContent"] = ListQuestions[i].main_text
84
+
85
+ # add parts to the question file
86
+ if ListQuestions[i].parts:
87
+ output["parts"][0]["content"] = ListQuestions[i].parts[0].text
88
+ output["parts"][0]["workedSolution"]["content"] = (
89
+ ListQuestions[i].parts[0].worked_solution
90
+ )
91
+ for j in range(1, len(ListQuestions[i].parts)):
92
+ output["parts"].append(deepcopy(question_template["parts"][0]))
93
+ output["parts"][j]["content"] = ListQuestions[i].parts[j].text
94
+ output["parts"][j]["orderNumber"] = j
95
+ output["parts"][j]["workedSolution"]["content"] = (
96
+ ListQuestions[i].parts[j].worked_solution
97
+ )
98
+
99
+ # Output file
100
+ filename = (
101
+ "question_" + str(i).zfill(3) + "_" + re.sub(r'[^\w\-_.]', '_', output['title'].strip())
102
+ )
103
+
104
+ # write questions into directory
105
+ with open(f"{output_question}/{filename}.json", "w") as file:
106
+ json.dump(output, file)
107
+
108
+ # write image into directory
109
+ for k in range(len(ListQuestions[i].images)):
110
+ image_path = os.path.abspath(
111
+ ListQuestions[i].images[k]
112
+ ) # converts computer path into python path
113
+ # If images exist, create a media directory
114
+ output_image = os.path.join(output_question, "media")
115
+ os.makedirs(output_image, exist_ok=True)
116
+ shutil.copy(image_path, output_image) # copies image into the directory
117
+
118
+ # output zip file in destination folder
119
+ _zip_sorted_folder(output_question, output_question + ".zip")
120
+
121
+
122
+ def main(set_questions: Set, output_dir: str) -> None:
123
+ """Preliminary defensive programming before calling the main converter function.
124
+
125
+ This ultimately then produces the Lambda Feedback JSON/ZIP files.
126
+
127
+ Args:
128
+ set_questions: A Set object containing questions.
129
+ output_dir: Where to output the final Lambda Feedback JSON/ZIP files.
130
+ """
131
+ # Use path so minimal template can be found regardless of where the user is running python from.
132
+ with open(Path(__file__).with_name(MINIMAL_QUESTION_TEMPLATE), "r") as file:
133
+ question_template = json.load(file)
134
+
135
+ with open(Path(__file__).with_name(MINIMAL_SET_TEMPLATE), "r") as file:
136
+ set_template = json.load(file)
137
+
138
+ # check if directory exists in file
139
+ if os.path.isdir(output_dir):
140
+ try:
141
+ shutil.rmtree(output_dir)
142
+ except OSError as e:
143
+ print("Error: %s : %s" % (output_dir, e.strerror))
144
+ converter(question_template, set_template, set_questions, output_dir)
@@ -1,23 +1,23 @@
1
1
  {
2
+ "orderNumber": 0,
3
+ "title": "Question title here",
4
+ "masterContent": "Top level question here",
5
+ "publish": false,
2
6
  "displayFinalAnswer": true,
3
7
  "displayStructuredTutorial": true,
4
8
  "displayWorkedSolution": true,
5
- "masterContent": "Top level question here",
9
+ "displayChatbot": false,
6
10
  "parts": [
7
11
  {
8
- "answer": "",
9
- "content": "Part text here",
10
12
  "orderNumber": 0,
13
+ "content": "Part text here",
14
+ "answerContent": "",
11
15
  "responseAreas": [],
12
- "tutorial": [],
13
- "universalPartId": "N/A",
14
16
  "workedSolution": {
17
+ "title": "",
15
18
  "content": "Part worked solution here",
16
- "id": "N/A",
17
- "title": ""
19
+ "children": []
18
20
  }
19
21
  }
20
- ],
21
- "publish": false,
22
- "title": "Question title here"
22
+ ]
23
23
  }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "a simple example",
3
+ "description": "description here",
4
+ "manuallyHidden": true,
5
+ "finalAnswerVisibility": "OPEN_WITH_WARNINGS",
6
+ "workedSolutionVisibility": "OPEN_WITH_WARNINGS",
7
+ "structuredTutorialVisibility": "OPEN",
8
+ "chatbotVisibility": "HIDE"
9
+ }
@@ -15,7 +15,7 @@ import panflute as pf
15
15
  import rich_click as click
16
16
 
17
17
  import in2lambda.filters
18
- from in2lambda.api.module import Module
18
+ from in2lambda.api.set import Set
19
19
 
20
20
 
21
21
  def docx_to_md(docx_file: str) -> str:
@@ -81,7 +81,7 @@ def runner(
81
81
  chosen_filter: str,
82
82
  output_dir: Optional[str] = None,
83
83
  answer_file: Optional[str] = None,
84
- ) -> Module:
84
+ ) -> Set:
85
85
  r"""Takes in a TeX file for a given subject and outputs how it's broken down within Lambda Feedback.
86
86
 
87
87
  Args:
@@ -100,12 +100,12 @@ def runner(
100
100
  >>> from in2lambda.main import runner
101
101
  >>> # Retrieve an example TeX file and run the given filter.
102
102
  >>> runner(f"{os.path.dirname(in2lambda.__file__)}/filters/PartsSepSol/example.tex", "PartsSepSol") # doctest: +ELLIPSIS
103
- Module(questions=[Question(title='', parts=[Part(text=..., worked_solution=''), ...], images=[], main_text='This is a sample question\n\n'), ...])
103
+ Set(_name='set', _description='', _finalAnswerVisibility='OPEN_WITH_WARNINGS', _workedSolutionVisibility='OPEN_WITH_WARNINGS', _structuredTutorialVisibility='OPEN', questions=[Question(title='', parts=[Part(text=..., worked_solution=''), ...], images=[], main_text='This is a sample question\n\n'), ...])
104
104
  >>> runner(f"{os.path.dirname(in2lambda.__file__)}/filters/PartsOneSol/example.tex", "PartsOneSol") # doctest: +ELLIPSIS
105
- Module(questions=[Question(title='', parts=[Part(text='This is part (a)\n\n', worked_solution=''), ...], images=[], main_text='Here is some preliminary question information that might be useful.'), ...)
105
+ Set(_name='set', _description='', _finalAnswerVisibility='OPEN_WITH_WARNINGS', _workedSolutionVisibility='OPEN_WITH_WARNINGS', _structuredTutorialVisibility='OPEN', questions=[Question(title='', parts=[Part(text=..., worked_solution=''), ...], images=[], main_text='Here is some preliminary question information that might be useful.'), ...])
106
106
  """
107
107
  # The list of questions for Lambda Feedback as a Python API.
108
- module = Module()
108
+ set_obj = Set()
109
109
 
110
110
  # Dynamically import the correct pandoc filter depending on the subject.
111
111
  filter_module = importlib.import_module(f"in2lambda.filters.{chosen_filter}.filter")
@@ -124,7 +124,7 @@ def runner(
124
124
  pf.run_filter(
125
125
  filter_module.pandoc_filter,
126
126
  doc=pf.convert_text(text, input_format=input_format, standalone=True),
127
- module=module,
127
+ set=set_obj,
128
128
  tex_file=question_file,
129
129
  parsing_answers=False,
130
130
  )
@@ -146,16 +146,16 @@ def runner(
146
146
  doc=pf.convert_text(
147
147
  answer_text, input_format=answer_format, standalone=True
148
148
  ),
149
- module=module,
149
+ set=set_obj,
150
150
  tex_file=answer_file,
151
151
  parsing_answers=True,
152
152
  )
153
153
 
154
154
  # Read the Python API format and convert to JSON.
155
155
  if output_dir is not None:
156
- module.to_json(output_dir)
156
+ set_obj.to_json(output_dir)
157
157
 
158
- return module
158
+ return set_obj
159
159
 
160
160
 
161
161
  @click.command(
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "in2lambda"
3
- version = "0.3.3"
3
+ version = "1.0.0"
4
4
  description = "Converts content ready for import into Lambda Feedback"
5
5
  authors = []
6
6
  license = "MIT"
@@ -26,6 +26,7 @@ python = "^3.10"
26
26
  panflute = "^2.3.1"
27
27
  rich-click = "^1.7.4"
28
28
  beartype = "^0.17.2"
29
+ requests = "<2.32.0"
29
30
 
30
31
  [tool.poetry.scripts]
31
32
  in2lambda = "in2lambda.main:cli"
@@ -56,6 +57,9 @@ sphinx-togglebutton = "^0.3.2"
56
57
  # panflute is missing type hints
57
58
  ignore_missing_imports = true
58
59
 
60
+ [tool.isort]
61
+ profile = "black"
62
+
59
63
  [build-system]
60
64
  requires = ["poetry-core"]
61
65
  build-backend = "poetry.core.masonry.api"
@@ -1,125 +0,0 @@
1
- """Represents a list of questions."""
2
-
3
- from dataclasses import dataclass, field
4
- from typing import Union
5
-
6
- import panflute as pf
7
-
8
- from in2lambda.api.question import Question
9
- from in2lambda.json_convert import json_convert
10
-
11
-
12
- @dataclass
13
- class Module:
14
- """Represents a list of questions."""
15
-
16
- questions: list[Question] = field(default_factory=list)
17
- _current_question_index = -1
18
-
19
- @property
20
- def current_question(self) -> Question:
21
- """The current question being modified, or Question("INVALID") if there are no questions.
22
-
23
- The reasoning behind returning Question("INVALID") is in case filter logic is being applied
24
- on text before the first question (e.g. intro paragraphs). In that case, there is no effect.
25
-
26
- Returns:
27
- The current question or Question("INVALID") if there are no questions.
28
-
29
- Examples:
30
- >>> from in2lambda.api.module import Module
31
- >>> Module().current_question
32
- Question(title='INVALID', parts=[], images=[], main_text='')
33
- >>> module = Module()
34
- >>> module.add_question()
35
- >>> module.current_question
36
- Question(title='', parts=[], images=[], main_text='')
37
- """
38
- return (
39
- self.questions[self._current_question_index]
40
- if self.questions
41
- else Question("INVALID")
42
- )
43
-
44
- def add_question(
45
- self, title: str = "", main_text: Union[pf.Element, str] = pf.Str("")
46
- ) -> None:
47
- """Inserts a new question into the module.
48
-
49
- Args:
50
- title: An optional string for the title of the question. If no title
51
- is provided, the question title auto-increments i.e. Question 1, 2, etc.
52
- main_text: An optional string or panflute element for the main question text.
53
-
54
- Examples:
55
- >>> from in2lambda.api.module import Module
56
- >>> import panflute as pf
57
- >>> module = Module()
58
- >>> module.add_question("Some title", pf.Para(pf.Str("hello"), pf.Space, pf.Str("there")))
59
- >>> module
60
- Module(questions=[Question(title='Some title', parts=[], images=[], main_text='hello there')])
61
- >>> module.add_question(main_text="Normal string text")
62
- >>> module.questions[1].main_text
63
- 'Normal string text'
64
- """
65
- question = Question(title=title)
66
- question.main_text = main_text
67
- self.questions.append(question)
68
-
69
- def increment_current_question(self) -> None:
70
- """Manually overrides the current question being modified.
71
-
72
- The default (-1) indicates the last question added. Incrementing for the
73
- first time sets to 0 i.e. the first question.
74
-
75
- The is useful if adding question text first and answers later.
76
-
77
- Examples:
78
- >>> from in2lambda.api.module import Module
79
- >>> module = Module()
80
- >>> # Imagine adding the questions from a question file first...
81
- >>> module.add_question("Question 1")
82
- >>> module.add_question("Question 2")
83
- >>> # ...and then adding solutions from an answer file later
84
- >>> module.increment_current_question() # Loop back to question 1
85
- >>> module.current_question.add_solution("Question 1 answer")
86
- >>> module.increment_current_question()
87
- >>> module.current_question.add_solution("Question 2 answer")
88
- >>> module.questions
89
- [Question(title='Question 1', parts=[Part(text='', worked_solution='Question 1 answer')], images=[], main_text=''),\
90
- Question(title='Question 2', parts=[Part(text='', worked_solution='Question 2 answer')], images=[], main_text='')]
91
- """
92
- self._current_question_index += 1
93
-
94
- def to_json(self, output_dir: str) -> None:
95
- """Turns this module into Lambda Feedback JSON/ZIP files.
96
-
97
- WARNING: This will overwrite any existing files in the directory.
98
-
99
- Args:
100
- output_dir: Where to output the final Lambda Feedback JSON/ZIP files.
101
-
102
- Examples:
103
- >>> import tempfile
104
- >>> import os
105
- >>> import json
106
- >>> # Create a module with two questions
107
- >>> module = Module()
108
- >>> module.add_question("Question 1")
109
- >>> module.add_question("Question 2")
110
- >>> with tempfile.TemporaryDirectory() as temp_dir:
111
- ... # Write the JSON files to the temporary directory
112
- ... module.to_json(temp_dir)
113
- ... # Check the contents of the directory
114
- ... sorted(os.listdir(temp_dir))
115
- ... # Check the contents of the set directory
116
- ... sorted(os.listdir(f"{temp_dir}/set"))
117
- ... # Check the title of the first question
118
- ... with open(f"{temp_dir}/set/question_1.json") as file:
119
- ... print(f"Question 1's title: {json.load(file)['title']}")
120
- ['set', 'set.zip']
121
- ['media', 'question_1.json', 'question_2.json']
122
- Question 1's title: Question 1
123
-
124
- """
125
- json_convert.main(self.questions, output_dir)
@@ -1 +0,0 @@
1
- """Files relating to translating the questions of a Python module object into JSON."""
@@ -1,99 +0,0 @@
1
- """Converts questions from a Python module object into Lambda Feedback JSON."""
2
-
3
- import json
4
- import os
5
- import shutil
6
- from copy import deepcopy
7
- from pathlib import Path
8
- from typing import Any
9
-
10
- from in2lambda.api.question import Question
11
-
12
- MINIMAL_TEMPLATE = "minimal_template.json"
13
-
14
-
15
- def converter(
16
- template: dict[str, Any], ListQuestions: list[Question], output_dir: str
17
- ) -> None:
18
- """Turns a list of question objects into Lambda Feedback JSON.
19
-
20
- Args:
21
- template: The loaded JSON from the minimal template (it needs to be in sync).
22
- ListQuestions: A list of question objects.
23
- output_dir: The absolute path for where to produced the final JSON/zip files.
24
- """
25
- # Create output by copying template
26
-
27
- # create directory to put the questions
28
- os.makedirs(output_dir, exist_ok=True)
29
- output_question = os.path.join(output_dir, "set")
30
- os.makedirs(output_question, exist_ok=True)
31
-
32
- # create directory to put images - should be in set
33
- output_image = os.path.join(output_question, "media")
34
- os.makedirs(output_image, exist_ok=True)
35
-
36
- for i in range(len(ListQuestions)):
37
- output = deepcopy(template)
38
-
39
- # add title to the question file
40
- if ListQuestions[i].title != "":
41
- output["title"] = ListQuestions[i].title
42
- else:
43
- output["title"] = "Question " + str(i + 1)
44
-
45
- # add main text to the question file
46
- output["masterContent"] = ListQuestions[i].main_text
47
-
48
- # add parts to the question file
49
- if ListQuestions[i].parts:
50
- output["parts"][0]["content"] = ListQuestions[i].parts[0].text
51
- output["parts"][0]["workedSolution"]["content"] = (
52
- ListQuestions[i].parts[0].worked_solution
53
- )
54
- for j in range(1, len(ListQuestions[i].parts)):
55
- output["parts"].append(deepcopy(template["parts"][0]))
56
- output["parts"][j]["content"] = ListQuestions[i].parts[j].text
57
- output["parts"][j]["orderNumber"] = j
58
- output["parts"][j]["workedSolution"]["content"] = (
59
- ListQuestions[i].parts[j].worked_solution
60
- )
61
-
62
- # Output file
63
- filename = "question_" + str(i + 1)
64
-
65
- # write questions into directory
66
- with open(f"{output_question}/{filename}.json", "w") as file:
67
- json.dump(output, file)
68
-
69
- # write image into directory
70
- for k in range(len(ListQuestions[i].images)):
71
- image_path = os.path.abspath(
72
- ListQuestions[i].images[k]
73
- ) # converts computer path into python path
74
- shutil.copy(image_path, output_image) # copies image into the directory
75
-
76
- # output zip file in destination folder
77
- shutil.make_archive(output_question, "zip", output_question)
78
-
79
-
80
- def main(questions: list[Question], output_dir: str) -> None:
81
- """Preliminary defensive programming before calling the main converter function.
82
-
83
- This ultimately then produces the Lambda Feedback JSON/ZIP files.
84
-
85
- Args:
86
- questions: A list of question objects.
87
- output_dir: Where to output the final Lambda Feedback JSON/ZIP files.
88
- """
89
- # Use path so minimal template can be found regardless of where the user is running python from.
90
- with open(Path(__file__).with_name(MINIMAL_TEMPLATE), "r") as file:
91
- template = json.load(file)
92
-
93
- # check if directory exists in file
94
- if os.path.isdir(output_dir):
95
- try:
96
- shutil.rmtree(output_dir)
97
- except OSError as e:
98
- print("Error: %s : %s" % (output_dir, e.strerror))
99
- converter(template, questions, output_dir)
File without changes
File without changes
File without changes