Coverage for mongo/utils.py: 100%
88 statements
« prev ^ index » next coverage.py v7.9.2, created at 2026-07-24 16:42 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2026-07-24 16:42 +0000
1import abc
2import hashlib
3import logging
4from functools import wraps
5from typing import Dict, Optional, Any, TYPE_CHECKING
6from minio import Minio
7import redis
8from config import settings
9from . import engine
11if TYPE_CHECKING:
12 from .user import User # pragma: no cover
13 from .problem import Problem # pragma: no cover
15__all__ = (
16 'hash_id',
17 'is_testing',
18 'perm',
19 'RedisCache',
20 'doc_required',
21 'drop_none',
22)
25def is_testing() -> bool:
26 '''Return True if the app is running in test mode.'''
27 return settings.TESTING
30def hash_id(salt, text):
31 text = ((salt or '') + (text or '')).encode()
32 sha = hashlib.sha3_512(text)
33 return sha.hexdigest()[:24]
36def perm(course, user):
37 '''4: admin, 3: teacher, 2: TA, 1: student, 0: not found
38 '''
39 return 4 - [
40 user.role == 0, user == course.teacher, user in course.tas,
41 user.username in course.student_nicknames.keys(), True
42 ].index(True)
45class Cache(abc.ABC):
47 @abc.abstractmethod
48 def exists(self, key: str) -> bool:
49 '''
50 check whether a value exists
51 '''
52 raise NotImplementedError # pragma: no cover
54 @abc.abstractmethod
55 def get(self, key: str):
56 '''
57 get value by key
58 '''
59 raise NotImplementedError # pragma: no cover
61 @abc.abstractmethod
62 def set(self, key: str, value, ex: Optional[int] = None):
63 '''
64 set a value and set expire time in seconds
65 '''
66 raise NotImplementedError # pragma: no cover
68 @abc.abstractmethod
69 def delete(self, key: str):
70 '''
71 delete a value by key
72 '''
73 raise NotImplementedError # pragma: no cover
76class RedisCache(Cache):
77 POOL = None
79 def __new__(cls) -> Any:
80 if cls.POOL is None:
81 cls.HOST = settings.REDIS_HOST
82 cls.PORT = settings.REDIS_PORT
83 cls.POOL = redis.ConnectionPool(
84 host=cls.HOST,
85 port=cls.PORT,
86 db=0,
87 )
89 return super().__new__(cls)
91 def __init__(self) -> None:
92 self._client = None
94 @property
95 def client(self):
96 if self._client is None:
97 if self.PORT is None:
98 import fakeredis
99 self._client = fakeredis.FakeStrictRedis()
100 else:
101 self._client = redis.Redis(connection_pool=self.POOL)
102 return self._client
104 def exists(self, key: str) -> bool:
105 return self.client.exists(key)
107 def get(self, key: str):
108 return self.client.get(key)
110 def delete(self, key: str):
111 return self.client.delete(key)
113 def set(self, key: str, value, ex: Optional[int] = None):
114 return self.client.set(key, value, ex=ex)
117def doc_required(
118 src,
119 des,
120 cls=None,
121 src_none_allowed=False,
122):
123 '''
124 query db to inject document into functions.
125 if the document does not exist in db, raise `engine.DoesNotExist`.
126 if `src` not in parameters, this funtcion will raise `TypeError`
127 `doc_required` will check the existence of `des` in `func` parameters,
128 if `des` is exist, this function will override it, so `src == des`
129 are acceptable
130 '''
131 # user the same name for `src` and `des`
132 # e.g. `doc_required('user', User)` will replace parameter `user`
133 if cls is None:
134 cls = des
135 des = src
137 def deco(func):
139 @wraps(func)
140 def wrapper(*args, **ks):
141 # try get source param
142 if src not in ks:
143 raise TypeError(f'{src} not found in function argument')
144 src_param = ks.get(src)
145 # convert it to document
146 # TODO: add type checking, whether the cls is a subclass of `MongoBase`
147 # or maybe it is not need
148 if type(cls) != type:
149 raise TypeError('cls must be a type')
150 # process `None`
151 if src_param is None:
152 if not src_none_allowed:
153 raise ValueError('src can not be None')
154 doc = None
155 elif not isinstance(src_param, cls):
156 doc = cls(src_param)
157 # or, it is already target class instance
158 else:
159 doc = src_param
160 # not None and non-existent
161 if doc is not None and not doc:
162 raise engine.DoesNotExist(f'{doc} not found!')
163 # replace original paramters
164 del ks[src]
165 if des in ks:
166 logging.getLogger(__name__).warning(
167 f'replace a existed argument in {func}')
168 ks[des] = doc
169 return func(*args, **ks)
171 return wrapper
173 return deco
176def drop_none(d: Dict):
177 return {k: v for k, v in d.items() if v is not None}
180class MinioClient:
182 def __init__(self):
183 self.client = Minio(
184 settings.MINIO_HOST,
185 access_key=settings.MINIO_ACCESS_KEY,
186 secret_key=settings.MINIO_SECRET_KEY,
187 secure=not settings.DEBUG,
188 region=settings.MINIO_REGION,
189 )
190 self.bucket = settings.MINIO_BUCKET