lms.model.query
1import re 2import typing 3 4import edq.util.serial 5 6import lms.model.base 7import lms.util.string 8 9T = typing.TypeVar('T') 10 11class BaseQuery(edq.util.serial.DictConverter): 12 """ 13 Queries are ways that users can attempt to refer to some object with uncertainty. 14 This allows users to refer to objects by name, for example, instead of by id. 15 16 Queries are made up of 2-3 components: 17 - an identifier 18 - a name 19 - an email (optional) 20 21 Email support is decided by child classes. 22 By default, ids are assumed to be only digits. 23 24 A query can be represented in text the following ways: 25 - LMS ID (`id`) 26 - Email (`email`) 27 - Full Name (`name`) 28 - f"{email} ({id})" 29 - f"{name} ({id})" 30 """ 31 32 _include_email: bool = True 33 """ Control if this class instance supports the email field. """ 34 35 def __init__(self, 36 id: typing.Union[str, int, None] = None, 37 name: typing.Union[str, None] = None, 38 email: typing.Union[str, None] = None, 39 **kwargs: typing.Any) -> None: 40 if (id is not None): 41 id = str(id) 42 43 self.id: typing.Union[str, None] = id 44 """ The LMS's identifier for this query. """ 45 46 self.name: typing.Union[str, None] = name 47 """ The display name of this query. """ 48 49 self.email: typing.Union[str, None] = email 50 """ The email address of this query. """ 51 52 if ((self.id is None) and (self.name is None) and (self.email is None)): 53 raise ValueError("Query is empty, it must have at least one piece of information (id, name, email).") 54 55 def match(self, target: typing.Union[typing.Any, 'BaseQuery', None]) -> bool: 56 """ 57 Check if this query matches the given target. 58 A missing field in the query means that field will not be checked. 59 A missing field in the target is seen as empty and mill by checked against. 60 """ 61 62 if (target is None): 63 return False 64 65 field_names = ['id', 'name'] 66 if (self._include_email): 67 field_names.append('email') 68 69 for field_name in field_names: 70 self_value = getattr(self, field_name, None) 71 target_value = getattr(target, field_name, None) 72 73 if (self_value is None): 74 continue 75 76 if (self_value != target_value): 77 return False 78 79 return True 80 81 def to_dict(self, 82 context: typing.Union[edq.util.serial.SerializationContext, None] = None, 83 ) -> typing.Dict[str, edq.util.serial.PODType]: 84 data: typing.Dict[str, edq.util.serial.PODType] = { 85 'id': self.id, 86 'name': self.name, 87 } 88 89 if (self._include_email): 90 data['email'] = self.email 91 92 return data 93 94 def _get_comparison_payload(self, include_id: bool) -> typing.Tuple: 95 """ Get values for comparison. """ 96 97 payload = [] 98 99 if (include_id): 100 payload.append(self.id) 101 102 payload.append(self.name) 103 104 if (self._include_email): 105 payload.append(self.email) 106 107 return tuple(payload) 108 109 def __eq__(self, other: object) -> bool: 110 if (not isinstance(other, BaseQuery)): 111 return False 112 113 # Check the ID specially. 114 comparison = lms.util.string.compare_maybe_ints(self.id, other.id) 115 if (comparison != 0): 116 return False 117 118 return self._get_comparison_payload(False) == other._get_comparison_payload(False) 119 120 def __lt__(self, other: object) -> bool: 121 if (not isinstance(other, BaseQuery)): 122 return False 123 124 # Check the ID specially. 125 comparison = lms.util.string.compare_maybe_ints(self.id, other.id) 126 if (comparison != 0): 127 return (comparison < 0) 128 129 return self._get_comparison_payload(False) < other._get_comparison_payload(False) 130 131 def __hash__(self) -> int: 132 return hash(self._get_comparison_payload(True)) 133 134 def __str__(self) -> str: 135 text = self.email 136 if ((not self._include_email) or (text is None)): 137 text = self.name 138 139 if (self.id is not None): 140 if (text is not None): 141 text = f"{text} ({self.id})" 142 else: 143 text = self.id 144 145 if (text is None): 146 return '<unknown>' 147 148 return text 149 150 def _to_text(self) -> str: 151 """ Represent this query as a string. """ 152 153 return str(self) 154 155class ResolvedBaseQuery(BaseQuery): 156 """ 157 A BaseQuery that has been resolved (verified) from a real instance. 158 """ 159 160 def __init__(self, 161 **kwargs: typing.Any) -> None: 162 super().__init__(**kwargs) 163 164 if (self.id is None): 165 raise ValueError("A resolved query cannot be created without an ID.") 166 167 def get_id(self) -> str: 168 """ Get the ID (which must exists) for this query. """ 169 170 if (self.id is None): 171 raise ValueError("A resolved query cannot be created without an ID.") 172 173 return self.id 174 175def parse_int_query(query_type: typing.Type[T], text: typing.Union[str, None], 176 check_email: bool = True, 177 ) -> typing.Union[T, None]: 178 """ 179 Parse a query with the assumption that LMS ids are ints. 180 181 Accepts queries are in the following forms: 182 - LMS ID (`id`) 183 - Email (`email`) 184 - Name (`name`) 185 - f"{email} ({id})" 186 - f"{name} ({id})" 187 """ 188 189 if (text is None): 190 return None 191 192 # Clean whitespace. 193 text = re.sub(r'\s+', ' ', str(text)).strip() 194 if (len(text) == 0): 195 return None 196 197 id = None 198 email = None 199 name = None 200 201 match = re.search(r'^(\S.*)\((\d+)\)$', text) 202 if (match is not None): 203 # Query has both text and id. 204 name = match.group(1).strip() 205 id = match.group(2) 206 elif (re.search(r'^\d+$', text) is not None): 207 # Query must be an ID. 208 id = text 209 else: 210 name = text 211 212 # Check if the name is actually an email address. 213 if (check_email and (name is not None) and ('@' in name)): 214 email = name 215 name = None 216 217 data = { 218 'id': id, 219 'name': name, 220 'email': email, 221 } 222 223 return query_type(**data)
12class BaseQuery(edq.util.serial.DictConverter): 13 """ 14 Queries are ways that users can attempt to refer to some object with uncertainty. 15 This allows users to refer to objects by name, for example, instead of by id. 16 17 Queries are made up of 2-3 components: 18 - an identifier 19 - a name 20 - an email (optional) 21 22 Email support is decided by child classes. 23 By default, ids are assumed to be only digits. 24 25 A query can be represented in text the following ways: 26 - LMS ID (`id`) 27 - Email (`email`) 28 - Full Name (`name`) 29 - f"{email} ({id})" 30 - f"{name} ({id})" 31 """ 32 33 _include_email: bool = True 34 """ Control if this class instance supports the email field. """ 35 36 def __init__(self, 37 id: typing.Union[str, int, None] = None, 38 name: typing.Union[str, None] = None, 39 email: typing.Union[str, None] = None, 40 **kwargs: typing.Any) -> None: 41 if (id is not None): 42 id = str(id) 43 44 self.id: typing.Union[str, None] = id 45 """ The LMS's identifier for this query. """ 46 47 self.name: typing.Union[str, None] = name 48 """ The display name of this query. """ 49 50 self.email: typing.Union[str, None] = email 51 """ The email address of this query. """ 52 53 if ((self.id is None) and (self.name is None) and (self.email is None)): 54 raise ValueError("Query is empty, it must have at least one piece of information (id, name, email).") 55 56 def match(self, target: typing.Union[typing.Any, 'BaseQuery', None]) -> bool: 57 """ 58 Check if this query matches the given target. 59 A missing field in the query means that field will not be checked. 60 A missing field in the target is seen as empty and mill by checked against. 61 """ 62 63 if (target is None): 64 return False 65 66 field_names = ['id', 'name'] 67 if (self._include_email): 68 field_names.append('email') 69 70 for field_name in field_names: 71 self_value = getattr(self, field_name, None) 72 target_value = getattr(target, field_name, None) 73 74 if (self_value is None): 75 continue 76 77 if (self_value != target_value): 78 return False 79 80 return True 81 82 def to_dict(self, 83 context: typing.Union[edq.util.serial.SerializationContext, None] = None, 84 ) -> typing.Dict[str, edq.util.serial.PODType]: 85 data: typing.Dict[str, edq.util.serial.PODType] = { 86 'id': self.id, 87 'name': self.name, 88 } 89 90 if (self._include_email): 91 data['email'] = self.email 92 93 return data 94 95 def _get_comparison_payload(self, include_id: bool) -> typing.Tuple: 96 """ Get values for comparison. """ 97 98 payload = [] 99 100 if (include_id): 101 payload.append(self.id) 102 103 payload.append(self.name) 104 105 if (self._include_email): 106 payload.append(self.email) 107 108 return tuple(payload) 109 110 def __eq__(self, other: object) -> bool: 111 if (not isinstance(other, BaseQuery)): 112 return False 113 114 # Check the ID specially. 115 comparison = lms.util.string.compare_maybe_ints(self.id, other.id) 116 if (comparison != 0): 117 return False 118 119 return self._get_comparison_payload(False) == other._get_comparison_payload(False) 120 121 def __lt__(self, other: object) -> bool: 122 if (not isinstance(other, BaseQuery)): 123 return False 124 125 # Check the ID specially. 126 comparison = lms.util.string.compare_maybe_ints(self.id, other.id) 127 if (comparison != 0): 128 return (comparison < 0) 129 130 return self._get_comparison_payload(False) < other._get_comparison_payload(False) 131 132 def __hash__(self) -> int: 133 return hash(self._get_comparison_payload(True)) 134 135 def __str__(self) -> str: 136 text = self.email 137 if ((not self._include_email) or (text is None)): 138 text = self.name 139 140 if (self.id is not None): 141 if (text is not None): 142 text = f"{text} ({self.id})" 143 else: 144 text = self.id 145 146 if (text is None): 147 return '<unknown>' 148 149 return text 150 151 def _to_text(self) -> str: 152 """ Represent this query as a string. """ 153 154 return str(self)
Queries are ways that users can attempt to refer to some object with uncertainty. This allows users to refer to objects by name, for example, instead of by id.
Queries are made up of 2-3 components:
- an identifier
- a name
- an email (optional)
Email support is decided by child classes. By default, ids are assumed to be only digits.
A query can be represented in text the following ways:
36 def __init__(self, 37 id: typing.Union[str, int, None] = None, 38 name: typing.Union[str, None] = None, 39 email: typing.Union[str, None] = None, 40 **kwargs: typing.Any) -> None: 41 if (id is not None): 42 id = str(id) 43 44 self.id: typing.Union[str, None] = id 45 """ The LMS's identifier for this query. """ 46 47 self.name: typing.Union[str, None] = name 48 """ The display name of this query. """ 49 50 self.email: typing.Union[str, None] = email 51 """ The email address of this query. """ 52 53 if ((self.id is None) and (self.name is None) and (self.email is None)): 54 raise ValueError("Query is empty, it must have at least one piece of information (id, name, email).")
56 def match(self, target: typing.Union[typing.Any, 'BaseQuery', None]) -> bool: 57 """ 58 Check if this query matches the given target. 59 A missing field in the query means that field will not be checked. 60 A missing field in the target is seen as empty and mill by checked against. 61 """ 62 63 if (target is None): 64 return False 65 66 field_names = ['id', 'name'] 67 if (self._include_email): 68 field_names.append('email') 69 70 for field_name in field_names: 71 self_value = getattr(self, field_name, None) 72 target_value = getattr(target, field_name, None) 73 74 if (self_value is None): 75 continue 76 77 if (self_value != target_value): 78 return False 79 80 return True
Check if this query matches the given target. A missing field in the query means that field will not be checked. A missing field in the target is seen as empty and mill by checked against.
82 def to_dict(self, 83 context: typing.Union[edq.util.serial.SerializationContext, None] = None, 84 ) -> typing.Dict[str, edq.util.serial.PODType]: 85 data: typing.Dict[str, edq.util.serial.PODType] = { 86 'id': self.id, 87 'name': self.name, 88 } 89 90 if (self._include_email): 91 data['email'] = self.email 92 93 return data
Return a dict that can be used to represent this object. If the dict is passed to from_dict(), an identical object should be reconstructed.
A general (but inefficient) implementation is provided by default.
156class ResolvedBaseQuery(BaseQuery): 157 """ 158 A BaseQuery that has been resolved (verified) from a real instance. 159 """ 160 161 def __init__(self, 162 **kwargs: typing.Any) -> None: 163 super().__init__(**kwargs) 164 165 if (self.id is None): 166 raise ValueError("A resolved query cannot be created without an ID.") 167 168 def get_id(self) -> str: 169 """ Get the ID (which must exists) for this query. """ 170 171 if (self.id is None): 172 raise ValueError("A resolved query cannot be created without an ID.") 173 174 return self.id
A BaseQuery that has been resolved (verified) from a real instance.
176def parse_int_query(query_type: typing.Type[T], text: typing.Union[str, None], 177 check_email: bool = True, 178 ) -> typing.Union[T, None]: 179 """ 180 Parse a query with the assumption that LMS ids are ints. 181 182 Accepts queries are in the following forms: 183 - LMS ID (`id`) 184 - Email (`email`) 185 - Name (`name`) 186 - f"{email} ({id})" 187 - f"{name} ({id})" 188 """ 189 190 if (text is None): 191 return None 192 193 # Clean whitespace. 194 text = re.sub(r'\s+', ' ', str(text)).strip() 195 if (len(text) == 0): 196 return None 197 198 id = None 199 email = None 200 name = None 201 202 match = re.search(r'^(\S.*)\((\d+)\)$', text) 203 if (match is not None): 204 # Query has both text and id. 205 name = match.group(1).strip() 206 id = match.group(2) 207 elif (re.search(r'^\d+$', text) is not None): 208 # Query must be an ID. 209 id = text 210 else: 211 name = text 212 213 # Check if the name is actually an email address. 214 if (check_email and (name is not None) and ('@' in name)): 215 email = name 216 name = None 217 218 data = { 219 'id': id, 220 'name': name, 221 'email': email, 222 } 223 224 return query_type(**data)
Parse a query with the assumption that LMS ids are ints.
Accepts queries are in the following forms:
- LMS ID (id)
- Email (email)
- Name (name)
- f"{email} ({id})"
- f"{name} ({id})"