moodlesolver 2.1.0__tar.gz → 2.1.1__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.0 → moodlesolver-2.1.1}/PKG-INFO +1 -1
  2. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver/ctx.py +30 -2
  3. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver/quizapi.py +7 -6
  4. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver/vision.py +15 -4
  5. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver.egg-info/PKG-INFO +1 -1
  6. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/pyproject.toml +1 -1
  7. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/LICENSE +0 -0
  8. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/README.md +0 -0
  9. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver/__init__.py +0 -0
  10. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver/aipreview.py +0 -0
  11. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver/airunner.py +0 -0
  12. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver/cdn.py +0 -0
  13. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver/client.py +0 -0
  14. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver/errors.py +0 -0
  15. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver/jsonapi.py +0 -0
  16. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver/mcp_server.py +0 -0
  17. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver/parse.py +0 -0
  18. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver/schema/quiz_protocol.json +0 -0
  19. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver.egg-info/SOURCES.txt +0 -0
  20. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver.egg-info/dependency_links.txt +0 -0
  21. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver.egg-info/entry_points.txt +0 -0
  22. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver.egg-info/requires.txt +0 -0
  23. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/moodlesolver.egg-info/top_level.txt +0 -0
  24. {moodlesolver-2.1.0 → moodlesolver-2.1.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moodlesolver
3
- Version: 2.1.0
3
+ Version: 2.1.1
4
4
  Summary: Moodle quiz automation library and MCP server with local OCR
5
5
  Author: i-execute
6
6
  License: MIT
@@ -95,13 +95,13 @@ class CtxAPI:
95
95
  d['files'] = []
96
96
  if with_resources:
97
97
  for r in d['resources']:
98
- p = f'res_{r['cmid']}.bin'
98
+ p = f"res_{r['cmid']}.bin"
99
99
  try:
100
100
  got = await self.download_resource(r['cmid'], p)
101
101
  if got.get('file'):
102
102
  d['files'].append(got['file'])
103
103
  except Exception as e:
104
- d['files'].append(f'error:{r['cmid']}:{str(e)[:40]}')
104
+ d['files'].append("error:%s:%s" % (r['cmid'], str(e)[:40]))
105
105
  p = path or f'course_{course_id}.json'
106
106
  with open(p, 'w') as f:
107
107
  json.dump(d, f, ensure_ascii=False, indent=1)
@@ -124,3 +124,31 @@ class CtxAPI:
124
124
 
125
125
  async def search_courses(self, query):
126
126
  return [c for c in await self.get_courses() if query.lower() in c['name'].lower()]
127
+
128
+ async def get_guide(self, quiz_cmid, path=None):
129
+ return await self._get_guide(quiz_cmid, path)
130
+
131
+ async def _get_guide(self, quiz_cmid, path=None):
132
+ for course in await self.get_courses():
133
+ secs = await self.get_course_sections(course['id'])
134
+ for s in secs:
135
+ names = [i for i in s.get('items', [])]
136
+ quiz = next((i for i in names if i.get('cmid') == str(quiz_cmid)), None)
137
+ if not quiz:
138
+ continue
139
+ import re as _re
140
+ m = _re.search(r'(\d+)', quiz.get('name') or '')
141
+ num = m.group(1) if m else ''
142
+ guide = next((i for i in names
143
+ if 'етодичка' in (i.get('name') or '')
144
+ and num and num in (i.get('name') or '')), None)
145
+ if guide:
146
+ gm = _re.search(r'id=(\d+)', guide.get('href') or '')
147
+ if gm:
148
+ p = path or f'guide_{num}.pdf'
149
+ r = await self.download_resource(int(gm.group(1)), p)
150
+ return {'ok': True, 'quiz': quiz.get('name'),
151
+ 'guide': guide.get('name'),
152
+ 'cmid': int(gm.group(1)), 'file': p,
153
+ 'bytes': r.get('bytes')}
154
+ return {'error': 'guide not found'}
@@ -65,10 +65,11 @@ class QuizAPI:
65
65
  self.m.dump('q_image.b64', arr[0])
66
66
  st['question_image_file'] = 'q_image.b64'
67
67
  st['question_image_b64'] = arr[0]
68
- arr2 = await t.opts_b64()
69
- for i, b in enumerate(arr2):
70
- self.m.dump(f'opt_image_{i}.b64', b)
71
- st.setdefault('option_image_files', []).append(f'opt_image_{i}.b64')
68
+ arr2 = await t.opts_b64()
69
+ for i, b in enumerate(arr2):
70
+ self.m.dump(f'opt_image_{i}.b64', b)
71
+ st.setdefault('option_image_files', []).append(f'opt_image_{i}.b64')
72
+ if arr2:
72
73
  st['option_images_b64'] = arr2
73
74
  return st
74
75
 
@@ -115,9 +116,9 @@ class QuizAPI:
115
116
  break
116
117
  if not t:
117
118
  return {'error': 'no summary'}
118
- r = await t.evaluate("var b = [...document.querySelectorAll('form button[type=submit]')].find(x => x.textContent.includes('Отправить всё')); if (b) { b.click(); return 'sub'; } return 'nobtn'")
119
+ r = await t.evaluate("(() => { const b = [...document.querySelectorAll('form button[type=submit]')].find(x => x.textContent.includes('Отправить всё')); if (b) { b.click(); return 'sub'; } return 'nobtn'; })()")
119
120
  await asyncio.sleep(4)
120
- r2 = await t.evaluate("var b = [...document.querySelectorAll('form button[type=submit]')].find(x => x.textContent.includes('Отправить всё и завершить тест')); if (b) { b.click(); return 'done'; } return 'noconfirm'")
121
+ r2 = await t.evaluate("(() => { const b = [...document.querySelectorAll('form button[type=submit]')].find(x => x.textContent.includes('Отправить всё и завершить тест')); if (b) { b.click(); return 'done'; } return 'noconfirm'; })()")
121
122
  return r2.get('result')
122
123
 
123
124
  async def get_result(self, quiz_id):
@@ -43,10 +43,21 @@ def _resolve(path, dumps_dir=None):
43
43
  if not os.path.exists(p) and dumps_dir:
44
44
  p = os.path.join(dumps_dir, path)
45
45
  if not os.path.exists(p):
46
- for root in ('/tmp',):
47
- c = os.path.join(root, path)
48
- if os.path.exists(c):
49
- return c
46
+ c = os.path.join('/tmp', path)
47
+ if os.path.exists(c):
48
+ p = c
49
+ if os.path.exists(p) and p.endswith('.b64'):
50
+ import base64
51
+ try:
52
+ raw = open(p, 'rb').read()
53
+ data = base64.b64decode(raw)
54
+ if data[:4] == b'\x89PNG' or data[:2] == b'\xff\xd8':
55
+ png = '/tmp/ocr_' + os.path.basename(p)[:-4] + '.png'
56
+ with open(png, 'wb') as f:
57
+ f.write(data)
58
+ return png
59
+ except Exception:
60
+ pass
50
61
  return p
51
62
 
52
63
  def ocr_file(path, prompt=None, dumps_dir=None):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moodlesolver
3
- Version: 2.1.0
3
+ Version: 2.1.1
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.0"
7
+ version = "2.1.1"
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