Loglan-Core 0.3.0__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {loglan_core-0.3.0 → loglan_core-0.4.2/Loglan_Core.egg-info}/PKG-INFO +2 -2
- {loglan_core-0.3.0/Loglan_Core.egg-info → loglan_core-0.4.2}/PKG-INFO +2 -2
- loglan_core-0.4.2/loglan_core/addons/base_selector.py +268 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/definition_selector.py +53 -37
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/key_selector.py +37 -35
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/word_selector.py +61 -133
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/base.py +29 -6
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/word.py +27 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/setup.py +1 -1
- loglan_core-0.3.0/loglan_core/addons/base_selector.py +0 -202
- {loglan_core-0.3.0 → loglan_core-0.4.2}/LICENSE +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/Loglan_Core.egg-info/SOURCES.txt +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/Loglan_Core.egg-info/dependency_links.txt +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/Loglan_Core.egg-info/requires.txt +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/Loglan_Core.egg-info/top_level.txt +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/README.md +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/__init__.py +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/__init__.py +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/export_word_converter.py +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/exporter.py +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/utils.py +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/word_linker.py +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/word_sourcer.py +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/author.py +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/connect_tables.py +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/definition.py +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/event.py +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/key.py +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/setting.py +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/syllable.py +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/table_names.py +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/type.py +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/word_spell.py +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/pyproject.toml +0 -0
- {loglan_core-0.3.0 → loglan_core-0.4.2}/setup.cfg +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: Loglan-Core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Loglan Dictionary Database Model for SQLAlchemy
|
|
5
5
|
Home-page: https://github.com/torrua/loglan_core
|
|
6
|
-
Download-URL: https://github.com/torrua/loglan_core/archive/0.
|
|
6
|
+
Download-URL: https://github.com/torrua/loglan_core/archive/0.4.2.tar.gz
|
|
7
7
|
Author: torrua
|
|
8
8
|
Author-email: torrua@gmail.com
|
|
9
9
|
License: MIT
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: Loglan-Core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Loglan Dictionary Database Model for SQLAlchemy
|
|
5
5
|
Home-page: https://github.com/torrua/loglan_core
|
|
6
|
-
Download-URL: https://github.com/torrua/loglan_core/archive/0.
|
|
6
|
+
Download-URL: https://github.com/torrua/loglan_core/archive/0.4.2.tar.gz
|
|
7
7
|
Author: torrua
|
|
8
8
|
Author-email: torrua@gmail.com
|
|
9
9
|
License: MIT
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides a base selector for SQLAlchemy
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Type, Iterable, Any
|
|
8
|
+
|
|
9
|
+
from sqlalchemy import select, Select
|
|
10
|
+
from sqlalchemy.orm import Session, selectinload, InstrumentedAttribute
|
|
11
|
+
from typing_extensions import Self
|
|
12
|
+
|
|
13
|
+
from loglan_core.base import BaseModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseSelector: # pylint: disable=too-many-ancestors
|
|
17
|
+
"""
|
|
18
|
+
A custom base selector that inherits from SQLAlchemy's Select class.
|
|
19
|
+
This class provides methods to execute a session and fetch results
|
|
20
|
+
in different ways. It also provides a way to fetch many results.
|
|
21
|
+
|
|
22
|
+
Methods:
|
|
23
|
+
--------
|
|
24
|
+
execute(session: Session) -> ResultProxy:
|
|
25
|
+
Executes the session and returns the result.
|
|
26
|
+
|
|
27
|
+
all(session: Session) -> List[ResultRow]:
|
|
28
|
+
Executes the session and returns all the results.
|
|
29
|
+
|
|
30
|
+
scalar(session: Session) -> Any:
|
|
31
|
+
Executes the session and returns a scalar result.
|
|
32
|
+
|
|
33
|
+
fetchmany(session: Session, size: int | None = None) -> List[ResultRow]:
|
|
34
|
+
Executes the session and fetches a specified number of results.
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
model: Type[BaseModel],
|
|
41
|
+
is_sqlite: bool = False,
|
|
42
|
+
case_sensitive: bool = False,
|
|
43
|
+
disable_model_check: bool = False,
|
|
44
|
+
):
|
|
45
|
+
"""Initializes the Selector.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
model (Type): The SQLAlchemy model class to query.
|
|
49
|
+
is_sqlite (bool): Flag indicating if the database is SQLite.
|
|
50
|
+
case_sensitive (bool): Flag indicating if the queries should be case-sensitive.
|
|
51
|
+
disable_model_check (bool): Flag indicating if the model check should be disabled.
|
|
52
|
+
"""
|
|
53
|
+
self.disable_model_check = disable_model_check
|
|
54
|
+
if not self.disable_model_check:
|
|
55
|
+
self._is_model_accepted(model, BaseModel)
|
|
56
|
+
|
|
57
|
+
self.model = model
|
|
58
|
+
self._statement = select(self.model)
|
|
59
|
+
self._selected_columns = [self.model]
|
|
60
|
+
|
|
61
|
+
self.is_sqlite = is_sqlite
|
|
62
|
+
self.case_sensitive = case_sensitive
|
|
63
|
+
|
|
64
|
+
def execute(self, session: Session):
|
|
65
|
+
"""
|
|
66
|
+
Executes the given session and returns the result.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
session (Session): SQLAlchemy Session object.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
ResultProxy: The result of the executed session.
|
|
73
|
+
"""
|
|
74
|
+
return session.execute(self._statement)
|
|
75
|
+
|
|
76
|
+
def all(self, session: Session):
|
|
77
|
+
"""
|
|
78
|
+
Executes the given session and returns all the results as a list.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
session (Session): SQLAlchemy Session object.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
List[ResultRow]: All the results of the executed session.
|
|
85
|
+
"""
|
|
86
|
+
return session.execute(self._statement).scalars().all()
|
|
87
|
+
|
|
88
|
+
def scalar(self, session: Session):
|
|
89
|
+
"""
|
|
90
|
+
Executes the given session and returns a scalar result.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
session (Session): SQLAlchemy Session object.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Any: The scalar result of the executed session.
|
|
97
|
+
"""
|
|
98
|
+
return session.execute(self._statement).scalar()
|
|
99
|
+
|
|
100
|
+
def fetchmany(self, session: Session, size: int | None = None):
|
|
101
|
+
"""
|
|
102
|
+
Executes the given session and fetches a specified number of results.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
session (Session): SQLAlchemy Session object.
|
|
106
|
+
size (int, optional): Number of results to fetch. If None, fetches all results.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
List[ResultRow]: The fetched results.
|
|
110
|
+
"""
|
|
111
|
+
return session.execute(self._statement).scalars().fetchmany(size)
|
|
112
|
+
|
|
113
|
+
def select_columns(self, *columns: type[BaseModel]) -> Self:
|
|
114
|
+
"""Specify which columns to select without resetting the filters.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
*columns: The columns to select.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Self: The current instance for method chaining.
|
|
121
|
+
"""
|
|
122
|
+
self._selected_columns = list(columns)
|
|
123
|
+
existing_conditions = self._statement.whereclause
|
|
124
|
+
|
|
125
|
+
if existing_conditions is None:
|
|
126
|
+
self._statement = select(*self._selected_columns)
|
|
127
|
+
else:
|
|
128
|
+
self._statement = select(*self._selected_columns).where(existing_conditions)
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
def limit(self, limit: int) -> Self:
|
|
132
|
+
"""Limit the number of results returned.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
limit (int): The maximum number of results to return.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Self: The current instance for method chaining.
|
|
139
|
+
"""
|
|
140
|
+
self._statement = self._statement.limit(limit)
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def offset(self, offset: int) -> Self:
|
|
144
|
+
"""Set the offset for the results returned.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
offset (int): The number of results to skip before starting to return results.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Self: The current instance for method chaining.
|
|
151
|
+
"""
|
|
152
|
+
self._statement = self._statement.offset(offset)
|
|
153
|
+
return self
|
|
154
|
+
|
|
155
|
+
def order_by(self, *columns) -> Self:
|
|
156
|
+
"""Specify the order in which results should be returned.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
*columns: The columns to order by.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Self: The current instance for method chaining.
|
|
163
|
+
"""
|
|
164
|
+
self._statement = self._statement.order_by(*columns)
|
|
165
|
+
return self
|
|
166
|
+
|
|
167
|
+
def filter(self, *args) -> Self:
|
|
168
|
+
"""Filter results based on arbitrary keyword arguments.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
*args: Column-value pairs to filter by.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Self: The current instance for method chaining.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
self._statement = self._statement.filter(*args)
|
|
178
|
+
|
|
179
|
+
return self
|
|
180
|
+
|
|
181
|
+
def filter_by(self, **kwargs) -> Self:
|
|
182
|
+
"""Filter results based on arbitrary keyword arguments.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
*kwargs: Column-value pairs to filter by.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Self: The current instance for method chaining.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
self._statement = self._statement.filter_by(**kwargs)
|
|
192
|
+
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
def where(self, *args) -> Self:
|
|
196
|
+
"""Filter results based on arbitrary keyword arguments.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
*args: Column-value pairs to filter by.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Self: The current instance for method chaining.
|
|
203
|
+
"""
|
|
204
|
+
self._statement = self._statement.where(*args)
|
|
205
|
+
return self
|
|
206
|
+
|
|
207
|
+
def _generate_column_condition(self, key: str | InstrumentedAttribute, value: Any):
|
|
208
|
+
column = getattr(self.model, key, None) if isinstance(key, str) else key
|
|
209
|
+
if column is None:
|
|
210
|
+
raise AttributeError(f"Model {self.model} has no attribute {key}")
|
|
211
|
+
|
|
212
|
+
value = str(value).replace("*", "%")
|
|
213
|
+
if self.case_sensitive:
|
|
214
|
+
return column.op("GLOB")(value) if self.is_sqlite else column.like(value)
|
|
215
|
+
return column.ilike(value)
|
|
216
|
+
|
|
217
|
+
def get_statement(self) -> Select:
|
|
218
|
+
"""Get the current SQLAlchemy _statement.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Select: The current SQLAlchemy _statement.
|
|
222
|
+
"""
|
|
223
|
+
return self._statement
|
|
224
|
+
|
|
225
|
+
def __call__(self, session: Session):
|
|
226
|
+
"""Execute the current _statement and return the results.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
session (Session): The SQLAlchemy session to use for executing the query.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
list: The results of the query.
|
|
233
|
+
"""
|
|
234
|
+
return self.all(session)
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _is_model_accepted(model, parent: type[BaseModel] = BaseModel):
|
|
238
|
+
"""
|
|
239
|
+
Checks if the model is an instance of BaseModel or its child.
|
|
240
|
+
Raises:
|
|
241
|
+
ValueError: If the model is not an instance of BaseModel or its child.
|
|
242
|
+
"""
|
|
243
|
+
if not issubclass(model, parent):
|
|
244
|
+
raise ValueError(
|
|
245
|
+
f"Provided class_={model} is not a inherited from {parent}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def with_relationships(self, selected: Iterable[str] | None = None) -> Self:
|
|
249
|
+
"""
|
|
250
|
+
Adds relationships to the query.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
selected (set[str]): A set of relationship names to include.
|
|
254
|
+
Defaults to None if all relationships should be included.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Self: A query with the relationships added.
|
|
258
|
+
"""
|
|
259
|
+
available_relationships = {
|
|
260
|
+
attr: getattr(self.model, attr) for attr in self.model.relationships()
|
|
261
|
+
}
|
|
262
|
+
relationships = {
|
|
263
|
+
selectinload(v)
|
|
264
|
+
for k, v in available_relationships.items()
|
|
265
|
+
if not selected or k in selected
|
|
266
|
+
}
|
|
267
|
+
self._statement = self._statement.options(*relationships)
|
|
268
|
+
return self
|
|
@@ -11,9 +11,10 @@ Classes:
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
-
from typing import
|
|
14
|
+
from typing import Type
|
|
15
15
|
|
|
16
16
|
from sqlalchemy import select, true
|
|
17
|
+
from typing_extensions import Self
|
|
17
18
|
|
|
18
19
|
from loglan_core.addons.base_selector import BaseSelector
|
|
19
20
|
from loglan_core.addons.utils import (
|
|
@@ -44,39 +45,36 @@ class DefinitionSelector(BaseSelector): # pylint: disable=too-many-ancestors
|
|
|
44
45
|
ValueError: If the provided class_ is not a subclass of BaseDefinition.
|
|
45
46
|
|
|
46
47
|
Attributes:
|
|
47
|
-
|
|
48
|
+
model: The class to be used as the returned object.
|
|
48
49
|
is_sqlite: Boolean specifying if the object is being used with SQLite or not.
|
|
49
50
|
"""
|
|
50
51
|
|
|
51
|
-
def __init__(
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
model: Type[BaseDefinition] = BaseDefinition,
|
|
55
|
+
is_sqlite: bool = False,
|
|
56
|
+
case_sensitive: bool = False,
|
|
57
|
+
disable_model_check: bool = False,
|
|
58
|
+
):
|
|
52
59
|
"""
|
|
53
|
-
Initializes the object with the
|
|
60
|
+
Initializes the DefinitionSelector object with the provided parameters.
|
|
54
61
|
|
|
55
62
|
Args:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
is_sqlite (bool):
|
|
63
|
+
model (Type[BaseDefinition]): The class to be used as the base key.
|
|
64
|
+
Must be a subclass of BaseDefinition.
|
|
65
|
+
is_sqlite (bool): If SQLite is being used. Defaults to False.
|
|
66
|
+
case_sensitive (bool): If the queries should be case-sensitive.
|
|
67
|
+
disable_model_check (bool): If the model check is disabled during initialization.
|
|
59
68
|
|
|
60
69
|
Raises:
|
|
61
|
-
ValueError: If the provided
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
None
|
|
70
|
+
ValueError: If the provided model is not a subclass of BaseDefinition.
|
|
65
71
|
"""
|
|
66
|
-
if not issubclass(class_, BaseDefinition):
|
|
67
|
-
raise ValueError(
|
|
68
|
-
f"Provided attribute class_={class_} is not a {BaseDefinition} or its child"
|
|
69
|
-
)
|
|
70
|
-
super().__init__(class_)
|
|
71
|
-
self.class_ = class_
|
|
72
|
-
self.is_sqlite = is_sqlite
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return True
|
|
73
|
+
super().__init__(model, is_sqlite, case_sensitive, disable_model_check)
|
|
74
|
+
if not disable_model_check:
|
|
75
|
+
self._is_model_accepted(model, BaseDefinition)
|
|
76
|
+
|
|
77
|
+
self.model = model
|
|
80
78
|
|
|
81
79
|
def by_event(self, event_id: int | None = None) -> DefinitionSelector:
|
|
82
80
|
"""
|
|
@@ -90,20 +88,20 @@ class DefinitionSelector(BaseSelector): # pylint: disable=too-many-ancestors
|
|
|
90
88
|
DefinitionSelector: The filtered DefinitionSelector instance.
|
|
91
89
|
"""
|
|
92
90
|
subquery = (
|
|
93
|
-
select(self.
|
|
91
|
+
select(self.model.id)
|
|
94
92
|
.join(t_connect_keys)
|
|
95
93
|
.join(BaseWord)
|
|
96
94
|
.where(filter_word_by_event_id(event_id))
|
|
97
95
|
.scalar_subquery()
|
|
98
96
|
)
|
|
99
|
-
|
|
97
|
+
self._statement = self._statement.where(self.model.id.in_(subquery))
|
|
98
|
+
return self
|
|
100
99
|
|
|
101
100
|
def by_key(
|
|
102
101
|
self,
|
|
103
102
|
key: BaseKey | str,
|
|
104
103
|
language: str | None = None,
|
|
105
|
-
|
|
106
|
-
) -> DefinitionSelector:
|
|
104
|
+
) -> Self:
|
|
107
105
|
"""
|
|
108
106
|
This method filters the definitions by the provided key, language and case sensitivity.
|
|
109
107
|
|
|
@@ -111,22 +109,33 @@ class DefinitionSelector(BaseSelector): # pylint: disable=too-many-ancestors
|
|
|
111
109
|
key (BaseKey | str): The key to filter by. Can be an instance of BaseKey or a string.
|
|
112
110
|
language (str | None): The language to filter by.
|
|
113
111
|
If None, no language filtering is applied.
|
|
114
|
-
case_sensitive (bool): Flag indicating whether filtering should be case sensitive.
|
|
115
112
|
|
|
116
113
|
Returns:
|
|
117
|
-
|
|
114
|
+
Self: The filtered DefinitionSelector instance with distinct keys.
|
|
118
115
|
"""
|
|
119
116
|
|
|
120
117
|
search_key = key.word if isinstance(key, BaseKey) else str(key)
|
|
121
|
-
filter_key = filter_key_by_word_cs(
|
|
118
|
+
filter_key = filter_key_by_word_cs(
|
|
119
|
+
search_key, self.case_sensitive, self.is_sqlite
|
|
120
|
+
)
|
|
122
121
|
filter_language = filter_key_by_language(
|
|
123
122
|
key.language if isinstance(key, BaseKey) else language
|
|
124
123
|
)
|
|
125
124
|
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
if hasattr(self.model, "keys"):
|
|
126
|
+
self._statement = (
|
|
127
|
+
self._statement.join(self.model.keys)
|
|
128
|
+
.where(filter_key, filter_language)
|
|
129
|
+
.distinct()
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
raise AttributeError(
|
|
133
|
+
f"{self.model.__name__} does not have a 'keys' attribute"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return self
|
|
128
137
|
|
|
129
|
-
def by_language(self, language: str | None = None) ->
|
|
138
|
+
def by_language(self, language: str | None = None) -> Self:
|
|
130
139
|
"""
|
|
131
140
|
This method filters the definitions by the given language.
|
|
132
141
|
|
|
@@ -135,7 +144,14 @@ class DefinitionSelector(BaseSelector): # pylint: disable=too-many-ancestors
|
|
|
135
144
|
no language filtering is applied.
|
|
136
145
|
|
|
137
146
|
Returns:
|
|
138
|
-
|
|
147
|
+
Self: The filtered DefinitionSelector instance.
|
|
139
148
|
"""
|
|
140
|
-
|
|
141
|
-
|
|
149
|
+
if hasattr(self.model, "language"):
|
|
150
|
+
filter_language = self.model.language == language if language else true()
|
|
151
|
+
else:
|
|
152
|
+
raise AttributeError(
|
|
153
|
+
f"{self.model.__name__} does not have a 'language' attribute"
|
|
154
|
+
)
|
|
155
|
+
self._statement = self._statement.where(filter_language)
|
|
156
|
+
|
|
157
|
+
return self
|
|
@@ -23,7 +23,7 @@ This allows for flexible and powerful querying of keys in a codebase.
|
|
|
23
23
|
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
|
-
from typing import
|
|
26
|
+
from typing import Type
|
|
27
27
|
|
|
28
28
|
from sqlalchemy import select
|
|
29
29
|
|
|
@@ -44,38 +44,40 @@ class KeySelector(BaseSelector): # pylint: disable=too-many-ancestors
|
|
|
44
44
|
A class used to select keys meeting certain criteria.
|
|
45
45
|
|
|
46
46
|
Attributes:
|
|
47
|
-
|
|
47
|
+
model (Type[BaseKey]): The class to be used as the base key.
|
|
48
48
|
Must be a subclass of BaseKey.
|
|
49
|
-
is_sqlite (bool):
|
|
49
|
+
is_sqlite (bool): If SQLite is being used. Defaults to False.
|
|
50
|
+
case_sensitive (bool): If the queries should be case-sensitive.
|
|
51
|
+
disable_model_check (bool): If the model check is disabled during initialization.
|
|
50
52
|
"""
|
|
51
53
|
|
|
52
|
-
def __init__(
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
model: Type[BaseKey] = BaseKey,
|
|
57
|
+
is_sqlite: bool = False,
|
|
58
|
+
case_sensitive: bool = False,
|
|
59
|
+
disable_model_check: bool = False,
|
|
60
|
+
) -> None:
|
|
53
61
|
"""
|
|
54
62
|
Initializes the KeySelector object with the provided parameters.
|
|
55
63
|
|
|
56
64
|
Args:
|
|
57
|
-
|
|
65
|
+
model (Type[BaseKey]): The class to be used as the base key.
|
|
58
66
|
Must be a subclass of BaseKey.
|
|
59
|
-
is_sqlite (bool):
|
|
67
|
+
is_sqlite (bool): If SQLite is being used. Defaults to False.
|
|
68
|
+
case_sensitive (bool): If the queries should be case-sensitive.
|
|
69
|
+
disable_model_check (bool): If the model check is disabled during initialization.
|
|
60
70
|
|
|
61
71
|
Raises:
|
|
62
|
-
ValueError: If the provided
|
|
72
|
+
ValueError: If the provided model is not a subclass of BaseKey.
|
|
63
73
|
"""
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
self.
|
|
71
|
-
|
|
72
|
-
@property
|
|
73
|
-
def inherit_cache(self): # pylint: disable=C0116
|
|
74
|
-
"""
|
|
75
|
-
Returns:
|
|
76
|
-
bool
|
|
77
|
-
"""
|
|
78
|
-
return True
|
|
74
|
+
|
|
75
|
+
super().__init__(model, is_sqlite, case_sensitive, disable_model_check)
|
|
76
|
+
|
|
77
|
+
if not self.disable_model_check:
|
|
78
|
+
self._is_model_accepted(model, BaseKey)
|
|
79
|
+
|
|
80
|
+
self.model = model
|
|
79
81
|
|
|
80
82
|
def by_event(self, event_id: int | None = None) -> KeySelector:
|
|
81
83
|
"""
|
|
@@ -90,31 +92,30 @@ class KeySelector(BaseSelector): # pylint: disable=too-many-ancestors
|
|
|
90
92
|
"""
|
|
91
93
|
|
|
92
94
|
subquery = (
|
|
93
|
-
select(self.
|
|
95
|
+
select(self.model.id)
|
|
94
96
|
.join(t_connect_keys)
|
|
95
97
|
.join(BaseDefinition)
|
|
96
98
|
.join(BaseWord)
|
|
97
99
|
.where(filter_word_by_event_id(event_id))
|
|
98
100
|
.scalar_subquery()
|
|
99
101
|
)
|
|
100
|
-
|
|
102
|
+
self._statement = self._statement.where(self.model.id.in_(subquery))
|
|
103
|
+
return self
|
|
101
104
|
|
|
102
|
-
def by_key(self, key: str
|
|
105
|
+
def by_key(self, key: str) -> KeySelector:
|
|
103
106
|
"""
|
|
104
107
|
Filters the keys by the given key.
|
|
105
108
|
|
|
106
109
|
Args:
|
|
107
110
|
key (str): The key to filter by.
|
|
108
|
-
case_sensitive (bool): Determines whether the key search should be
|
|
109
|
-
case-sensitive. Defaults to False.
|
|
110
111
|
|
|
111
112
|
Returns:
|
|
112
113
|
KeySelector: The filtered KeySelector instance.
|
|
113
114
|
"""
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
self.where(filter_key_by_word_cs(key, case_sensitive, self.is_sqlite)),
|
|
115
|
+
self._statement = self._statement.where(
|
|
116
|
+
filter_key_by_word_cs(key, self.case_sensitive, self.is_sqlite)
|
|
117
117
|
)
|
|
118
|
+
return self
|
|
118
119
|
|
|
119
120
|
def by_language(self, language: str | None = None) -> KeySelector:
|
|
120
121
|
"""
|
|
@@ -127,7 +128,8 @@ class KeySelector(BaseSelector): # pylint: disable=too-many-ancestors
|
|
|
127
128
|
Returns:
|
|
128
129
|
KeySelector: The filtered KeySelector instance.
|
|
129
130
|
"""
|
|
130
|
-
|
|
131
|
+
self._statement = self._statement.where(filter_key_by_language(language))
|
|
132
|
+
return self
|
|
131
133
|
|
|
132
134
|
def by_word_id(self, word_id: int) -> KeySelector:
|
|
133
135
|
"""
|
|
@@ -143,12 +145,12 @@ class KeySelector(BaseSelector): # pylint: disable=too-many-ancestors
|
|
|
143
145
|
will be counted with ```.count()``` but excluded from ```.all()``` request
|
|
144
146
|
|
|
145
147
|
"""
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
self.distinct()
|
|
148
|
+
self._statement = (
|
|
149
|
+
self._statement.distinct()
|
|
149
150
|
.join(t_connect_keys)
|
|
150
151
|
.join(BaseDefinition, BaseDefinition.id == t_connect_keys.c.DID)
|
|
151
152
|
.join(BaseWord, BaseWord.id == BaseDefinition.word_id)
|
|
152
153
|
.filter(BaseWord.id == word_id)
|
|
153
|
-
.order_by(BaseKey.word.asc())
|
|
154
|
+
.order_by(BaseKey.word.asc())
|
|
154
155
|
)
|
|
156
|
+
return self
|
|
@@ -6,53 +6,20 @@ event, key, type, and name through the WordSelector class.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from
|
|
10
|
-
from typing import Type, Iterable
|
|
9
|
+
from typing import Type
|
|
11
10
|
|
|
12
|
-
from sqlalchemy import and_, select
|
|
13
|
-
from sqlalchemy.orm import selectinload
|
|
11
|
+
from sqlalchemy import and_, select
|
|
14
12
|
from typing_extensions import Self
|
|
15
13
|
|
|
16
|
-
from loglan_core.connect_tables import t_connect_words
|
|
17
14
|
from loglan_core.addons.base_selector import BaseSelector
|
|
18
15
|
from loglan_core.addons.definition_selector import DefinitionSelector
|
|
19
16
|
from loglan_core.addons.utils import filter_word_by_event_id
|
|
17
|
+
from loglan_core.connect_tables import t_connect_words
|
|
20
18
|
from loglan_core.key import BaseKey
|
|
21
19
|
from loglan_core.type import BaseType
|
|
22
20
|
from loglan_core.word import BaseWord
|
|
23
21
|
|
|
24
22
|
|
|
25
|
-
def order_by_name(function):
|
|
26
|
-
"""
|
|
27
|
-
A decorator that sorts the output of a function by the `name` attribute of
|
|
28
|
-
the resulting class instances.
|
|
29
|
-
|
|
30
|
-
Args:
|
|
31
|
-
function (callable): The function whose result is to be sorted.
|
|
32
|
-
|
|
33
|
-
Returns:
|
|
34
|
-
callable: A function that will execute the input function and sort its result.
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
@wraps(function)
|
|
38
|
-
def wrapper(*args, **kwargs):
|
|
39
|
-
"""
|
|
40
|
-
Wrapper function that applies the sorting logic to the result of the decorated function.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
*args: Positional arguments to be passed to the decorated function.
|
|
44
|
-
**kwargs: Keyword arguments to be passed to the decorated function.
|
|
45
|
-
|
|
46
|
-
Returns:
|
|
47
|
-
The sorted result of the decorated function.
|
|
48
|
-
|
|
49
|
-
"""
|
|
50
|
-
result = function(*args, **kwargs)
|
|
51
|
-
return result.order_by(result.class_.name)
|
|
52
|
-
|
|
53
|
-
return wrapper
|
|
54
|
-
|
|
55
|
-
|
|
56
23
|
class WordSelector(BaseSelector): # pylint: disable=too-many-ancestors
|
|
57
24
|
"""
|
|
58
25
|
Class to extract words from a database based on various criteria.
|
|
@@ -60,44 +27,39 @@ class WordSelector(BaseSelector): # pylint: disable=too-many-ancestors
|
|
|
60
27
|
Extends the SQLAlchemy Select class to provide additional functionality.
|
|
61
28
|
"""
|
|
62
29
|
|
|
63
|
-
def __init__(
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
model: Type[BaseWord] = BaseWord,
|
|
33
|
+
is_sqlite: bool = False,
|
|
34
|
+
case_sensitive: bool = False,
|
|
35
|
+
disable_model_check: bool = False,
|
|
36
|
+
):
|
|
64
37
|
"""
|
|
65
|
-
|
|
38
|
+
Initializes the WordSelector object with the provided parameters.
|
|
66
39
|
|
|
67
40
|
Args:
|
|
68
|
-
|
|
41
|
+
model (Type[BaseWord]): The class to be used as the base key.
|
|
42
|
+
Must be a subclass of BaseWord.
|
|
69
43
|
is_sqlite (bool): If SQLite is being used. Defaults to False.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
raise ValueError(
|
|
73
|
-
f"Provided attribute class_={class_} is not a {BaseWord} or its child"
|
|
74
|
-
)
|
|
75
|
-
super().__init__(class_)
|
|
76
|
-
self.class_ = class_
|
|
77
|
-
self.is_sqlite = is_sqlite
|
|
44
|
+
case_sensitive (bool): If the queries should be case-sensitive.
|
|
45
|
+
disable_model_check (bool): If the model check is disabled during initialization.
|
|
78
46
|
|
|
79
|
-
|
|
47
|
+
Raises:
|
|
48
|
+
ValueError: If the provided model is not a subclass of BaseWord.
|
|
80
49
|
"""
|
|
81
|
-
Adds relationships to the query.
|
|
82
50
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
51
|
+
super().__init__(
|
|
52
|
+
model=model,
|
|
53
|
+
is_sqlite=is_sqlite,
|
|
54
|
+
case_sensitive=case_sensitive,
|
|
55
|
+
disable_model_check=disable_model_check,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if not self.disable_model_check:
|
|
59
|
+
self._is_model_accepted(model, BaseWord)
|
|
60
|
+
|
|
61
|
+
self.model = model
|
|
86
62
|
|
|
87
|
-
Returns:
|
|
88
|
-
Self: A query with the relationships added.
|
|
89
|
-
"""
|
|
90
|
-
available_relationships = {
|
|
91
|
-
attr: getattr(self.class_, attr) for attr in self.class_.relationships()
|
|
92
|
-
}
|
|
93
|
-
relationships = {
|
|
94
|
-
selectinload(v)
|
|
95
|
-
for k, v in available_relationships.items()
|
|
96
|
-
if not selected or k in selected
|
|
97
|
-
}
|
|
98
|
-
return self.options(*relationships)
|
|
99
|
-
|
|
100
|
-
@order_by_name
|
|
101
63
|
def by_event(self, event_id: int | None = None) -> Self:
|
|
102
64
|
"""
|
|
103
65
|
Applies a filter to select words associated with a specific event.
|
|
@@ -108,65 +70,36 @@ class WordSelector(BaseSelector): # pylint: disable=too-many-ancestors
|
|
|
108
70
|
Returns:
|
|
109
71
|
Self: A query with the filter applied.
|
|
110
72
|
"""
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
@order_by_name
|
|
114
|
-
def by_attributes(
|
|
115
|
-
self,
|
|
116
|
-
case_sensitive: bool = False,
|
|
117
|
-
**kwargs,
|
|
118
|
-
) -> Self:
|
|
119
|
-
"""
|
|
120
|
-
Selects all words by a set of attributes.
|
|
121
|
-
|
|
122
|
-
Args:
|
|
123
|
-
case_sensitive (bool): Whether the search should be case-sensitive.
|
|
124
|
-
Defaults to False.
|
|
125
|
-
**kwargs: A set of attributes to filter by.
|
|
73
|
+
self._statement = self._statement.where(filter_word_by_event_id(event_id))
|
|
74
|
+
return self
|
|
126
75
|
|
|
127
|
-
Returns:
|
|
128
|
-
Self: A query with the filter applied.
|
|
129
|
-
"""
|
|
130
|
-
# pylint: disable=no-member
|
|
131
|
-
return (
|
|
132
|
-
super() # type:ignore
|
|
133
|
-
.__get__(self, type(self))
|
|
134
|
-
.by_attrs(
|
|
135
|
-
class_=self.class_,
|
|
136
|
-
is_sqlite=self.is_sqlite,
|
|
137
|
-
case_sensitive=case_sensitive,
|
|
138
|
-
**kwargs,
|
|
139
|
-
)
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
@order_by_name
|
|
143
76
|
def by_name(
|
|
144
77
|
self,
|
|
145
78
|
name: str,
|
|
146
|
-
case_sensitive: bool = False,
|
|
147
79
|
) -> Self:
|
|
148
80
|
"""
|
|
149
81
|
Applies a filter to select words by a specific name.
|
|
150
82
|
|
|
151
83
|
Args:
|
|
152
84
|
name (str): The name to filter by.
|
|
153
|
-
case_sensitive (bool): Whether the search should be case-sensitive.
|
|
154
85
|
Defaults to False.
|
|
155
86
|
Returns:
|
|
156
87
|
Self: A query with the filter applied.
|
|
157
88
|
"""
|
|
89
|
+
if hasattr(self.model, "name"):
|
|
90
|
+
condition = self._generate_column_condition(self.model.name, name)
|
|
91
|
+
else:
|
|
92
|
+
raise AttributeError(
|
|
93
|
+
f"{self.model.__name__} does not have a 'name' attribute"
|
|
94
|
+
)
|
|
158
95
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
name=name,
|
|
162
|
-
)
|
|
96
|
+
self._statement = self._statement.where(condition)
|
|
97
|
+
return self
|
|
163
98
|
|
|
164
|
-
@order_by_name
|
|
165
99
|
def by_key(
|
|
166
100
|
self,
|
|
167
101
|
key: BaseKey | str,
|
|
168
102
|
language: str | None = None,
|
|
169
|
-
case_sensitive: bool = False,
|
|
170
103
|
) -> Self:
|
|
171
104
|
"""
|
|
172
105
|
Applies a filter to select words by a specific key.
|
|
@@ -175,22 +108,23 @@ class WordSelector(BaseSelector): # pylint: disable=too-many-ancestors
|
|
|
175
108
|
key (BaseKey | str): The key to filter by.
|
|
176
109
|
It can either be an instance of BaseKey or a string.
|
|
177
110
|
language (str | None): The language of the key. Defaults to None.
|
|
178
|
-
case_sensitive (bool): Whether the search should be case-sensitive.
|
|
179
111
|
Defaults to False.
|
|
180
112
|
|
|
181
113
|
Returns:
|
|
182
114
|
Self: A query with the filter applied.
|
|
183
115
|
"""
|
|
184
116
|
|
|
185
|
-
definition_query = DefinitionSelector(
|
|
117
|
+
definition_query = DefinitionSelector(
|
|
118
|
+
is_sqlite=self.is_sqlite,
|
|
119
|
+
case_sensitive=self.case_sensitive,
|
|
120
|
+
).by_key(
|
|
186
121
|
key=key,
|
|
187
122
|
language=language,
|
|
188
|
-
case_sensitive=case_sensitive,
|
|
189
123
|
)
|
|
190
|
-
subquery = select(definition_query.subquery().c.word_id)
|
|
191
|
-
|
|
124
|
+
subquery = select(definition_query.get_statement().subquery().c.word_id)
|
|
125
|
+
self._statement = self._statement.where(self.model.id.in_(subquery))
|
|
126
|
+
return self
|
|
192
127
|
|
|
193
|
-
@order_by_name
|
|
194
128
|
def by_type(
|
|
195
129
|
self,
|
|
196
130
|
type_: BaseType | str | None = None,
|
|
@@ -215,7 +149,10 @@ class WordSelector(BaseSelector): # pylint: disable=too-many-ancestors
|
|
|
215
149
|
Self: A query with the filter applied.
|
|
216
150
|
"""
|
|
217
151
|
if isinstance(type_, BaseType):
|
|
218
|
-
|
|
152
|
+
self._statement = self._statement.join(BaseType).where(
|
|
153
|
+
BaseType.id == type_.id
|
|
154
|
+
)
|
|
155
|
+
return self
|
|
219
156
|
|
|
220
157
|
type_values = (
|
|
221
158
|
(BaseType.type_, type_),
|
|
@@ -227,11 +164,13 @@ class WordSelector(BaseSelector): # pylint: disable=too-many-ancestors
|
|
|
227
164
|
i[0].ilike(str(i[1]).replace("*", "%")) for i in type_values if i[1]
|
|
228
165
|
]
|
|
229
166
|
|
|
230
|
-
|
|
231
|
-
self
|
|
232
|
-
|
|
167
|
+
if not type_filters:
|
|
168
|
+
self._statement = self._statement
|
|
169
|
+
return self
|
|
170
|
+
|
|
171
|
+
self._statement = self._statement.join(BaseType).where(and_(*type_filters))
|
|
172
|
+
return self
|
|
233
173
|
|
|
234
|
-
@order_by_name
|
|
235
174
|
def get_derivatives_of(self, word_id: int) -> Self:
|
|
236
175
|
"""
|
|
237
176
|
Selects all words that are derived from the given word.
|
|
@@ -243,21 +182,15 @@ class WordSelector(BaseSelector): # pylint: disable=too-many-ancestors
|
|
|
243
182
|
Self: A query with the filter applied.
|
|
244
183
|
"""
|
|
245
184
|
|
|
246
|
-
derivative_ids_subquery = (
|
|
247
|
-
|
|
248
|
-
.select_from(
|
|
249
|
-
join(
|
|
250
|
-
t_connect_words,
|
|
251
|
-
self.class_,
|
|
252
|
-
t_connect_words.c.child_id == self.class_.id,
|
|
253
|
-
)
|
|
254
|
-
)
|
|
255
|
-
.where(t_connect_words.c.parent_id == word_id)
|
|
185
|
+
derivative_ids_subquery = select(t_connect_words.c.child_id).where(
|
|
186
|
+
t_connect_words.c.parent_id == word_id
|
|
256
187
|
)
|
|
257
188
|
|
|
258
|
-
|
|
189
|
+
self._statement = self._statement.where(
|
|
190
|
+
self.model.id.in_(derivative_ids_subquery)
|
|
191
|
+
)
|
|
192
|
+
return self
|
|
259
193
|
|
|
260
|
-
@order_by_name
|
|
261
194
|
def get_affixes_of(self, word_id: int) -> Self:
|
|
262
195
|
"""
|
|
263
196
|
Selects all affixes that are derived from the given word.
|
|
@@ -270,7 +203,6 @@ class WordSelector(BaseSelector): # pylint: disable=too-many-ancestors
|
|
|
270
203
|
"""
|
|
271
204
|
return self.get_derivatives_of(word_id).by_type(type_x="Affix")
|
|
272
205
|
|
|
273
|
-
@order_by_name
|
|
274
206
|
def get_complexes_of(self, word_id: int) -> Self:
|
|
275
207
|
"""
|
|
276
208
|
Selects all complexes that are derived from the given word.
|
|
@@ -282,7 +214,3 @@ class WordSelector(BaseSelector): # pylint: disable=too-many-ancestors
|
|
|
282
214
|
Self: A query with the filter applied.
|
|
283
215
|
"""
|
|
284
216
|
return self.get_derivatives_of(word_id).by_type(group="Cpx")
|
|
285
|
-
|
|
286
|
-
@property
|
|
287
|
-
def inherit_cache(self): # pylint: disable=missing-function-docstring
|
|
288
|
-
return True
|
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
|
|
9
9
|
from sqlalchemy import String, inspect, func, select
|
|
10
|
+
from sqlalchemy.ext.hybrid import hybrid_property
|
|
10
11
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
11
12
|
from sqlalchemy.orm import Session, registry as rg
|
|
12
13
|
from typing_extensions import Annotated
|
|
@@ -137,15 +138,19 @@ class BaseModel(DeclarativeBase):
|
|
|
137
138
|
)
|
|
138
139
|
return f"{self.__class__.__name__}({obj_str})"
|
|
139
140
|
|
|
140
|
-
@
|
|
141
|
-
def _filter_add_to_repr(k, v):
|
|
141
|
+
@classmethod
|
|
142
|
+
def _filter_add_to_repr(cls, k, v):
|
|
142
143
|
"""
|
|
143
144
|
Static method that filters out keys that start with "_" and keys
|
|
144
145
|
that are "created" or "updated" and keys without values from the
|
|
145
146
|
object's attributes. The method is used to generate a string
|
|
146
147
|
representation of the object. It is used internally by the __repr__.
|
|
147
148
|
"""
|
|
148
|
-
return
|
|
149
|
+
return (
|
|
150
|
+
not k.startswith("_")
|
|
151
|
+
and k not in ["created", "updated", *cls.relationships()]
|
|
152
|
+
and v
|
|
153
|
+
)
|
|
149
154
|
|
|
150
155
|
@classmethod
|
|
151
156
|
def get_by_id(cls, session: Session, cid: int):
|
|
@@ -205,7 +210,7 @@ class BaseModel(DeclarativeBase):
|
|
|
205
210
|
Returns:
|
|
206
211
|
set[str]: A set of strings with names of all attribute keys.
|
|
207
212
|
"""
|
|
208
|
-
return set(cls.__mapper__.attrs.keys())
|
|
213
|
+
return set(cls.__mapper__.attrs.keys()) | cls.hybrid_properties()
|
|
209
214
|
|
|
210
215
|
@classmethod
|
|
211
216
|
def attributes_basic(cls) -> set[str]:
|
|
@@ -218,7 +223,7 @@ class BaseModel(DeclarativeBase):
|
|
|
218
223
|
Returns:
|
|
219
224
|
set[str]: A set of strings with names of basic attributes.
|
|
220
225
|
"""
|
|
221
|
-
return set(cls.attributes_all() - cls.relationships())
|
|
226
|
+
return set(cls.attributes_all() - cls.relationships() - cls.hybrid_properties())
|
|
222
227
|
|
|
223
228
|
@classmethod
|
|
224
229
|
def attributes_extended(cls) -> set[str]:
|
|
@@ -258,7 +263,12 @@ class BaseModel(DeclarativeBase):
|
|
|
258
263
|
Returns:
|
|
259
264
|
set[str]: A set of strings with names of the foreign keys.
|
|
260
265
|
"""
|
|
261
|
-
return set(
|
|
266
|
+
return set(
|
|
267
|
+
cls.attributes_all()
|
|
268
|
+
- cls.relationships()
|
|
269
|
+
- cls.non_foreign_keys()
|
|
270
|
+
- cls.hybrid_properties()
|
|
271
|
+
)
|
|
262
272
|
|
|
263
273
|
@classmethod
|
|
264
274
|
def non_foreign_keys(cls) -> set[str]:
|
|
@@ -278,3 +288,16 @@ class BaseModel(DeclarativeBase):
|
|
|
278
288
|
column.name for column in columns if not column.foreign_keys
|
|
279
289
|
}
|
|
280
290
|
return non_foreign_keys
|
|
291
|
+
|
|
292
|
+
@classmethod
|
|
293
|
+
def hybrid_properties(cls) -> set[str]:
|
|
294
|
+
"""
|
|
295
|
+
Class method that computes the hybrid properties of the class.
|
|
296
|
+
|
|
297
|
+
It doesn’t require any parameters as it operates on the class itself.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
set[str]: A set of strings with names of the hybrid properties.
|
|
301
|
+
"""
|
|
302
|
+
inspector = inspect(cls).all_orm_descriptors
|
|
303
|
+
return {i.__name__ for i in inspector if isinstance(i, hybrid_property)}
|
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
import datetime
|
|
8
8
|
|
|
9
9
|
from sqlalchemy import ForeignKey, JSON
|
|
10
|
+
from sqlalchemy.ext.hybrid import hybrid_property
|
|
10
11
|
from sqlalchemy.orm import mapped_column, Mapped
|
|
11
12
|
from sqlalchemy.orm import relationship
|
|
12
13
|
|
|
@@ -104,6 +105,7 @@ class BaseWord(BaseModel):
|
|
|
104
105
|
type: Mapped[BaseType] = relationship(
|
|
105
106
|
foreign_keys=[type_id],
|
|
106
107
|
back_populates="words",
|
|
108
|
+
lazy="joined", # required for proper loading affixes and complexes
|
|
107
109
|
)
|
|
108
110
|
|
|
109
111
|
event_start_id: Mapped[int] = mapped_column(
|
|
@@ -153,3 +155,28 @@ class BaseWord(BaseModel):
|
|
|
153
155
|
secondaryjoin=(t_connect_words.c.parent_id == id),
|
|
154
156
|
back_populates="derivatives",
|
|
155
157
|
)
|
|
158
|
+
|
|
159
|
+
@hybrid_property
|
|
160
|
+
def affixes(self) -> list[BaseWord]:
|
|
161
|
+
"""
|
|
162
|
+
Hybrid property that returns a list of affixes that are derived from the word.
|
|
163
|
+
|
|
164
|
+
This property is a hybrid of a Python property and a SQLAlchemy expression.
|
|
165
|
+
It can be used as a property in Python code or as a column in a SQLAlchemy query.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
list[BaseWord]: A list of affixes that are derived from the word.
|
|
169
|
+
"""
|
|
170
|
+
return [child for child in self.derivatives if child.type.type_x == "Affix"]
|
|
171
|
+
|
|
172
|
+
@hybrid_property
|
|
173
|
+
def complexes(self) -> list[BaseWord]:
|
|
174
|
+
"""Hybrid property that returns a list of complexes derived from the word.
|
|
175
|
+
|
|
176
|
+
This property is a hybrid of a Python property and a SQLAlchemy expression.
|
|
177
|
+
It can be used as a property in Python code or as a column in a SQLAlchemy query.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
list[BaseWord]: A list of complexes derived from the word.
|
|
181
|
+
"""
|
|
182
|
+
return [child for child in self.derivatives if child.type.group == "Cpx"]
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
This module provides a base selector for SQLAlchemy
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
from typing import Any, Type
|
|
8
|
-
from typing_extensions import Self
|
|
9
|
-
|
|
10
|
-
from sqlalchemy import Select, BinaryExpression
|
|
11
|
-
from sqlalchemy.orm import Session, InstrumentedAttribute
|
|
12
|
-
|
|
13
|
-
from loglan_core.base import BaseModel
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class BaseSelector(Select): # pylint: disable=too-many-ancestors
|
|
17
|
-
"""
|
|
18
|
-
A custom base selector that inherits from SQLAlchemy's Select class.
|
|
19
|
-
This class provides methods to execute a session and fetch results
|
|
20
|
-
in different ways. It also provides a way to fetch many results.
|
|
21
|
-
|
|
22
|
-
Methods:
|
|
23
|
-
--------
|
|
24
|
-
execute(session: Session) -> ResultProxy:
|
|
25
|
-
Executes the session and returns the result.
|
|
26
|
-
|
|
27
|
-
all(session: Session) -> List[ResultRow]:
|
|
28
|
-
Executes the session and returns all the results.
|
|
29
|
-
|
|
30
|
-
scalar(session: Session) -> Any:
|
|
31
|
-
Executes the session and returns a scalar result.
|
|
32
|
-
|
|
33
|
-
fetchmany(session: Session, size: int | None = None) -> List[ResultRow]:
|
|
34
|
-
Executes the session and fetches a specified number of results.
|
|
35
|
-
|
|
36
|
-
condition_by_attribute(
|
|
37
|
-
class_: Type[BaseModel],
|
|
38
|
-
attr: InstrumentedAttribute | str,
|
|
39
|
-
value: Any,
|
|
40
|
-
is_sqlite: bool = False,
|
|
41
|
-
case_sensitive: bool = False,
|
|
42
|
-
) -> BinaryExpression:
|
|
43
|
-
Creates a filter to select items by a specific attribute value.
|
|
44
|
-
Support wildcard and case-sensitive search.
|
|
45
|
-
"""
|
|
46
|
-
|
|
47
|
-
def execute(self, session: Session):
|
|
48
|
-
"""
|
|
49
|
-
Executes the given session and returns the result.
|
|
50
|
-
|
|
51
|
-
Args:
|
|
52
|
-
session (Session): SQLAlchemy Session object.
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
ResultProxy: The result of the executed session.
|
|
56
|
-
"""
|
|
57
|
-
return session.execute(self)
|
|
58
|
-
|
|
59
|
-
def all(self, session: Session):
|
|
60
|
-
"""
|
|
61
|
-
Executes the given session and returns all the results as a list.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
session (Session): SQLAlchemy Session object.
|
|
65
|
-
|
|
66
|
-
Returns:
|
|
67
|
-
List[ResultRow]: All the results of the executed session.
|
|
68
|
-
"""
|
|
69
|
-
return self.execute(session).scalars().all()
|
|
70
|
-
|
|
71
|
-
def scalar(self, session: Session):
|
|
72
|
-
"""
|
|
73
|
-
Executes the given session and returns a scalar result.
|
|
74
|
-
|
|
75
|
-
Args:
|
|
76
|
-
session (Session): SQLAlchemy Session object.
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
Any: The scalar result of the executed session.
|
|
80
|
-
"""
|
|
81
|
-
return self.execute(session).scalar()
|
|
82
|
-
|
|
83
|
-
def fetchmany(self, session: Session, size: int | None = None):
|
|
84
|
-
"""
|
|
85
|
-
Executes the given session and fetches a specified number of results.
|
|
86
|
-
|
|
87
|
-
Args:
|
|
88
|
-
session (Session): SQLAlchemy Session object.
|
|
89
|
-
size (int, optional): Number of results to fetch. If None, fetches all results.
|
|
90
|
-
|
|
91
|
-
Returns:
|
|
92
|
-
List[ResultRow]: The fetched results.
|
|
93
|
-
"""
|
|
94
|
-
return self.execute(session).scalars().fetchmany(size)
|
|
95
|
-
|
|
96
|
-
@classmethod
|
|
97
|
-
def condition_by_attribute(
|
|
98
|
-
cls,
|
|
99
|
-
class_: Type[BaseModel],
|
|
100
|
-
attr: InstrumentedAttribute | str,
|
|
101
|
-
value: Any,
|
|
102
|
-
is_sqlite: bool = False,
|
|
103
|
-
case_sensitive: bool = False,
|
|
104
|
-
) -> BinaryExpression:
|
|
105
|
-
"""
|
|
106
|
-
Applies a filter to select words by a specific attribute value.
|
|
107
|
-
|
|
108
|
-
Args:
|
|
109
|
-
class_ (BaseModel): The class to select from.
|
|
110
|
-
attr (str): The attribute to filter by.
|
|
111
|
-
value (Any): The value of the attribute to filter by.
|
|
112
|
-
is_sqlite (bool): If SQLite is being used. Defaults to False.
|
|
113
|
-
case_sensitive (bool): Whether the search should be case-sensitive.
|
|
114
|
-
Defaults to False.
|
|
115
|
-
Returns:
|
|
116
|
-
BaseSelector: A query with the filter applied.
|
|
117
|
-
"""
|
|
118
|
-
|
|
119
|
-
cls._is_class_acceptable(class_)
|
|
120
|
-
attr = cls._get_attr(class_, attr)
|
|
121
|
-
|
|
122
|
-
value = str(value).replace("*", "%")
|
|
123
|
-
|
|
124
|
-
if case_sensitive:
|
|
125
|
-
return attr.op("GLOB")(value) if is_sqlite else attr.like(value)
|
|
126
|
-
return attr.ilike(value)
|
|
127
|
-
|
|
128
|
-
@staticmethod
|
|
129
|
-
def _get_attr(
|
|
130
|
-
class_: Type[BaseModel],
|
|
131
|
-
attr: InstrumentedAttribute | str,
|
|
132
|
-
) -> InstrumentedAttribute:
|
|
133
|
-
"""
|
|
134
|
-
Gets the attribute from the class.
|
|
135
|
-
|
|
136
|
-
Args:
|
|
137
|
-
class_ (BaseModel): The class to get the attribute from.
|
|
138
|
-
attr (str | InstrumentedAttribute): The attribute to get.
|
|
139
|
-
|
|
140
|
-
Raises:
|
|
141
|
-
AttributeError: If the attribute is not found in the class.
|
|
142
|
-
|
|
143
|
-
Returns:
|
|
144
|
-
InstrumentedAttribute: The attribute from the class.
|
|
145
|
-
"""
|
|
146
|
-
if isinstance(attr, str):
|
|
147
|
-
try:
|
|
148
|
-
return getattr(class_, attr)
|
|
149
|
-
except AttributeError as exc:
|
|
150
|
-
raise AttributeError(
|
|
151
|
-
f"Provided attribute={attr} is not an attribute of {class_}"
|
|
152
|
-
) from exc
|
|
153
|
-
return attr
|
|
154
|
-
|
|
155
|
-
@staticmethod
|
|
156
|
-
def _is_class_acceptable(class_: Type[BaseModel]):
|
|
157
|
-
"""
|
|
158
|
-
Checks if the class is an instance of BaseModel or its child.
|
|
159
|
-
|
|
160
|
-
Args:
|
|
161
|
-
class_ (Type[BaseModel]): The class to check.
|
|
162
|
-
|
|
163
|
-
Raises:
|
|
164
|
-
ValueError: If the class is not an instance of BaseModel or its child.
|
|
165
|
-
"""
|
|
166
|
-
if not issubclass(class_, BaseModel):
|
|
167
|
-
raise ValueError(
|
|
168
|
-
f"Provided class_={class_} is not a {BaseModel} or its child"
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
def by_attrs(
|
|
172
|
-
self,
|
|
173
|
-
class_: Type[BaseModel],
|
|
174
|
-
is_sqlite: bool = False,
|
|
175
|
-
case_sensitive: bool = False,
|
|
176
|
-
**kwargs,
|
|
177
|
-
) -> Self:
|
|
178
|
-
"""
|
|
179
|
-
Selects all words by a set of attributes.
|
|
180
|
-
|
|
181
|
-
Args:
|
|
182
|
-
class_ (Type[BaseModel]): The class to select from.
|
|
183
|
-
is_sqlite (bool): If SQLite is being used. Defaults to False.
|
|
184
|
-
case_sensitive (bool): Whether the search should be case-sensitive.
|
|
185
|
-
Defaults to False.
|
|
186
|
-
**kwargs: A set of attributes to filter by.
|
|
187
|
-
|
|
188
|
-
Returns:
|
|
189
|
-
BaseSelector: A query with the filter applied.
|
|
190
|
-
"""
|
|
191
|
-
return self.where(
|
|
192
|
-
*{
|
|
193
|
-
self.condition_by_attribute(
|
|
194
|
-
class_,
|
|
195
|
-
k,
|
|
196
|
-
v,
|
|
197
|
-
is_sqlite=is_sqlite,
|
|
198
|
-
case_sensitive=case_sensitive,
|
|
199
|
-
)
|
|
200
|
-
for k, v in kwargs.items()
|
|
201
|
-
}
|
|
202
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|