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

  • 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, context: Optional[edq.util.common.SerializationContext] = None) -> Dict[str, Union[bool, float, int, str, List[ForwardRef('PODType')], Dict[str, ForwardRef('PODType')], NoneType]]:
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.

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

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

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]:
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})"