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)
class BaseQuery(edq.util.json.DictConverter):
 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:

  • LMS ID (id)
  • Email (email)
  • Full Name (name)
  • f"{email} ({id})"
  • f"{name} ({id})"
BaseQuery( id: Union[str, int, NoneType] = None, name: Optional[str] = None, email: Optional[str] = None, **kwargs: Any)
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).")
id: Optional[str]

The LMS's identifier for this query.

name: Optional[str]

The display name of this query.

email: Optional[str]

The email address of this query.

def match(self, target: Union[Any, BaseQuery, NoneType]) -> bool:
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.

def to_dict(self) -> Dict[str, Any]:
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.

class ResolvedBaseQuery(BaseQuery):
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.

ResolvedBaseQuery(**kwargs: Any)
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.")
def get_id(self) -> str:
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

Get the ID (which must exists) for this query.

Inherited Members
BaseQuery
id
name
email
match
to_dict
def parse_int_query( query_type: Type[~T], text: Optional[str], check_email: bool = True) -> Optional[~T]:
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})"