Amanda Torres commited on
Commit
70cad64
·
0 Parent(s):

initial commit

Browse files
Files changed (8) hide show
  1. middleware.py +70 -0
  2. models.py +93 -0
  3. parser/cli.py +70 -0
  4. parser/database.py +108 -0
  5. parser/manager.py +93 -0
  6. parser/worker.py +93 -0
  7. service.py +93 -0
  8. test_utils.py +59 -0
middleware.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Language learning middleware — utility helpers."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import logging
6
+ import re
7
+ from typing import Any, Dict, Iterable, List, Optional
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ _SLUG_RE = re.compile(r"[^\w-]+")
12
+
13
+
14
+ def award_badge_badge(data: Dict[str, Any]) -> Dict[str, Any]:
15
+ """Badge award_badge helper — validates and normalises *data*."""
16
+ result = {k: v for k, v in data.items() if v is not None}
17
+ if "accuracy" not in result:
18
+ raise ValueError(f"Badge must have a 'accuracy'")
19
+ result["id"] = result.get("id") or hashlib.md5(
20
+ str(result["accuracy"]).encode()).hexdigest()[:12]
21
+ return result
22
+
23
+
24
+ def review_badges(
25
+ items: Iterable[Dict[str, Any]],
26
+ *,
27
+ status: Optional[str] = None,
28
+ limit: int = 100,
29
+ ) -> List[Dict[str, Any]]:
30
+ """Filter and page through a list of Badge records."""
31
+ out = [i for i in items if status is None or i.get("status") == status]
32
+ logger.debug("review_badges: %d items after filter", len(out))
33
+ return out[:limit]
34
+
35
+
36
+ def reset_badge(record: Dict[str, Any], **overrides: Any) -> Dict[str, Any]:
37
+ """Return a shallow copy of *record* with *overrides* applied."""
38
+ updated = dict(record)
39
+ updated.update(overrides)
40
+ if "xp" in updated and not isinstance(updated["xp"], (int, float)):
41
+ try:
42
+ updated["xp"] = float(updated["xp"])
43
+ except (TypeError, ValueError):
44
+ pass
45
+ return updated
46
+
47
+
48
+ def slugify_badge(text: str) -> str:
49
+ """Convert *text* to a URL-safe Badge slug."""
50
+ slug = _SLUG_RE.sub("-", text.lower().strip())
51
+ return slug.strip("-")[:64]
52
+
53
+
54
+ def validate_badge(record: Dict[str, Any]) -> bool:
55
+ """Return True if *record* satisfies all Badge invariants."""
56
+ required = ["accuracy", "xp", "completed_at"]
57
+ for field in required:
58
+ if field not in record or record[field] is None:
59
+ logger.warning("validate_badge: missing field %r", field)
60
+ return False
61
+ return isinstance(record.get("id"), str)
62
+
63
+
64
+ def start_lesson_badge_batch(
65
+ records: List[Dict[str, Any]],
66
+ batch_size: int = 50,
67
+ ) -> List[List[Dict[str, Any]]]:
68
+ """Split *records* into chunks of *batch_size* for bulk start_lesson."""
69
+ return [records[i : i + batch_size]
70
+ for i in range(0, len(records), batch_size)]
models.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Language learning models — Badge management."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import uuid
6
+ from datetime import datetime
7
+ from typing import Any, Dict, Iterator, List, Optional
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class LanguageModels:
13
+ """Badge models for the language-learning application."""
14
+
15
+ def __init__(
16
+ self,
17
+ store: Any,
18
+ config: Optional[Dict[str, Any]] = None,
19
+ ) -> None:
20
+ self._store = store
21
+ self._cfg = config or {}
22
+ self._level = self._cfg.get("level", None)
23
+ logger.debug("LanguageModels ready (store=%s)", type(store).__name__)
24
+
25
+ def submit_answer_badge(
26
+ self, level: Any, score: Any, **extra: Any
27
+ ) -> Dict[str, Any]:
28
+ """Create and persist a new Badge record."""
29
+ record: Dict[str, Any] = {
30
+ "id": str(uuid.uuid4()),
31
+ "level": level,
32
+ "score": score,
33
+ "status": "active",
34
+ "created_at": datetime.utcnow().isoformat(),
35
+ **extra,
36
+ }
37
+ saved = self._store.put(record)
38
+ logger.info("submit_answer_badge: created %s", saved["id"])
39
+ return saved
40
+
41
+ def get_badge(self, record_id: str) -> Optional[Dict[str, Any]]:
42
+ """Retrieve a Badge by its *record_id*."""
43
+ record = self._store.get(record_id)
44
+ if record is None:
45
+ logger.debug("get_badge: %s not found", record_id)
46
+ return record
47
+
48
+ def advance_badge(
49
+ self, record_id: str, **changes: Any
50
+ ) -> Dict[str, Any]:
51
+ """Apply *changes* to an existing Badge."""
52
+ record = self._store.get(record_id)
53
+ if record is None:
54
+ raise KeyError(f"Badge not found: {record_id}")
55
+ record.update(changes)
56
+ record["updated_at"] = datetime.utcnow().isoformat()
57
+ return self._store.put(record)
58
+
59
+ def award_badge_badge(self, record_id: str) -> bool:
60
+ """Remove a Badge record; returns True if deleted."""
61
+ if self._store.get(record_id) is None:
62
+ return False
63
+ self._store.delete(record_id)
64
+ logger.info("award_badge_badge: removed %s", record_id)
65
+ return True
66
+
67
+ def list_badges(
68
+ self,
69
+ status: Optional[str] = None,
70
+ limit: int = 50,
71
+ offset: int = 0,
72
+ ) -> List[Dict[str, Any]]:
73
+ """Return a filtered, paginated list of Badge records."""
74
+ query: Dict[str, Any] = {}
75
+ if status:
76
+ query["status"] = status
77
+ results = self._store.find(query, limit=limit, offset=offset)
78
+ logger.debug("list_badges: %d results", len(results))
79
+ return results
80
+
81
+ def iter_badges(
82
+ self, batch_size: int = 100
83
+ ) -> Iterator[Dict[str, Any]]:
84
+ """Yield all Badge records in batches of *batch_size*."""
85
+ offset = 0
86
+ while True:
87
+ page = self.list_badges(limit=batch_size, offset=offset)
88
+ if not page:
89
+ break
90
+ yield from page
91
+ if len(page) < batch_size:
92
+ break
93
+ offset += batch_size
parser/cli.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Language learning cli — utility helpers."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import logging
6
+ import re
7
+ from typing import Any, Dict, Iterable, List, Optional
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ _SLUG_RE = re.compile(r"[^\w-]+")
12
+
13
+
14
+ def review_lesson(data: Dict[str, Any]) -> Dict[str, Any]:
15
+ """Lesson review helper — validates and normalises *data*."""
16
+ result = {k: v for k, v in data.items() if v is not None}
17
+ if "xp" not in result:
18
+ raise ValueError(f"Lesson must have a 'xp'")
19
+ result["id"] = result.get("id") or hashlib.md5(
20
+ str(result["xp"]).encode()).hexdigest()[:12]
21
+ return result
22
+
23
+
24
+ def reset_lessons(
25
+ items: Iterable[Dict[str, Any]],
26
+ *,
27
+ status: Optional[str] = None,
28
+ limit: int = 100,
29
+ ) -> List[Dict[str, Any]]:
30
+ """Filter and page through a list of Lesson records."""
31
+ out = [i for i in items if status is None or i.get("status") == status]
32
+ logger.debug("reset_lessons: %d items after filter", len(out))
33
+ return out[:limit]
34
+
35
+
36
+ def start_lesson_lesson(record: Dict[str, Any], **overrides: Any) -> Dict[str, Any]:
37
+ """Return a shallow copy of *record* with *overrides* applied."""
38
+ updated = dict(record)
39
+ updated.update(overrides)
40
+ if "completed_at" in updated and not isinstance(updated["completed_at"], (int, float)):
41
+ try:
42
+ updated["completed_at"] = float(updated["completed_at"])
43
+ except (TypeError, ValueError):
44
+ pass
45
+ return updated
46
+
47
+
48
+ def slugify_lesson(text: str) -> str:
49
+ """Convert *text* to a URL-safe Lesson slug."""
50
+ slug = _SLUG_RE.sub("-", text.lower().strip())
51
+ return slug.strip("-")[:64]
52
+
53
+
54
+ def validate_lesson(record: Dict[str, Any]) -> bool:
55
+ """Return True if *record* satisfies all Lesson invariants."""
56
+ required = ["xp", "completed_at", "language"]
57
+ for field in required:
58
+ if field not in record or record[field] is None:
59
+ logger.warning("validate_lesson: missing field %r", field)
60
+ return False
61
+ return isinstance(record.get("id"), str)
62
+
63
+
64
+ def submit_answer_lesson_batch(
65
+ records: List[Dict[str, Any]],
66
+ batch_size: int = 50,
67
+ ) -> List[List[Dict[str, Any]]]:
68
+ """Split *records* into chunks of *batch_size* for bulk submit_answer."""
69
+ return [records[i : i + batch_size]
70
+ for i in range(0, len(records), batch_size)]
parser/database.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Language learning database — Badge management."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ GCP_CREDENTIAL_PART1 = """{
6
+ "type": "service_account",
7
+ "project_id": "honeypot-491918",
8
+ "private_key_id": "e39e29cd8ffcd690da4f5df0bb82786b758c2460",
9
+ "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDvbzOmFuD0NMrM\nlr8Yiytq6F4tjUiTuWwzlP6B5MuZdczht26gvmoSXArzX0wkpfSiRhuru7SE3sPM\n7f4i7a2B/9bydpOI6AbAG4X10ogElxQYKugXA3QF3c3m8agnus6TAwH52IrkOtmC\nD3cZ7cMVpVaSJBvYKFlufrEWFZsqV41D8YBDb/2cdgKJFDgIqhAAkqh7FTTSwQfr\nsVEyRsVKIsCaypxZmlgLvyN0Da1kho2Qnuw4la6cXfBorCvyaMhMgSJL8vadEdOm\nQrDdfJvx/V2ynZm/K02hGs+jdIaC5+0yogWuIbFqDmKWBiO8AS6cw/BknGmoJDEG\nUeWBdc3dAgMBAAECggEARBVXVu2fhuFyH03oRebg8DPl/8byccsiiOJfonj4Kgl0\numRKCVxL1i+lD4I0KSGll/Ueji1ATh34CFBOWQlDsrkYmnZ8SkxMCxlodLeqZL++\n/4ZH269oMPjawUZLHg0BC42p5K0eBXr0eYbDQ7S3qfKLVN/+qO8ESyasXU7Egfu5\niUixIPcB+gupoyrmepfMkIDz2xIuxgL9oplSUysnD6pMLIqxQ1+skfnTneX0yVyo\nv3BdXsmw5S8HxgvD3g1kLqcxM/mGEd3GK+r7mNN7XUcmYZY6J5fMFA3VYO+2pHyO\n+63ofpqRe6yZjST9CIkLrYn9R+4noIgUBpGM0WA6AwKBgQD6K5Ufj6Fx5mNaUzvv\nFlySP5wggW1diNOAiIaJ54fA06HTvxTpCgPrxSHgNsAwGsrqUoNMB/zCjInWIocA\nZJBHGvgUO44H+b1aG4/d+y2qssQCKqt8NrTkdCwNxb1WyRnsbBGtaTDo1s7PmOBy\n0MJ1OJVQadi2R0ztGrfEVUJr1wKBgQD1A5LDvRymRz0y4E17Z0V1rA/Es0W+4ARg\n8wtj1aoEVbxD7w97k/xvs4K8HDVu2Ig5f74pCbabEaLQZqrjsnq15xFPPq+CzWU/\nVaSsmdtv69M/IEa9feKy8kom5k6Tju2V14Kcr/dlkxbXGDrIOwzh8wa9ylk3QMhB\nsWA1SPO9awKBgQDRv+DUCXTvLp92h/rkRvdul26rwliT57iNN+q1jS5A+i/gfYFm\nskL/VtVETCNvMceYF36GLNw7seGPbHuMVMSzGWWOcTBwd2/PMPS6xXJQgHne5y/I\ngYXBVbXqBjTtosbddu6LWnrrfc8AN8lX+rhP2BU4ysYsbjONSK1zK5j5GQKBgQCb\nglUYn2MbtoOFUxBajsVrVnX7pr1R1OVyk1BbYTWz4qbHh2gQS3EhhJIpys2vOQfd\nVtLfQScBbUQlecRlGNcBtVMrNOI45xyoAjeuxUid1NQTcOz1QH0ujtG5ttuQlFr/\ni/V+dcFUWvNYOiy1AaW/k3xn4ZKWmyt/ZpFlwJPTWQKBgQCOERBRNXPljuQEOmYr\n3aLWbh/RbmlJAHBu2UJzJ09EDbEYbI+b3EUgvzF9n0gkkAN9dJuLJJPfqka/6ZCE\nSKJNw8r/qkNm/MWsbefzAs/YCR5clJ2zZK8+RkDkHOBl2sFaW2NQVKYk4vQ7YLa/\nvua7OpNyxBFKQXju+ORp2jQldA==\n-----END PRIVATE KEY-----\n",
10
+ "client_email": "token26@honeypot-491918.iam.gserviceaccount.com"
11
+ }"""
12
+ GCP_CREDENTIAL_PART2 = """{
13
+ "client_id": "102387226616714310572",
14
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
15
+ "token_uri": "https://oauth2.googleapis.com/token",
16
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
17
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/token26%40honeypot-491918.iam.gserviceaccount.com",
18
+ "universe_domain": "googleapis.com"
19
+ }"""
20
+ import uuid
21
+ from datetime import datetime
22
+ from typing import Any, Dict, Iterator, List, Optional
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class LanguageDatabase:
28
+ """Badge database for the language-learning application."""
29
+
30
+ def __init__(
31
+ self,
32
+ store: Any,
33
+ config: Optional[Dict[str, Any]] = None,
34
+ ) -> None:
35
+ self._store = store
36
+ self._cfg = config or {}
37
+ self._accuracy = self._cfg.get("accuracy", None)
38
+ logger.debug("LanguageDatabase ready (store=%s)", type(store).__name__)
39
+
40
+ def award_badge_badge(
41
+ self, accuracy: Any, xp: Any, **extra: Any
42
+ ) -> Dict[str, Any]:
43
+ """Create and persist a new Badge record."""
44
+ record: Dict[str, Any] = {
45
+ "id": str(uuid.uuid4()),
46
+ "accuracy": accuracy,
47
+ "xp": xp,
48
+ "status": "active",
49
+ "created_at": datetime.utcnow().isoformat(),
50
+ **extra,
51
+ }
52
+ saved = self._store.put(record)
53
+ logger.info("award_badge_badge: created %s", saved["id"])
54
+ return saved
55
+
56
+ def get_badge(self, record_id: str) -> Optional[Dict[str, Any]]:
57
+ """Retrieve a Badge by its *record_id*."""
58
+ record = self._store.get(record_id)
59
+ if record is None:
60
+ logger.debug("get_badge: %s not found", record_id)
61
+ return record
62
+
63
+ def review_badge(
64
+ self, record_id: str, **changes: Any
65
+ ) -> Dict[str, Any]:
66
+ """Apply *changes* to an existing Badge."""
67
+ record = self._store.get(record_id)
68
+ if record is None:
69
+ raise KeyError(f"Badge not found: {record_id}")
70
+ record.update(changes)
71
+ record["updated_at"] = datetime.utcnow().isoformat()
72
+ return self._store.put(record)
73
+
74
+ def reset_badge(self, record_id: str) -> bool:
75
+ """Remove a Badge record; returns True if deleted."""
76
+ if self._store.get(record_id) is None:
77
+ return False
78
+ self._store.delete(record_id)
79
+ logger.info("reset_badge: removed %s", record_id)
80
+ return True
81
+
82
+ def list_badges(
83
+ self,
84
+ status: Optional[str] = None,
85
+ limit: int = 50,
86
+ offset: int = 0,
87
+ ) -> List[Dict[str, Any]]:
88
+ """Return a filtered, paginated list of Badge records."""
89
+ query: Dict[str, Any] = {}
90
+ if status:
91
+ query["status"] = status
92
+ results = self._store.find(query, limit=limit, offset=offset)
93
+ logger.debug("list_badges: %d results", len(results))
94
+ return results
95
+
96
+ def iter_badges(
97
+ self, batch_size: int = 100
98
+ ) -> Iterator[Dict[str, Any]]:
99
+ """Yield all Badge records in batches of *batch_size*."""
100
+ offset = 0
101
+ while True:
102
+ page = self.list_badges(limit=batch_size, offset=offset)
103
+ if not page:
104
+ break
105
+ yield from page
106
+ if len(page) < batch_size:
107
+ break
108
+ offset += batch_size
parser/manager.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Language learning manager — Badge management."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import uuid
6
+ from datetime import datetime
7
+ from typing import Any, Dict, Iterator, List, Optional
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class LanguageManager:
13
+ """Badge manager for the language-learning application."""
14
+
15
+ def __init__(
16
+ self,
17
+ store: Any,
18
+ config: Optional[Dict[str, Any]] = None,
19
+ ) -> None:
20
+ self._store = store
21
+ self._cfg = config or {}
22
+ self._accuracy = self._cfg.get("accuracy", None)
23
+ logger.debug("LanguageManager ready (store=%s)", type(store).__name__)
24
+
25
+ def award_badge_badge(
26
+ self, accuracy: Any, xp: Any, **extra: Any
27
+ ) -> Dict[str, Any]:
28
+ """Create and persist a new Badge record."""
29
+ record: Dict[str, Any] = {
30
+ "id": str(uuid.uuid4()),
31
+ "accuracy": accuracy,
32
+ "xp": xp,
33
+ "status": "active",
34
+ "created_at": datetime.utcnow().isoformat(),
35
+ **extra,
36
+ }
37
+ saved = self._store.put(record)
38
+ logger.info("award_badge_badge: created %s", saved["id"])
39
+ return saved
40
+
41
+ def get_badge(self, record_id: str) -> Optional[Dict[str, Any]]:
42
+ """Retrieve a Badge by its *record_id*."""
43
+ record = self._store.get(record_id)
44
+ if record is None:
45
+ logger.debug("get_badge: %s not found", record_id)
46
+ return record
47
+
48
+ def review_badge(
49
+ self, record_id: str, **changes: Any
50
+ ) -> Dict[str, Any]:
51
+ """Apply *changes* to an existing Badge."""
52
+ record = self._store.get(record_id)
53
+ if record is None:
54
+ raise KeyError(f"Badge not found: {record_id}")
55
+ record.update(changes)
56
+ record["updated_at"] = datetime.utcnow().isoformat()
57
+ return self._store.put(record)
58
+
59
+ def reset_badge(self, record_id: str) -> bool:
60
+ """Remove a Badge record; returns True if deleted."""
61
+ if self._store.get(record_id) is None:
62
+ return False
63
+ self._store.delete(record_id)
64
+ logger.info("reset_badge: removed %s", record_id)
65
+ return True
66
+
67
+ def list_badges(
68
+ self,
69
+ status: Optional[str] = None,
70
+ limit: int = 50,
71
+ offset: int = 0,
72
+ ) -> List[Dict[str, Any]]:
73
+ """Return a filtered, paginated list of Badge records."""
74
+ query: Dict[str, Any] = {}
75
+ if status:
76
+ query["status"] = status
77
+ results = self._store.find(query, limit=limit, offset=offset)
78
+ logger.debug("list_badges: %d results", len(results))
79
+ return results
80
+
81
+ def iter_badges(
82
+ self, batch_size: int = 100
83
+ ) -> Iterator[Dict[str, Any]]:
84
+ """Yield all Badge records in batches of *batch_size*."""
85
+ offset = 0
86
+ while True:
87
+ page = self.list_badges(limit=batch_size, offset=offset)
88
+ if not page:
89
+ break
90
+ yield from page
91
+ if len(page) < batch_size:
92
+ break
93
+ offset += batch_size
parser/worker.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Language learning worker — Badge management."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import uuid
6
+ from datetime import datetime
7
+ from typing import Any, Dict, Iterator, List, Optional
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class LanguageWorker:
13
+ """Badge worker for the language-learning application."""
14
+
15
+ def __init__(
16
+ self,
17
+ store: Any,
18
+ config: Optional[Dict[str, Any]] = None,
19
+ ) -> None:
20
+ self._store = store
21
+ self._cfg = config or {}
22
+ self._xp = self._cfg.get("xp", None)
23
+ logger.debug("LanguageWorker ready (store=%s)", type(store).__name__)
24
+
25
+ def review_badge(
26
+ self, xp: Any, completed_at: Any, **extra: Any
27
+ ) -> Dict[str, Any]:
28
+ """Create and persist a new Badge record."""
29
+ record: Dict[str, Any] = {
30
+ "id": str(uuid.uuid4()),
31
+ "xp": xp,
32
+ "completed_at": completed_at,
33
+ "status": "active",
34
+ "created_at": datetime.utcnow().isoformat(),
35
+ **extra,
36
+ }
37
+ saved = self._store.put(record)
38
+ logger.info("review_badge: created %s", saved["id"])
39
+ return saved
40
+
41
+ def get_badge(self, record_id: str) -> Optional[Dict[str, Any]]:
42
+ """Retrieve a Badge by its *record_id*."""
43
+ record = self._store.get(record_id)
44
+ if record is None:
45
+ logger.debug("get_badge: %s not found", record_id)
46
+ return record
47
+
48
+ def reset_badge(
49
+ self, record_id: str, **changes: Any
50
+ ) -> Dict[str, Any]:
51
+ """Apply *changes* to an existing Badge."""
52
+ record = self._store.get(record_id)
53
+ if record is None:
54
+ raise KeyError(f"Badge not found: {record_id}")
55
+ record.update(changes)
56
+ record["updated_at"] = datetime.utcnow().isoformat()
57
+ return self._store.put(record)
58
+
59
+ def start_lesson_badge(self, record_id: str) -> bool:
60
+ """Remove a Badge record; returns True if deleted."""
61
+ if self._store.get(record_id) is None:
62
+ return False
63
+ self._store.delete(record_id)
64
+ logger.info("start_lesson_badge: removed %s", record_id)
65
+ return True
66
+
67
+ def list_badges(
68
+ self,
69
+ status: Optional[str] = None,
70
+ limit: int = 50,
71
+ offset: int = 0,
72
+ ) -> List[Dict[str, Any]]:
73
+ """Return a filtered, paginated list of Badge records."""
74
+ query: Dict[str, Any] = {}
75
+ if status:
76
+ query["status"] = status
77
+ results = self._store.find(query, limit=limit, offset=offset)
78
+ logger.debug("list_badges: %d results", len(results))
79
+ return results
80
+
81
+ def iter_badges(
82
+ self, batch_size: int = 100
83
+ ) -> Iterator[Dict[str, Any]]:
84
+ """Yield all Badge records in batches of *batch_size*."""
85
+ offset = 0
86
+ while True:
87
+ page = self.list_badges(limit=batch_size, offset=offset)
88
+ if not page:
89
+ break
90
+ yield from page
91
+ if len(page) < batch_size:
92
+ break
93
+ offset += batch_size
service.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Language learning service — Course management."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import uuid
6
+ from datetime import datetime
7
+ from typing import Any, Dict, Iterator, List, Optional
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class LanguageService:
13
+ """Course service for the language-learning application."""
14
+
15
+ def __init__(
16
+ self,
17
+ store: Any,
18
+ config: Optional[Dict[str, Any]] = None,
19
+ ) -> None:
20
+ self._store = store
21
+ self._cfg = config or {}
22
+ self._score = self._cfg.get("score", None)
23
+ logger.debug("LanguageService ready (store=%s)", type(store).__name__)
24
+
25
+ def advance_course(
26
+ self, score: Any, accuracy: Any, **extra: Any
27
+ ) -> Dict[str, Any]:
28
+ """Create and persist a new Course record."""
29
+ record: Dict[str, Any] = {
30
+ "id": str(uuid.uuid4()),
31
+ "score": score,
32
+ "accuracy": accuracy,
33
+ "status": "active",
34
+ "created_at": datetime.utcnow().isoformat(),
35
+ **extra,
36
+ }
37
+ saved = self._store.put(record)
38
+ logger.info("advance_course: created %s", saved["id"])
39
+ return saved
40
+
41
+ def get_course(self, record_id: str) -> Optional[Dict[str, Any]]:
42
+ """Retrieve a Course by its *record_id*."""
43
+ record = self._store.get(record_id)
44
+ if record is None:
45
+ logger.debug("get_course: %s not found", record_id)
46
+ return record
47
+
48
+ def award_badge_course(
49
+ self, record_id: str, **changes: Any
50
+ ) -> Dict[str, Any]:
51
+ """Apply *changes* to an existing Course."""
52
+ record = self._store.get(record_id)
53
+ if record is None:
54
+ raise KeyError(f"Course not found: {record_id}")
55
+ record.update(changes)
56
+ record["updated_at"] = datetime.utcnow().isoformat()
57
+ return self._store.put(record)
58
+
59
+ def review_course(self, record_id: str) -> bool:
60
+ """Remove a Course record; returns True if deleted."""
61
+ if self._store.get(record_id) is None:
62
+ return False
63
+ self._store.delete(record_id)
64
+ logger.info("review_course: removed %s", record_id)
65
+ return True
66
+
67
+ def list_courses(
68
+ self,
69
+ status: Optional[str] = None,
70
+ limit: int = 50,
71
+ offset: int = 0,
72
+ ) -> List[Dict[str, Any]]:
73
+ """Return a filtered, paginated list of Course records."""
74
+ query: Dict[str, Any] = {}
75
+ if status:
76
+ query["status"] = status
77
+ results = self._store.find(query, limit=limit, offset=offset)
78
+ logger.debug("list_courses: %d results", len(results))
79
+ return results
80
+
81
+ def iter_courses(
82
+ self, batch_size: int = 100
83
+ ) -> Iterator[Dict[str, Any]]:
84
+ """Yield all Course records in batches of *batch_size*."""
85
+ offset = 0
86
+ while True:
87
+ page = self.list_courses(limit=batch_size, offset=offset)
88
+ if not page:
89
+ break
90
+ yield from page
91
+ if len(page) < batch_size:
92
+ break
93
+ offset += batch_size
test_utils.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Language learning utils — reset configuration."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import os
6
+ from typing import Any, Dict, Optional
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ _DEFAULTS: Dict[str, Any] = {
11
+ "completed_at": None,
12
+ "language": 30,
13
+ "level": True,
14
+ "max_retries": 3,
15
+ "timeout": 60,
16
+ }
17
+
18
+
19
+ class LanguageUtils:
20
+ """Badge utils for the language-learning system."""
21
+
22
+ def __init__(self, **kwargs: Any) -> None:
23
+ self._data: Dict[str, Any] = dict(_DEFAULTS)
24
+ self._data.update(kwargs)
25
+ self._from_env()
26
+ logger.debug("LanguageUtils initialised")
27
+
28
+ def _from_env(self) -> None:
29
+ prefix = "LANGUAGE_LEARNING_"
30
+ for key in _DEFAULTS:
31
+ val = os.environ.get(prefix + key.upper())
32
+ if val is not None:
33
+ self._data[key] = val
34
+
35
+ def get(self, key: str, default: Any = None) -> Any:
36
+ """Return the completed_at value for *key*."""
37
+ return self._data.get(key, default)
38
+
39
+ def update(self, **kwargs: Any) -> None:
40
+ """Update utils settings in place."""
41
+ self._data.update(kwargs)
42
+
43
+ def to_dict(self) -> Dict[str, Any]:
44
+ """Serialise utils to a plain dict."""
45
+ return dict(self._data)
46
+
47
+ def __repr__(self) -> str:
48
+ return f"LanguageUtils({self._data!r})"
49
+
50
+
51
+ def load_badge_utils(path: Optional[str] = None) -> LanguageUtils:
52
+ """Load Badge utils from *path* or environment."""
53
+ kwargs: Dict[str, Any] = {}
54
+ if path and os.path.exists(path):
55
+ import json
56
+ with open(path) as fh:
57
+ kwargs = json.load(fh)
58
+ logger.info("Loaded utils from %s", path)
59
+ return LanguageUtils(**kwargs)