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.
Files changed (35) hide show
  1. {loglan_core-0.3.0 → loglan_core-0.4.2/Loglan_Core.egg-info}/PKG-INFO +2 -2
  2. {loglan_core-0.3.0/Loglan_Core.egg-info → loglan_core-0.4.2}/PKG-INFO +2 -2
  3. loglan_core-0.4.2/loglan_core/addons/base_selector.py +268 -0
  4. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/definition_selector.py +53 -37
  5. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/key_selector.py +37 -35
  6. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/word_selector.py +61 -133
  7. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/base.py +29 -6
  8. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/word.py +27 -0
  9. {loglan_core-0.3.0 → loglan_core-0.4.2}/setup.py +1 -1
  10. loglan_core-0.3.0/loglan_core/addons/base_selector.py +0 -202
  11. {loglan_core-0.3.0 → loglan_core-0.4.2}/LICENSE +0 -0
  12. {loglan_core-0.3.0 → loglan_core-0.4.2}/Loglan_Core.egg-info/SOURCES.txt +0 -0
  13. {loglan_core-0.3.0 → loglan_core-0.4.2}/Loglan_Core.egg-info/dependency_links.txt +0 -0
  14. {loglan_core-0.3.0 → loglan_core-0.4.2}/Loglan_Core.egg-info/requires.txt +0 -0
  15. {loglan_core-0.3.0 → loglan_core-0.4.2}/Loglan_Core.egg-info/top_level.txt +0 -0
  16. {loglan_core-0.3.0 → loglan_core-0.4.2}/README.md +0 -0
  17. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/__init__.py +0 -0
  18. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/__init__.py +0 -0
  19. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/export_word_converter.py +0 -0
  20. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/exporter.py +0 -0
  21. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/utils.py +0 -0
  22. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/word_linker.py +0 -0
  23. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/addons/word_sourcer.py +0 -0
  24. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/author.py +0 -0
  25. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/connect_tables.py +0 -0
  26. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/definition.py +0 -0
  27. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/event.py +0 -0
  28. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/key.py +0 -0
  29. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/setting.py +0 -0
  30. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/syllable.py +0 -0
  31. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/table_names.py +0 -0
  32. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/type.py +0 -0
  33. {loglan_core-0.3.0 → loglan_core-0.4.2}/loglan_core/word_spell.py +0 -0
  34. {loglan_core-0.3.0 → loglan_core-0.4.2}/pyproject.toml +0 -0
  35. {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.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.3.0.tar.gz
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.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.3.0.tar.gz
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 cast
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
- class_: The class to be used as the returned object.
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__(self, class_=BaseDefinition, is_sqlite: bool = False):
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 given parameters.
60
+ Initializes the DefinitionSelector object with the provided parameters.
54
61
 
55
62
  Args:
56
- class_ (Type[BaseDefinition]): The class to be used as the returned object.
57
- Must be a subclass of BaseDefinition.
58
- is_sqlite (bool): Whether the object is being used with SQLite or not.
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 class_ is not a subclass of BaseDefinition.
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
- @property
75
- def inherit_cache(self): # pylint: disable=C0116
76
- """
77
- :return: bool
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.class_.id)
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
- return cast(DefinitionSelector, self.where(self.class_.id.in_(subquery)))
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
- case_sensitive: bool = False,
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
- DefinitionSelector: The filtered DefinitionSelector instance with distinct keys.
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(search_key, case_sensitive, self.is_sqlite)
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
- statement = self.join(self.class_.keys).filter(filter_key, filter_language)
127
- return cast(DefinitionSelector, statement.distinct())
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) -> DefinitionSelector:
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
- DefinitionSelector: The filtered DefinitionSelector instance.
147
+ Self: The filtered DefinitionSelector instance.
139
148
  """
140
- filter_language = self.class_.language == language if language else true()
141
- return cast(DefinitionSelector, self.filter(filter_language))
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 cast
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
- class_ (Type[BaseKey]): The class to be used as the base key.
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): Indicator if the object is being used with SQLite or not.
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__(self, class_=BaseKey, is_sqlite: bool = False) -> None:
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
- class_ (Type[BaseKey]): The class to be used as the base key.
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): Indicator if the object is being used with SQLite or not.
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 class_ is not a subclass of BaseKey.
72
+ ValueError: If the provided model is not a subclass of BaseKey.
63
73
  """
64
- if not issubclass(class_, BaseKey):
65
- raise ValueError(
66
- f"Provided attribute class_={class_} is not a {BaseKey} or its child"
67
- )
68
- super().__init__(class_)
69
- self.class_ = class_
70
- self.is_sqlite = is_sqlite
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.class_.id)
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
- return cast(KeySelector, self.where(self.class_.id.in_(subquery)))
102
+ self._statement = self._statement.where(self.model.id.in_(subquery))
103
+ return self
101
104
 
102
- def by_key(self, key: str, case_sensitive: bool = False) -> KeySelector:
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
- return cast(
115
- KeySelector,
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
- return cast(KeySelector, self.where(filter_key_by_language(language)))
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
- return cast(
147
- KeySelector,
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 functools import wraps
10
- from typing import Type, Iterable
9
+ from typing import Type
11
10
 
12
- from sqlalchemy import and_, select, join
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__(self, class_: Type[BaseWord] = BaseWord, is_sqlite: bool = False):
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
- Initialize a WordSelector instance.
38
+ Initializes the WordSelector object with the provided parameters.
66
39
 
67
40
  Args:
68
- class_ (BaseWord): The class to select from. Defaults to BaseWord.
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
- if not issubclass(class_, BaseWord):
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
- def with_relationships(self, selected: Iterable[str] | None = None) -> Self:
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
- Args:
84
- selected (set[str]): A set of relationship names to include.
85
- Defaults to None if all relationships should be included.
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
- return self.where(filter_word_by_event_id(event_id))
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
- return self.by_attributes(
160
- case_sensitive=case_sensitive,
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(is_sqlite=self.is_sqlite).by_key(
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
- return self.where(self.class_.id.in_(subquery))
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
- return self.join(BaseType).where(BaseType.id == type_.id)
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
- return (
231
- self if not type_filters else self.join(BaseType).where(and_(*type_filters))
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
- select(self.class_.id)
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
- return self.where(self.class_.id.in_(derivative_ids_subquery))
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
- @staticmethod
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 not k.startswith("_") and k not in ["created", "updated"] and v
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(cls.attributes_all() - cls.relationships() - cls.non_foreign_keys())
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"]
@@ -4,7 +4,7 @@ from io import open
4
4
 
5
5
  from setuptools import setup
6
6
 
7
- CURRENT_VERSION = "0.3.0"
7
+ CURRENT_VERSION = "0.4.2"
8
8
 
9
9
 
10
10
  def read(filename): # pylint: disable=C0116
@@ -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