Passed
Push — main ( 2fdcc0...4cd557 )
by torrua
01:38
created

loglan_db.model_base.BaseWord.query_parents()   A

Complexity

Conditions 1

Size

Total Lines 10
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 10
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nop 1
crap 1
1
# -*- coding: utf-8 -*-
2
# pylint: disable=C0103, C0303
3
4 1
"""
5
This module contains a basic LOD dictionary model for a SQL database.
6
Each class is a detailed description of a db table:
7
Authors, Events, Keys, Definitions, Words, etc.
8
"""
9
10 1
from __future__ import annotations
11
12 1
import os
13 1
import re
14 1
from typing import List, Union, Optional
15
16 1
from flask_sqlalchemy import BaseQuery, SQLAlchemy
17 1
from sqlalchemy import exists, or_
18
19 1
from loglan_db import db, app_lod
20 1
from loglan_db.model_init import InitBase, DBBase
21
22 1
if os.environ.get("IS_PDOC", "False") == "True":
23
    db = SQLAlchemy(app_lod())
24
25
26 1
t_name_authors = "authors"
27
"""`str` : `__tablename__` value for `BaseAuthor` table"""
28
29 1
t_name_events = "events"
30
"""`str` : `__tablename__` value for `BaseEvent` table"""
31 1
t_name_keys = "keys"
32
"""`str` : `__tablename__` value for `BaseKey` table"""
33
34 1
t_name_settings = "settings"
35
"""`str` : `__tablename__` value for `BaseSetting` table"""
36
37 1
t_name_syllables = "syllables"
38
"""`str` : `__tablename__` value for `BaseSyllable` table"""
39
40 1
t_name_types = "types"
41
"""`str` : `__tablename__` value for `BaseType` table"""
42
43 1
t_name_words = "words"
44
"""`str` : `__tablename__` value for `BaseWord` table"""
45
46 1
t_name_definitions = "definitions"
47
"""`str` : `__tablename__` value for `BaseDefinition` table"""
48
49 1
t_name_word_spells = "word_spells"
50
"""`str` : `__tablename__` value for `BaseWordSpell` table"""
51
52 1
t_name_word_sources = "word_sources"
53
"""`str` : `__tablename__` value for `BaseWordSource` table"""
54
55 1
t_name_connect_authors = "connect_authors"
56
"""`str` : `__tablename__` value for `t_connect_authors` table"""
57
58 1
t_name_connect_words = "connect_words"
59
"""`str` : `__tablename__` value for `t_connect_words` table"""
60
61 1
t_name_connect_keys = "connect_keys"
62
"""`str` : `__tablename__` value for `t_connect_keys` table"""
63
64 1
__pdoc__ = {
65
    'BaseEvent.appeared_words':
66
        """*Relationship query for getting a list of words appeared during this event*
67
68
    **query** : Optional[List[BaseWord]]""",
69
70
    'BaseEvent.deprecated_words':
71
        """*Relationship query for getting a list of words deprecated during this event*
72
73
    **query** : Optional[List[BaseWord]]""",
74
75
    'BaseAuthor.contribution':
76
        """*Relationship query for getting a list of words coined by this author*
77
78
    **query** : Optional[List[BaseWord]]""",
79
80
    'BaseType.words': 'words',
81
    'BaseDefinition.source_word': 'source_word',
82
    'BaseKey.definitions':
83
        """*Relationship query for getting a list of definitions related to this key*
84
85
    **query** : Optional[List[BaseDefinition]]""",
86
87
    'BaseAuthor.created': False, 'BaseAuthor.updated': False,
88
    'BaseEvent.created': False, 'BaseEvent.updated': False,
89
    'BaseKey.created': False, 'BaseKey.updated': False,
90
    'BaseSetting.created': False, 'BaseSetting.updated': False,
91
    'BaseSyllable.created': False, 'BaseSyllable.updated': False,
92
    'BaseType.created': False, 'BaseType.updated': False,
93
    'BaseDefinition.created': False, 'BaseDefinition.updated': False,
94
    'BaseWord.created': False, 'BaseWord.updated': False,
95
}
96
97 1
t_connect_authors = db.Table(
98
    t_name_connect_authors, db.metadata,
99
    db.Column('AID', db.ForeignKey(f'{t_name_authors}.id'), primary_key=True),
100
    db.Column('WID', db.ForeignKey(f'{t_name_words}.id'), primary_key=True), )
101
"""`(sqlalchemy.sql.schema.Table)`: 
102
Connecting table for "many-to-many" relationship 
103
between `BaseAuthor` and `BaseWord` objects"""
104
105 1
t_connect_words = db.Table(
106
    t_name_connect_words, db.metadata,
107
    db.Column('parent_id', db.ForeignKey(f'{t_name_words}.id'), primary_key=True),
108
    db.Column('child_id', db.ForeignKey(f'{t_name_words}.id'), primary_key=True), )
109
"""`(sqlalchemy.sql.schema.Table)`: 
110
Connecting table for "many-to-many" relationship 
111
(parent-child) between `BaseWord` objects"""
112
113 1
t_connect_keys = db.Table(
114
    t_name_connect_keys, db.metadata,
115
    db.Column('KID', db.ForeignKey(f'{t_name_keys}.id'), primary_key=True),
116
    db.Column('DID', db.ForeignKey(f'{t_name_definitions}.id'), primary_key=True), )
117
"""`(sqlalchemy.sql.schema.Table)`: 
118
Connecting table for "many-to-many" relationship 
119
between `BaseDefinition` and `BaseKey` objects"""
120
121
122 1
class BaseAuthor(db.Model, InitBase, DBBase):
123
    """Base Author's DB Model
124
125
    Describes a table structure for storing information about words authors.
126
127
    Connects with words with "many-to-many" relationship. See `t_connect_authors`.
128
129
    <details><summary>Show Examples</summary><p>
130
    ```python
131
    {'id': 13, 'full_name': 'James Cooke Brown',
132
    'abbreviation': 'JCB', 'notes': ''}
133
134
    {'id': 29, 'full_name': 'Loglan 4&5',
135
    'abbreviation': 'L4',
136
    'notes': 'The printed-on-paper book,
137
              1975 version of the dictionary.'}
138
    ```
139
    </p></details>
140
    """
141
142 1
    __tablename__ = t_name_authors
143
144 1
    id = db.Column(db.Integer, primary_key=True)
145
    """*Author's internal ID number*  
146
        **int** : primary_key=True"""
147
148 1
    abbreviation = db.Column(db.String(64), nullable=False, unique=True)
149
    """*Author's abbreviation (used in the LOD dictionary)*  
150
        **str** : max_length=64, nullable=False, unique=True
151
    Example:
152
        > JCB, L4
153
    """
154
155 1
    full_name = db.Column(db.String(64), nullable=True, unique=False)
156
    """*Author's full name (if exists)*  
157
        **str** : max_length=64, nullable=True, unique=False
158
    Example:
159
        > James Cooke Brown, Loglan 4&5
160
    """
161
162 1
    notes = db.Column(db.String(128), nullable=True, unique=False)
163 1
    """*Any additional information about author*  
164
        **str** : max_length=128, nullable=True, unique=False
165
    """
166
167
168 1
class BaseEvent(db.Model, InitBase, DBBase):
169
    """Base Event's DB Model
170
171
    Describes a table structure for storing information about lexical events.
172
173
    <details><summary>Show Examples</summary><p>
174
    ```python
175
    {'suffix': 'INIT', 'definition': 'The initial vocabulary before updates.',
176
     'date': datetime.date(1975, 1, 1), 'annotation': 'Initial', 'name': 'Start', 'id': 1}
177
178
    {'suffix': 'RDC', 'definition': 'parsed all the words in the dictionary,
179
    identified ones that the parser did not recognize as words',
180
    'date': datetime.date(2016, 1, 15), 'annotation': 'Randall Cleanup',
181
    'name': 'Randall Dictionary Cleanup', 'id': 5}
182
    ```
183
    </p></details>
184
    """
185 1
    __tablename__ = t_name_events
186
187 1
    id = db.Column(db.Integer, primary_key=True)
188
    """*Event's internal ID number*  
189
        **int** : primary_key=True"""
190 1
    date = db.Column(db.Date, nullable=False, unique=False)
191
    """*Event's starting day*  
192
        **dateime.date** : nullable=False, unique=False"""
193 1
    name = db.Column(db.String(64), nullable=False, unique=False)
194
    """*Event's short name*  
195
        **str** : max_length=64, nullable=False, unique=False"""
196 1
    definition = db.Column(db.Text, nullable=False, unique=False)
197
    """*Event's definition*  
198
        **str** : nullable=False, unique=False"""
199 1
    annotation = db.Column(db.String(16), nullable=False, unique=False)
200
    """*Event's annotation (displayed in old format dictionary HTML file)*  
201
        **str** : max_length=16, nullable=False, unique=False"""
202 1
    suffix = db.Column(db.String(16), nullable=False, unique=False)
203
    """*Event's suffix (used to create filename when exporting HTML file)*  
204
        **str** : max_length=16, nullable=False, unique=False"""
205
206 1
    @classmethod
207
    def latest(cls) -> BaseEvent:
208
        """
209
        Gets the latest (current) `BaseEvent` from DB
210
        """
211 1
        return cls.query.order_by(-cls.id).first()
212
213
214 1
class BaseKey(db.Model, InitBase, DBBase):
215
    """Base Key's DB Model
216
217
    Describes a table structure for storing information
218
    about key words of the word's definitions.
219
    Some key words could belong to many definitions
220
    and some definitions could have many key words.
221
    That's why the relationship between Key
222
    and Definition should be many-to-many. See `t_connect_keys`.
223
224
    There is additional `word_language` UniqueConstraint here.
225
226
    <details><summary>Show Examples</summary><p>
227
    ```python
228
    {'language': 'en', 'word': 'aura', 'id': 1234}
229
230
    {'language': 'en', 'word': 'emotionality', 'id': 4321}
231
    ```
232
    </p></details>
233
    """
234 1
    __tablename__ = t_name_keys
235 1
    __table_args__ = (
236
        db.UniqueConstraint('word', 'language', name='_word_language_uc'), )
237
238 1
    id = db.Column(db.Integer, primary_key=True)
239
    """*Key's internal ID number*  
240
        **int** : primary_key=True"""
241 1
    word = db.Column(db.String(64), nullable=False, unique=False)
242
    """*Key's vernacular word*  
243
        **str** : max_length=64, nullable=False, unique=False  
244
    It is non-unique, as words can be the same in spelling in different languages"""
245 1
    language = db.Column(db.String(16), nullable=False, unique=False)
246 1
    """*Key's language*  
247
        **str** : max_length=16, nullable=False, unique=False"""
248
249
250 1
class BaseSetting(db.Model, InitBase, DBBase):
251
    """Base Setting's DB Model
252
253
    Describes a table structure for storing dictionary settings.
254
255
    <details><summary>Show Examples</summary><p>
256
    ```python
257
    {'id': 1, 'last_word_id': 10141,
258
    'date': datetime.datetime(2020, 10, 25, 5, 10, 20),
259
    'db_release': '4.5.9', 'db_version': 2}
260
    ```
261
    </p></details>
262
    """
263 1
    __tablename__ = t_name_settings
264
265 1
    id = db.Column(db.Integer, primary_key=True)
266
    """*Setting's internal ID number*  
267
        **int** : primary_key=True"""
268 1
    date = db.Column(db.DateTime, nullable=True, unique=False)
269
    """*Last modified date*  
270
        **dateime.datetime** : nullable=True, unique=False"""
271 1
    db_version = db.Column(db.Integer, nullable=False, unique=False)
272
    """*Database version (for old application)*  
273
        **int** : nullable=False, unique=False"""
274 1
    last_word_id = db.Column(db.Integer, nullable=False, unique=False)
275
    """*ID number of the last word in DB*  
276
            **int** : nullable=False, unique=False"""
277 1
    db_release = db.Column(db.String(16), nullable=False)
278 1
    """*Database release (for new application)*  
279
            **str** : max_length=16, nullable=False, unique=True"""
280
281
282 1
class BaseSyllable(db.Model, InitBase, DBBase):
283
    """Base Syllable's DB Model
284
285
    Describes a table structure for storing information about loglan syllables.
286
287
    <details><summary>Show Examples</summary><p>
288
    ```python
289
    {'id': 37, 'name': 'zv', 'type': 'InitialCC', 'allowed': True}
290
291
    {'id': 38, 'name': 'cdz', 'type': 'UnintelligibleCCC', 'allowed': False}
292
    ```
293
    </p></details>
294
    """
295
296 1
    __tablename__ = t_name_syllables
297
298 1
    id = db.Column(db.Integer, primary_key=True)
299
    """*Syllable's internal ID number*  
300
        **int** : primary_key=True"""
301 1
    name = db.Column(db.String(8), nullable=False, unique=False)
302
    """*Syllable itself*  
303
            **str** : max_length=8, nullable=False, unique=False"""
304 1
    type = db.Column(db.String(32), nullable=False, unique=False)
305
    """*Syllable's type*  
306
            **str** : max_length=8, nullable=False, unique=False"""
307 1
    allowed = db.Column(db.Boolean, nullable=True, unique=False)
308 1
    """*Is this syllable acceptable in grammar*  
309
            **bool** : nullable=True, unique=False"""
310
311
312 1
class BaseType(db.Model, InitBase, DBBase):
313
    """BaseType model"""
314 1
    __tablename__ = t_name_types
315
316 1
    id = db.Column(db.Integer, primary_key=True)
317
    """Type's internal ID number: Integer - E.g. 4, 8"""
318
319 1
    type = db.Column(db.String(16), nullable=False)  # E.g. 2-Cpx, C-Prim
320 1
    type_x = db.Column(db.String(16), nullable=False)  # E.g. Predicate, Predicate
321 1
    group = db.Column(db.String(16))  # E.g. Cpx, Prim
322 1
    parentable = db.Column(db.Boolean, nullable=False)  # E.g. True, False
323 1
    description = db.Column(db.String(255))  # E.g. Two-term Complex, ...
324
325 1
    @classmethod
326
    def by(cls, type_filter: Union[str, List[str]]) -> BaseQuery:
327
        """
328
329
        Args:
330
          type_filter: Union[str, List[str]]:
331
332
        Returns:
333
334
        """
335 1
        type_filter = [type_filter, ] if isinstance(type_filter, str) else type_filter
336
337 1
        return cls.query.filter(or_(
338
            cls.type.in_(type_filter), cls.type_x.in_(type_filter), cls.group.in_(type_filter), ))
339
340
341 1
class BaseDefinition(db.Model, InitBase, DBBase):
342
    """BaseDefinition model"""
343 1
    __tablename__ = t_name_definitions
344
345 1
    id = db.Column(db.Integer, primary_key=True)
346
    """Definition's internal ID number: Integer"""
347
348 1
    word_id = db.Column(db.Integer, db.ForeignKey(f'{t_name_words}.id'), nullable=False)
349 1
    position = db.Column(db.Integer, nullable=False)
350 1
    usage = db.Column(db.String(64))
351 1
    grammar_code = db.Column(db.String(8))
352 1
    slots = db.Column(db.Integer)
353 1
    case_tags = db.Column(db.String(16))
354 1
    body = db.Column(db.Text, nullable=False)
355 1
    language = db.Column(db.String(16))
356 1
    notes = db.Column(db.String(255))
357
358 1
    APPROVED_CASE_TAGS = ["B", "C", "D", "F", "G", "J", "K", "N", "P", "S", "V", ]
359 1
    KEY_PATTERN = r"(?<=\«)(.+?)(?=\»)"
360
361 1
    keys = db.relationship(BaseKey.__name__, secondary=t_connect_keys,
362
                           backref="definitions", lazy='dynamic', enable_typechecks=False)
363
364 1
    @property
365
    def grammar(self) -> str:
366
        """
367
        Combine definition's 'slots' and 'grammar_code' attributes
368
369
        Returns:
370
            String with grammar data like (3v) or (2n)
371
        """
372 1
        return f"({self.slots if self.slots else ''}" \
373
               f"{self.grammar_code if self.grammar_code else ''})"
374
375 1
    def link_keys_from_list_of_str(
376
            self, source: List[str],
377
            language: str = None) -> List[BaseKey]:
378
        """Linking a list of vernacular words with BaseDefinition
379
        Only new words will be linked, skipping those that were previously linked
380
381
        Args:
382
          source: List[str]: List of words on vernacular language
383
          language: str: Language of source words (Default value = None)
384
385
        Returns:
386
          List of linked BaseKey objects
387
388
        """
389
390 1
        language = language if language else self.language
391
392 1
        new_keys = BaseKey.query.filter(
393
            BaseKey.word.in_(source),
394
            BaseKey.language == language,
395
            ~exists().where(BaseKey.id == self.keys.subquery().c.id),
396
        ).all()
397
398 1
        self.keys.extend(new_keys)
399 1
        return new_keys
400
401 1
    def link_key_from_str(self, word: str, language: str = None) -> Optional[BaseKey]:
402
        """Linking vernacular word with BaseDefinition object
403
        Only new word will be linked, skipping this that was previously linked
404
405
        Args:
406
          word: str: name of BaseWord on vernacular language
407
          language: str: BaseWord's language (Default value = None)
408
409
        Returns:
410
          Linked BaseKey object or None if it were already linked
411
412
        """
413 1
        language = language if language else self.language
414 1
        result = self.link_keys_from_list_of_str(source=[word, ], language=language)
415 1
        return result[0] if result else None
416
417 1
    def link_keys_from_definition_body(
418
            self, language: str = None,
419
            pattern: str = KEY_PATTERN) -> List[BaseKey]:
420
        """Extract and link keys from BaseDefinition's body
421
422
        Args:
423
          language: str: Language of BaseDefinition's keys (Default value = None)
424
          pattern: str: Regex pattern for extracting keys from the BaseDefinition's body
425
            (Default value = KEY_PATTERN)
426
427
        Returns:
428
          List of linked BaseKey objects
429
430
        """
431 1
        language = language if language else self.language
432 1
        keys = re.findall(pattern, self.body)
433 1
        return self.link_keys_from_list_of_str(source=keys, language=language)
434
435 1
    def link_keys(
436
            self, source: Union[List[str], str, None] = None,
437
            language: str = None, pattern: str = KEY_PATTERN) -> Optional[BaseKey, List[BaseKey]]:
438
        """Universal method for linking all available types of key sources with BaseDefinition
439
440
        Args:
441
          source: Union[List[str], str, None]:
442
            If no source is provided, keys will be extracted from the BaseDefinition's body
443
            If source is a string or a list of strings, the language of the keys must be specified
444
            TypeError will be raised if the source contains inappropriate data
445
            (Default value = None)
446
          language: str: Language of BaseDefinition's keys (Default value = None)
447
          pattern: str: Regex pattern for extracting keys from the BaseDefinition's body
448
            (Default value = KEY_PATTERN)
449
450
        Returns:
451
          None, BaseKey, or List of BaseKeys
452
453
        """
454
455 1
        language = language if language else self.language
456
457 1
        if not source:
458 1
            return self.link_keys_from_definition_body(language=language, pattern=pattern)
459
460 1
        if isinstance(source, str):
461 1
            return self.link_key_from_str(word=source, language=language)
462
463 1
        if isinstance(source, list) and all(isinstance(item, str) for item in source):
464 1
            return self.link_keys_from_list_of_str(source=source, language=language)
465
466 1
        raise TypeError("Source for keys should have a string, or list of strings."
467
                        "You input %s" % type(source))
468
469 1
    @classmethod
470 1
    def by_key(
471
            cls, key: Union[BaseKey, str],
472
            language: str = None,
473
            case_sensitive: bool = False,
474
            partial_results: bool = False,
475
    ) -> BaseQuery:
476
        """Definition.Query filtered by specified key
477
478
        Args:
479
          key: Union[BaseKey, str]:
480
          language: str: Language of key (Default value = None)
481
          case_sensitive: bool:  (Default value = False)
482
          partial_results: bool:  (Default value = False)
483
484
        Returns:
485
          BaseQuery
486
487
        """
488
489 1
        key = BaseKey.word if isinstance(key, BaseKey) else str(key)
490 1
        request = cls.query.join(t_connect_keys, BaseKey).order_by(BaseKey.word)
491
492 1
        if language:
493 1
            request = request.filter(BaseKey.language == language)
494
495 1
        if case_sensitive:
496 1
            if partial_results:
497 1
                return request.filter(BaseKey.word.like(f"{key}%"))
498 1
            return request.filter(BaseKey.word == key)
499
500 1
        if partial_results:
501 1
            return request.filter(BaseKey.word.ilike(f"{key}%"))
502
503 1
        return request.filter(BaseKey.word.ilike(key))
504
505
506 1
class BaseWord(db.Model, InitBase, DBBase):
507
    """BaseWord model"""
508 1
    __tablename__ = t_name_words
509
510 1
    id = db.Column(db.Integer, primary_key=True)
511
    """Word's internal ID number: Integer"""
512
513 1
    id_old = db.Column(db.Integer, nullable=False)  # Compatibility with the previous database
514 1
    name = db.Column(db.String(64), nullable=False)
515 1
    origin = db.Column(db.String(128))
516 1
    origin_x = db.Column(db.String(64))
517 1
    match = db.Column(db.String(8))
518 1
    rank = db.Column(db.String(8))
519 1
    year = db.Column(db.Date)
520 1
    notes = db.Column(db.JSON)
521 1
    TID_old = db.Column(db.Integer)  # references
522
523 1
    type_id = db.Column("type", db.ForeignKey(f'{t_name_types}.id'), nullable=False)
524 1
    type: BaseType = db.relationship(
525
        BaseType.__name__, backref="words", enable_typechecks=False)
526
527 1
    event_start_id = db.Column(
528
        "event_start", db.ForeignKey(f'{t_name_events}.id'), nullable=False)
529 1
    event_start: BaseEvent = db.relationship(
530
        BaseEvent.__name__, foreign_keys=[event_start_id],
531
        backref="appeared_words", enable_typechecks=False)
532
533 1
    event_end_id = db.Column("event_end", db.ForeignKey(f'{t_name_events}.id'))
534 1
    event_end: BaseEvent = db.relationship(
535
        BaseEvent.__name__, foreign_keys=[event_end_id],
536
        backref="deprecated_words", enable_typechecks=False)
537
538 1
    authors: BaseQuery = db.relationship(
539
        BaseAuthor.__name__, secondary=t_connect_authors,
540
        backref="contribution", lazy='dynamic', enable_typechecks=False)
541
542 1
    definitions: BaseQuery = db.relationship(
543
        BaseDefinition.__name__, backref="source_word",
544
        lazy='dynamic', enable_typechecks=False)
545
546
    # word's derivatives
547 1
    __derivatives = db.relationship(
548
        'BaseWord', secondary=t_connect_words,
549
        primaryjoin=(t_connect_words.c.parent_id == id),
550
        secondaryjoin=(t_connect_words.c.child_id == id),
551
        backref=db.backref('_parents', lazy='dynamic', enable_typechecks=False),
552
        lazy='dynamic', enable_typechecks=False)
553
554 1
    def __is_parented(self, child: BaseWord) -> bool:
555
        """
556
        Check, if this word is already added as a parent for this 'child'
557
558
        Args:
559
            child: BaseWord: BaseWord object to check
560
561
        Returns: bool:
562
563
        """
564 1
        return self.__derivatives.filter(t_connect_words.c.child_id == child.id).count() > 0
565
566 1
    def add_child(self, child: BaseWord) -> str:
567
        """Add derivative for the source word
568
        Get words from Used In and add relationship in database
569
570
        Args:
571
          child: BaseWord: Object to add
572
573
        Returns:
574
            String with the name of the added child (BaseWord.name)
575
576
        """
577
        # TODO add check if type of child is allowed to add to this word
578 1
        if not self.__is_parented(child):
579 1
            self.__derivatives.append(child)
580 1
        return child.name
581
582 1
    def add_children(self, children: List[BaseWord]):
583
        """Add derivatives for the source word
584
        Get words from Used In and add relationship in database
585
586
        Args:
587
          children: List[BaseWord]:
588
589
        Returns:
590
          None
591
592
        """
593
        # TODO add check if type of child is allowed to add to this word
594 1
        new_children = list(set(children) - set(self.__derivatives))
595 1
        _ = self.__derivatives.extend(new_children) if new_children else None
596
597 1
    def add_author(self, author: BaseAuthor) -> str:
598
        """Connect Author object with BaseWord object
599
600
        Args:
601
          author: BaseAuthor:
602
603
        Returns:
604
605
        """
606 1
        if not self.authors.filter(BaseAuthor.abbreviation == author.abbreviation).count() > 0:
607 1
            self.authors.append(author)
608 1
        return author.abbreviation
609
610 1
    def add_authors(self, authors: List[BaseAuthor]):
611
        """Connect Author objects with BaseWord object
612
613
        Args:
614
          authors: List[BaseAuthor]:
615
616
        Returns:
617
618
        """
619 1
        new_authors = list(set(authors) - set(self.authors))
620 1
        _ = self.authors.extend(new_authors) if new_authors else None
621
622 1
    def query_derivatives(self, word_type: str = None,
623
                          word_type_x: str = None, word_group: str = None) -> BaseQuery:
624
        """Query to get all derivatives of the word, depending on its parameters
625
626
        Args:
627
          word_type: str:  (Default value = None)
628
          word_type_x: str:  (Default value = None)
629
          word_group: str:  (Default value = None)
630
631
        Returns:
632
            BaseQuery
633
        """
634 1
        result = self.__derivatives.filter(self.id == t_connect_words.c.parent_id)
635
636 1
        if word_type or word_type_x or word_group:
637 1
            result = result.join(BaseType)
638
639 1
        if word_type:
640 1
            result = result.filter(BaseType.type == word_type)
641 1
        if word_type_x:
642 1
            result = result.filter(BaseType.type_x == word_type_x)
643 1
        if word_group:
644 1
            result = result.filter(BaseType.group == word_group)
645
646 1
        return result.order_by(BaseWord.name.asc())
647
648 1
    def query_parents(self) -> BaseQuery:
649
        """Query to get all parents of the Complexes, Little words or Affixes
650
        :return: Query
651
652
        Args:
653
654
        Returns:
655
            BaseQuery
656
        """
657 1
        return self._parents  # if self.type in self.__parentable else []
658
659 1
    def query_cpx(self) -> BaseQuery:
660
        """Query to qet all the complexes that exist for this word
661
        Only primitives have affixes
662
663
        Args:
664
665
        Returns:
666
            BaseQuery
667
        """
668 1
        return self.query_derivatives(word_group="Cpx")
669
670 1
    def query_afx(self) -> BaseQuery:
671
        """Query to qet all the affixes that exist for this word
672
        Only primitives have affixes
673
674
        Args:
675
676
        Returns:
677
            BaseQuery
678
        """
679 1
        return self.query_derivatives(word_type="Afx")
680
681 1
    def query_keys(self) -> BaseQuery:
682
        """Query for the BaseKeys linked with this BaseWord
683
684
        Args:
685
686
        Returns:
687
            BaseQuery
688
        """
689 1
        return BaseKey.query.join(
690
            t_connect_keys, BaseDefinition, BaseWord).filter(BaseWord.id == self.id)
691
692 1
    @property
693
    def parents(self) -> List[BaseWord]:
694
        """Get all parents of the Complexes, Little words or Affixes
695
696
        Args:
697
698
        Returns:
699
            List[BaseWord]
700
        """
701 1
        return self.query_parents().all()
702
703 1
    @property
704
    def complexes(self) -> List[BaseWord]:
705
        """Get all word's complexes if exist
706
707
        Args:
708
709
        Returns:
710
            List[BaseWord]
711
        """
712 1
        return self.query_cpx().all()
713
714 1
    @property
715
    def affixes(self) -> List[BaseWord]:
716
        """Get all word's affixes if exist
717
718
        Args:
719
720
        Returns:
721
            List[BaseWord]
722
        """
723 1
        return self.query_afx().all()
724
725 1
    @property
726
    def keys(self) -> List[BaseKey]:
727
        """Get all BaseKey object related to this BaseWord
728
        Keep in mind that duplicate keys for different definitions
729
        will not be added to the final result
730
731
        Args:
732
733
        Returns:
734
            List[BaseKey]
735
        """
736 1
        return self.query_keys().all()
737
738 1
    def get_sources_prim(self):
739
        """
740
741
        Returns:
742
743
        """
744
        # existing_prim_types = ["C", "D", "I", "L", "N", "O", "S", ]
745
746 1
        if not self.type.group == "Prim":
747 1
            return None
748
749 1
        prim_type = self.type.type[:1]
750
751 1
        if prim_type == "C":
752 1
            return self._get_sources_c_prim()
753
754 1
        return f"{self.name}: {self.origin}{' < ' + self.origin_x if self.origin_x else ''}"
755
756 1
    def _get_sources_c_prim(self) -> Optional[List[BaseWordSource]]:
757
        """
758
759
        Returns:
760
761
        """
762 1
        if self.type.type != "C-Prim":
763 1
            return None
764
765 1
        pattern_source = r"\d+\/\d+\w"
766 1
        sources = str(self.origin).split(" | ")
767 1
        word_sources = []
768
769 1
        for source in sources:
770 1
            compatibility = re.search(pattern_source, source)[0]
771 1
            c_l = compatibility[:-1].split("/")
772 1
            transcription = (re.search(rf"(?!{pattern_source}) .+", source)[0]).strip()
773 1
            word_source = BaseWordSource(**{
774
                "coincidence": int(c_l[0]),
775
                "length": int(c_l[1]),
776
                "language": compatibility[-1:],
777
                "transcription": transcription, })
778 1
            word_sources.append(word_source)
779
780 1
        return word_sources
781
782 1
    def get_sources_cpx(self, as_str: bool = False) -> List[Union[str, BaseWord]]:
783
        """Extract source words from self.origin field accordingly
784
        Args:
785
            as_str (bool): return BaseWord objects if False else as simple str
786
            (Default value = False)
787
        Example:
788
            'foldjacea' > ['forli', 'djano', 'cenja']
789
        Returns:
790
            List of words from which the self.name was created
791
792
        """
793
794
        # these prims have switched djifoas like 'flo' for 'folma'
795 1
        switch_prims = [
796
            'canli', 'farfu', 'folma', 'forli', 'kutla', 'marka',
797
            'mordu', 'sanca', 'sordi', 'suksi', 'surna']
798
799 1
        if not self.type.group == "Cpx":
800 1
            return []
801
802 1
        sources = self._prepare_sources_cpx()
803
804 1
        result = self.words_from_source_cpx(sources)
805
806 1
        if not as_str:
807 1
            return result
808
809 1
        result_as_str = []
810 1
        _ = [result_as_str.append(r) for r in sources if r not in result_as_str]
811 1
        return result_as_str
812
813 1
    @staticmethod
814
    def words_from_source_cpx(sources: List[str]) -> List[BaseWord]:
815
        """
816
817
        Args:
818
            sources:
819
820
        Returns:
821
822
        """
823 1
        exclude_type_ids = [t.id for t in BaseType.by(["LW", "Cpd"]).all()]
824 1
        return BaseWord.query \
825
            .filter(BaseWord.name.in_(sources)) \
826
            .filter(BaseWord.type_id.notin_(exclude_type_ids)).all()
827
828 1
    def _prepare_sources_cpx(self) -> List[str]:
829
        """
830
        # TODO
831
        Returns:
832
833
        """
834 1
        sources = self.origin.replace("(", "").replace(")", "").replace("/", "")
835 1
        sources = sources.split("+")
836 1
        sources = [
837
            s if not s.endswith(("r", "h")) else s[:-1]
838
            for s in sources if s not in ["y", "r", "n"]]
839 1
        return sources
840
841 1
    def get_sources_cpd(self, as_str: bool = False) -> List[Union[str, BaseWord]]:
842
        """Extract source words from self.origin field accordingly
843
844
        Args:
845
          as_str: bool: return BaseWord objects if False else as simple str
846
          (Default value = False)
847
848
        Returns:
849
          List of words from which the self.name was created
850
851
        """
852
853 1
        if not self.type.type == "Cpd":
854 1
            return []
855
856 1
        sources = self._prepare_sources_cpd()
857
858 1
        result = self.words_from_source_cpd(sources)
859
860 1
        if not as_str:
861 1
            return result
862
863 1
        result_as_str = []
864
865 1
        _ = [result_as_str.append(r) for r in sources if r not in result_as_str and r]
866
867 1
        return result_as_str
868
869 1
    def _prepare_sources_cpd(self) -> List[str]:
870
        """
871
872
        Returns:
873
874
        """
875 1
        sources = self.origin.replace("(", "").replace(")", "").replace("/", "").replace("-", "")
876 1
        sources = [s.strip() for s in sources.split("+")]
877 1
        return sources
878
879 1
    @staticmethod
880
    def words_from_source_cpd(sources: List[str]) -> List[BaseWord]:
881
        """
882
883
        Args:
884
            sources:
885
886
        Returns:
887
888
        """
889 1
        type_ids = [t.id for t in BaseType.by(["LW", "Cpd"]).all()]
890 1
        return BaseWord.query.filter(BaseWord.name.in_(sources)) \
891
            .filter(BaseWord.type_id.in_(type_ids)).all()
892
893 1
    @classmethod
894 1
    def by_event(cls, event_id: Union[BaseEvent, int] = None) -> BaseQuery:
895
        """Query filtered by specified Event (latest by default)
896
897
        Args:
898
          event_id: Union[BaseEvent, int]: Event object or Event.id (int) (Default value = None)
899
900
        Returns:
901
          BaseQuery
902
903
        """
904 1
        if not event_id:
905 1
            event_id = BaseEvent.latest().id
906
907 1
        event_id = BaseEvent.id if isinstance(event_id, BaseEvent) else int(event_id)
908
909 1
        return cls.query.filter(cls.event_start_id <= event_id) \
910
            .filter(or_(cls.event_end_id > event_id, cls.event_end_id.is_(None))) \
911
            .order_by(cls.name)
912
913 1
    @classmethod
914 1
    def by_name(cls, name: str, case_sensitive: bool = False) -> BaseQuery:
915
        """Word.Query filtered by specified name
916
917
        Args:
918
          name: str:
919
          case_sensitive: bool:  (Default value = False)
920
921
        Returns:
922
          BaseQuery
923
924
        """
925 1
        if case_sensitive:
926 1
            return cls.query.filter(cls.name == name)
927 1
        return cls.query.filter(cls.name.in_([name, name.lower(), name.upper()]))
928
929 1
    @classmethod
930 1
    def by_key(
931
            cls, key: Union[BaseKey, str],
932
            language: str = None,
933
            case_sensitive: bool = False) -> BaseQuery:
934
        """Word.Query filtered by specified key
935
936
        Args:
937
          key: Union[BaseKey, str]:
938
          language: str: Language of key (Default value = None)
939
          case_sensitive: bool:  (Default value = False)
940
941
        Returns:
942
          BaseQuery
943
944
        """
945
946 1
        key = BaseKey.word if isinstance(key, BaseKey) else str(key)
947 1
        request = cls.query.join(BaseDefinition, t_connect_keys, BaseKey)
948
949 1
        if case_sensitive:
950 1
            request = request.filter(BaseKey.word == key)
951
        else:
952 1
            request = request.filter(BaseKey.word.in_([key, key.lower(), key.upper()]))
953
954 1
        if language:
955 1
            request = request.filter(BaseKey.language == language)
956 1
        return request
957
958
959 1
class BaseWordSource(InitBase):
960
    """Word Source from BaseWord.origin for Prims"""
961 1
    __tablename__ = t_name_word_sources
962
963 1
    LANGUAGES = {
964
        "E": "English",
965
        "C": "Chinese",
966
        "H": "Hindi",
967
        "R": "Russian",
968
        "S": "Spanish",
969
        "F": "French",
970
        "J": "Japanese",
971
        "G": "German", }
972
973 1
    coincidence: int = None
974 1
    length: int = None
975 1
    language: str = None
976 1
    transcription: str = None
977
978 1
    @property
979
    def as_string(self) -> str:
980
        """
981
        Format WordSource as string, for example, '3/5R mesto'
982
        Returns:
983
            str
984
        """
985 1
        return f"{self.coincidence}/{self.length}{self.language} {self.transcription}"
986
987
988 1
class BaseWordSpell(InitBase):
989
    """BaseWordSpell model"""
990
    __tablename__ = t_name_word_spells
991