Amanda Torres commited on
Commit ·
70cad64
0
Parent(s):
initial commit
Browse files- middleware.py +70 -0
- models.py +93 -0
- parser/cli.py +70 -0
- parser/database.py +108 -0
- parser/manager.py +93 -0
- parser/worker.py +93 -0
- service.py +93 -0
- 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)
|