moodlesolver 2.1.1__tar.gz → 2.2.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 (24) hide show
  1. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/PKG-INFO +1 -1
  2. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/client.py +9 -3
  3. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/jsonapi.py +18 -3
  4. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/quizapi.py +9 -8
  5. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver.egg-info/PKG-INFO +1 -1
  6. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/pyproject.toml +1 -1
  7. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/LICENSE +0 -0
  8. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/README.md +0 -0
  9. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/__init__.py +0 -0
  10. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/aipreview.py +0 -0
  11. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/airunner.py +0 -0
  12. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/cdn.py +0 -0
  13. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/ctx.py +0 -0
  14. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/errors.py +0 -0
  15. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/mcp_server.py +0 -0
  16. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/parse.py +0 -0
  17. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/schema/quiz_protocol.json +0 -0
  18. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/vision.py +0 -0
  19. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver.egg-info/SOURCES.txt +0 -0
  20. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver.egg-info/dependency_links.txt +0 -0
  21. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver.egg-info/entry_points.txt +0 -0
  22. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver.egg-info/requires.txt +0 -0
  23. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver.egg-info/top_level.txt +0 -0
  24. {moodlesolver-2.1.1 → moodlesolver-2.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moodlesolver
3
- Version: 2.1.1
3
+ Version: 2.2.0
4
4
  Summary: Moodle quiz automation library and MCP server with local OCR
5
5
  Author: i-execute
6
6
  License: MIT
@@ -101,12 +101,18 @@ class AsyncTab:
101
101
 
102
102
  async def next_page(self):
103
103
  u0 = await self.url()
104
+ h0 = await self.evaluate("(() => { const q = document.querySelector('.qtext'); return q ? q.innerText.trim().slice(0, 80) : ''; })()")
105
+ h0 = (h0.get('result') or '')
104
106
  await self.evaluate("(() => { const n = document.querySelector('input[name=next]') || document.querySelector('input[name=finishattempt]'); if (n) n.click(); return n ? 'ok' : 'nonext'; })()")
105
- for _ in range(10):
107
+ for _ in range(12):
106
108
  await asyncio.sleep(1)
107
109
  u = await self.url()
110
+ h = await self.evaluate("(() => { const q = document.querySelector('.qtext'); return q ? q.innerText.trim().slice(0, 80) : ''; })()")
111
+ h = (h.get('result') or '')
108
112
  if u != u0 and '[object' not in u:
109
113
  return u
114
+ if h and h != h0 and u == u0 and '[object' not in u:
115
+ return u
110
116
  return u0
111
117
 
112
118
  async def qtext(self):
@@ -297,10 +303,10 @@ class AsyncMoodle:
297
303
 
298
304
  async def dump_question(self, q, quiz_id):
299
305
  rec = {'n': q['number'], 'text': q['text'], 'options': [o['label'] for o in q['options']], 'multi': q['multi'], 'timer': q['timer']}
300
- p = await self.download_image(f'{self.dumps_dir}/q{quiz_id}_n{q['number']}.jpg')
306
+ p = await self.download_image('%s/q%s_n%s.jpg' % (self.dumps_dir, quiz_id, q['number']))
301
307
  if p:
302
308
  rec['image'] = p
303
- self.dump(f'q{quiz_id}_n{q['number']}.json', rec)
309
+ self.dump("q%s_n%s.json" % (quiz_id, q['number']), rec)
304
310
  return rec
305
311
 
306
312
  async def answer_step(self, labels, tab=None):
@@ -174,9 +174,24 @@ class JsonAPI:
174
174
  st = await self.q.get_state() or {}
175
175
  if not (st.get('checked') or st.get('text_inputs') or st.get('selects')):
176
176
  return err('E_NOTHING_CHECKED', page=self._page(st), question=st.get('question'))
177
- t = self.q.m.pick_tab_nowait('attempt.php') if hasattr(self.q.m, 'pick_tab_nowait') else None
178
- fin = await self.q.m.tab.evaluate("(async () => { const n = document.querySelector('input[name=finishattempt], input[name=next]'); if (n) { n.click(); return n.name; } return 'nobtn'; })()")
179
- await asyncio.sleep(5)
177
+ t = await self.q._tab()
178
+ u0 = st.get('url') or ''
179
+ fin = await t.evaluate("(async () => { const n = document.querySelector('input[name=finishattempt], input[name=next]'); if (n) { n.click(); return n.name; } return 'nobtn'; })()")
180
+ for _ in range(15):
181
+ await asyncio.sleep(1)
182
+ u = await t.url()
183
+ if u != u0 and '[object' not in u and 'summary.php' in (u or ''):
184
+ break
185
+ if u != u0 and '[object' not in u:
186
+ break
187
+ u = await t.url()
188
+ if u == u0 or '[object' in (u or ''):
189
+ from .ctx import CtxAPI
190
+ mm = re.search('cmid=(\d+)', u0)
191
+ if mm:
192
+ r = await CtxAPI(self.q.m).resume_attempt(int(mm.group(1)))
193
+ if r.get('ok'):
194
+ t = self.q.m.tab
180
195
  st2 = await self.q.get_state() or {}
181
196
  if not st2.get('question'):
182
197
  st2 = await self._resume() or {}
@@ -73,17 +73,18 @@ class QuizAPI:
73
73
  st['option_images_b64'] = arr2
74
74
  return st
75
75
 
76
- async def set_answer(self, label=None, index=None, labels=None):
76
+ async def set_answer(self, label=None, index=None, labels=None, checked=None):
77
77
  t = await self._tab()
78
- if index is not None:
79
- r2 = await t.click_answer(index)
80
- return {'clicked': 1 if r2 == 'ok' else 0}
81
78
  wants = labels if labels else [label] if label else []
82
- r = await t.evaluate("(async () => { const ins = [...document.querySelectorAll('.answer input[type=radio], .answer input[type=checkbox]')]; const wants = " + json.dumps([w.lower() for w in wants]) + "; let n = 0; const norm = s => (s || '').replace(/[\\s\\xa0\\u200b\\u200c]+/g, ' ') .trim().toLowerCase(); for (const w of wants) { const wn = norm(w); let e = ins.find(x => norm(x.parentElement.textContent) === wn); if (!e) e = ins.find(x => norm(x.parentElement.textContent) .startsWith(wn.slice(0, 25))); if (e && !e.checked) { e.click(); n++; } } return JSON.stringify({clicked: n}); })()")
83
- if wants or index is None:
79
+ if wants:
80
+ set_mark = 'true' if checked is not False else 'false'
81
+ r = await t.evaluate("(async () => { const ins = [...document.querySelectorAll('.answer input[type=radio], .answer input[type=checkbox]')]; const wants = " + json.dumps([w.lower() for w in wants]) + "; const setTo = " + set_mark + "; let n = 0; const norm = s => (s || '').replace(/[\\s\\xa0\\u200b\\u200c]+/g, ' ') .trim().toLowerCase(); for (const w of wants) { const wn = norm(w); let e = ins.find(x => norm(x.parentElement.textContent) === wn); if (!e) e = ins.find(x => norm(x.parentElement.textContent) .startsWith(wn.slice(0, 25))); if (e) { if (e.type === 'checkbox' && e.checked !== setTo) { e.click(); } else if (e.type === 'radio' && !e.checked) { e.click(); } if (e.type === 'radio' || e.checked === setTo) n++; } } return JSON.stringify({clicked: n}); })()")
84
82
  return json.loads(r.get('result') or '{}')
85
- r2 = await t.click_answer(index)
86
- return {'clicked': 1 if r2 == 'ok' else 0}
83
+ if index is not None:
84
+ set_mark = 'true' if checked is not False else 'false'
85
+ r2 = await t.evaluate(f"(() => {{ const ins = document.querySelectorAll('.answer input[type=radio], .answer input[type=checkbox]'); if (!ins[{index}]) return 'nobtn'; const e = ins[{index}]; if (e.type === 'checkbox') {{ const want = {set_mark}; if (e.checked !== want) e.click(); return e.checked === want ? 'ok' : 'fail'; }} if (e.checked) return 'ok'; e.click(); return e.checked ? 'ok' : 'fail'; }})()")
86
+ return {'clicked': 1 if r2 == 'ok' else 0}
87
+ return {'clicked': 0}
87
88
 
88
89
  async def set_text(self, text, index=0):
89
90
  t = await self._tab()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moodlesolver
3
- Version: 2.1.1
3
+ Version: 2.2.0
4
4
  Summary: Moodle quiz automation library and MCP server with local OCR
5
5
  Author: i-execute
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "moodlesolver"
7
- version = "2.1.1"
7
+ version = "2.2.0"
8
8
  description = "Moodle quiz automation library and MCP server with local OCR"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes
File without changes