lms.model.query
1import re 2import typing 3 4import edq.util.json 5 6import lms.model.base 7import lms.util.string 8 9T = typing.TypeVar('T') 10 11class BaseQuery(edq.util.json.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) -> typing.Dict[str, typing.Any]: 82 data = { 83 'id': self.id, 84 'name': self.name, 85 } 86 87 if (self._include_email): 88 data['email'] = self.email 89 90 return data 91 92 def _get_comparison_payload(self, include_id: bool) -> typing.Tuple: 93 """ Get values for comparison. """ 94 95 payload = [] 96 97 if (include_id): 98 payload.append(self.id) 99 100 payload.append(self.name) 101 102 if (self._include_email): 103 payload.append(self.email) 104 105 return tuple(payload) 106 107 def __eq__(self, other: object) -> bool: 108 if (not isinstance(other, BaseQuery)): 109 return False 110 111 # Check the ID specially. 112 comparison = lms.util.string.compare_maybe_ints(self.id, other.id) 113 if (comparison != 0): 114 return False 115 116 return self._get_comparison_payload(False) == other._get_comparison_payload(False) 117 118 def __lt__(self, other: object) -> bool: 119 if (not isinstance(other, BaseQuery)): 120 return False 121 122 # Check the ID specially. 123 comparison = lms.util.string.compare_maybe_ints(self.id, other.id) 124 if (comparison != 0): 125 return (comparison < 0) 126 127 return self._get_comparison_payload(False) < other._get_comparison_payload(False) 128 129 def __hash__(self) -> int: 130 return hash(self._get_comparison_payload(True)) 131 132 def __str__(self) -> str: 133 text = self.email 134 if ((not self._include_email) or (text is None)): 135 text = self.name 136 137 if (self.id is not None): 138 if (text is not None): 139 text = f"{text} ({self.id})" 140 else: 141 text = self.id 142 143 if (text is None): 144 return '<unknown>' 145 146 return text 147 148 def _to_text(self) -> str: 149 """ Represent this query as a string. """ 150 151 return str(self) 152 153class ResolvedBaseQuery(BaseQuery): 154 """ 155 A BaseQuery that has been resolved (verified) from a real instance. 156 """ 157 158 def __init__(self, 159 **kwargs: typing.Any) -> None: 160 super().__init__(**kwargs) 161 162 if (self.id is None): 163 raise ValueError("A resolved query cannot be created without an ID.") 164 165 def get_id(self) -> str: 166 """ Get the ID (which must exists) for this query. """ 167 168 if (self.id is None): 169 raise ValueError("A resolved query cannot be created without an ID.") 170 171 return self.id 172 173def parse_int_query(query_type: typing.Type[T], text: typing.Union[str, None], 174 check_email: bool = True, 175 ) -> typing.Union[T, None]: 176 """ 177 Parse a query with the assumption that LMS ids are ints. 178 179 Accepts queries are in the following forms: 180 - LMS ID (`id`) 181 - Email (`email`) 182 - Name (`name`) 183 - f"{email} ({id})" 184 - f"{name} ({id})" 185 """ 186 187 if (text is None): 188 return None 189 190 # Clean whitespace. 191 text = re.sub(r'\s+', ' ', str(text)).strip() 192 if (len(text) == 0): 193 return None 194 195 id = None 196 email = None 197 name = None 198 199 match = re.search(r'^(\S.*)\((\d+)\)$', text) 200 if (match is not None): 201 # Query has both text and id. 202 name = match.group(1).strip() 203 id = match.group(2) 204 elif (re.search(r'^\d+$', text) is not None): 205 # Query must be an ID. 206 id = text 207 else: 208 name = text 209 210 # Check if the name is actually an email address. 211 if (check_email and (name is not None) and ('@' in name)): 212 email = name 213 name = None 214 215 data = { 216 'id': id, 217 'name': name, 218 'email': email, 219 } 220 221 return query_type(**data)
12class BaseQuery(edq.util.json.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) -> typing.Dict[str, typing.Any]: 83 data = { 84 'id': self.id, 85 'name': self.name, 86 } 87 88 if (self._include_email): 89 data['email'] = self.email 90 91 return data 92 93 def _get_comparison_payload(self, include_id: bool) -> typing.Tuple: 94 """ Get values for comparison. """ 95 96 payload = [] 97 98 if (include_id): 99 payload.append(self.id) 100 101 payload.append(self.name) 102 103 if (self._include_email): 104 payload.append(self.email) 105 106 return tuple(payload) 107 108 def __eq__(self, other: object) -> bool: 109 if (not isinstance(other, BaseQuery)): 110 return False 111 112 # Check the ID specially. 113 comparison = lms.util.string.compare_maybe_ints(self.id, other.id) 114 if (comparison != 0): 115 return False 116 117 return self._get_comparison_payload(False) == other._get_comparison_payload(False) 118 119 def __lt__(self, other: object) -> bool: 120 if (not isinstance(other, BaseQuery)): 121 return False 122 123 # Check the ID specially. 124 comparison = lms.util.string.compare_maybe_ints(self.id, other.id) 125 if (comparison != 0): 126 return (comparison < 0) 127 128 return self._get_comparison_payload(False) < other._get_comparison_payload(False) 129 130 def __hash__(self) -> int: 131 return hash(self._get_comparison_payload(True)) 132 133 def __str__(self) -> str: 134 text = self.email 135 if ((not self._include_email) or (text is None)): 136 text = self.name 137 138 if (self.id is not None): 139 if (text is not None): 140 text = f"{text} ({self.id})" 141 else: 142 text = self.id 143 144 if (text is None): 145 return '<unknown>' 146 147 return text 148 149 def _to_text(self) -> str: 150 """ Represent this query as a string. """ 151 152 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) -> typing.Dict[str, typing.Any]: 83 data = { 84 'id': self.id, 85 'name': self.name, 86 } 87 88 if (self._include_email): 89 data['email'] = self.email 90 91 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.
154class ResolvedBaseQuery(BaseQuery): 155 """ 156 A BaseQuery that has been resolved (verified) from a real instance. 157 """ 158 159 def __init__(self, 160 **kwargs: typing.Any) -> None: 161 super().__init__(**kwargs) 162 163 if (self.id is None): 164 raise ValueError("A resolved query cannot be created without an ID.") 165 166 def get_id(self) -> str: 167 """ Get the ID (which must exists) for this query. """ 168 169 if (self.id is None): 170 raise ValueError("A resolved query cannot be created without an ID.") 171 172 return self.id
A BaseQuery that has been resolved (verified) from a real instance.
174def parse_int_query(query_type: typing.Type[T], text: typing.Union[str, None], 175 check_email: bool = True, 176 ) -> typing.Union[T, None]: 177 """ 178 Parse a query with the assumption that LMS ids are ints. 179 180 Accepts queries are in the following forms: 181 - LMS ID (`id`) 182 - Email (`email`) 183 - Name (`name`) 184 - f"{email} ({id})" 185 - f"{name} ({id})" 186 """ 187 188 if (text is None): 189 return None 190 191 # Clean whitespace. 192 text = re.sub(r'\s+', ' ', str(text)).strip() 193 if (len(text) == 0): 194 return None 195 196 id = None 197 email = None 198 name = None 199 200 match = re.search(r'^(\S.*)\((\d+)\)$', text) 201 if (match is not None): 202 # Query has both text and id. 203 name = match.group(1).strip() 204 id = match.group(2) 205 elif (re.search(r'^\d+$', text) is not None): 206 # Query must be an ID. 207 id = text 208 else: 209 name = text 210 211 # Check if the name is actually an email address. 212 if (check_email and (name is not None) and ('@' in name)): 213 email = name 214 name = None 215 216 data = { 217 'id': id, 218 'name': name, 219 'email': email, 220 } 221 222 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})"