Test Failed
Push — master ( 112d0a...f337c2 )
by Oliver
02:15
created

person.MoP.__post_init__()   A

Complexity

Conditions 2

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 2
nop 1
1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3
"""
4
A set of dataclasses concerning roles of persons and their particulars.
5
"""
6
import datetime
7
import os
8
import sys
9
from dataclasses import dataclass, field
10
from typing import List, Optional, Set, Tuple
11
12
from gender_guesser import detector as sex  # type: ignore
13
14
PACKAGE_PARENT = ".."
15
SCRIPT_DIR = os.path.dirname(
16
    os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__)))
17
)  # isort:skip # noqa # pylint: disable=wrong-import-position
18
sys.path.append(
19
    os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT))
20
)  # isort: skip # noqa # pylint: disable=wrong-import-position
21
22
from src.resources.constants import GERMAN_PARTIES  # type: ignore  # noqa
23
from src.resources.constants import PEER_PREPOSITIONS  # type: ignore # noqa
24
from src.resources.constants import PEERTITLES  # type: ignore # noqa
25
from src.resources.helpers import (  # type: ignore # noqa; type: ignore # noqa; type: ignore  # noqa; type: ignore # noqa
26
    AttrDisplay,
27
    NotInRange,
28
    Party,
29
    TooManyFirstNames,
30
)
31
32
33
@dataclass
34
class _Name_default:
35
    middle_name_1: Optional[str] = field(default=None)
36
    middle_name_2: Optional[str] = field(default=None)
37
    maiden_name: Optional[str] = field(default=None)
38
    divorcée: Optional[str] = field(default=None)
39
40
41
@dataclass
42
class _Name_base:
43
    first_name: str
44
    last_name: str
45
46
47
@dataclass
48
class Name(_Name_default, _Name_base, AttrDisplay):
49
50
    """
51
    The most basic part to describe a person.
52
    To add more middle names, dataclass _Name_default has to be given further
53
    middle_name attributes. Since this project currently focusses on German
54
    politicians, the limit of three given names is preserved.
55
    """
56
57
    def __post_init__(self):
58
        """
59
        In case a Name instance is initialized with all first names in one
60
        string, __post_init__ will take care of this and assign each first
61
        name its attribute. Also it will raise TooManyFirstNames if more than
62
        three first names are given.
63
        """
64
        first_names = self.first_name.split(" ")
65
        self.first_name = first_names[0]
66
        if len(first_names) == 2:
67
            self.middle_name_1 = first_names[1]
68
        elif len(first_names) == 3:
69
            self.middle_name_1 = first_names[1]
70
            self.middle_name_2 = first_names[-1]
71
        elif len(first_names) > 3:
72
            print(first_names)
73
            raise TooManyFirstNames("There are more than three first names!")
74
75
76
@dataclass
77
class _Peertitle_default:
78
    peer_title: Optional[str] = field(default=None)
79
    peer_preposition: Optional[str] = field(default=None)
80
81
    def nobility_title(self) -> None:
82
        if self.peer_title is not None:
83
            title = self.peer_title
84
            self.peer_title, self.peer_preposition = self.title_fix(title)
85
86
    def title_fix(self, title) -> Tuple[str, str]:
87
        titles = title.split(" ")
88
        title_tmp = ""
89
        preposition_tmp = ""
90
        for prep in titles:
91
            if prep.lower() in PEER_PREPOSITIONS:
92
                preposition_tmp = preposition_tmp + prep.lower() + " "
93
            elif prep in PEERTITLES:
94
                title_tmp = title_tmp + prep + " "
95
        peer_preposition = preposition_tmp.strip()
96
        peer_title = title_tmp.strip()
97
98
        return peer_title, peer_preposition
99
100
101
@dataclass
102
class Noble(_Peertitle_default, Name, AttrDisplay):
103
    def __post_init__(self):
104
        """Initialize names and titles."""
105
        Name.__post_init__(self)
106
        self.nobility_title()
107
108
109
@dataclass
110
class _Academic_title_default:
111
    academic_title: Optional[str] = field(default=None)
112
113
    def degree_title(self) -> None:
114
        if self.academic_title is not None:
115
            title = self.academic_title
116
            self.academic_title = self.title_repair(title)
117
118
    def title_repair(self, title) -> str:
119
        if ".D" in title:
120
            title = ". ".join(c for c in title.split("."))
121
        if ".A" in title:
122
            title = ". ".join(c for c in title.split("."))
123
        if title.endswith("Dr"):
124
            title = title[:-2] + "Dr."
125
        while "  " in title:
126
            title = title.replace("  ", " ")
127
        title = title.strip()
128
129
        return title
130
131
132
@dataclass
133
class Academic(_Academic_title_default, Name, AttrDisplay):
134
    def __post_init__(self):
135
        Name.__post_init__(self)
136
        self.degree_title()
137
138
139
@dataclass
140
class _Person_default:
141
    gender: str = field(default="unknown")
142
    born: str = field(default="unknown")
143
    date_of_birth: str = field(default="unknown")
144
    age: str = field(default="unknown")
145
    deceased: str = field(default="unknown")
146
    profession: str = field(default="unknown")
147
148
149
@dataclass
150
class Person(
151
    _Peertitle_default,
152
    _Academic_title_default,
153
    _Person_default,
154
    Name,
155
    AttrDisplay,  # noqa
156
):
157
    def __post_init__(self):
158
        Name.__post_init__(self)
159
        Academic.__post_init__(self)
160
        self.get_sex()
161
        self.get_year_of_birth()
162
        self.get_age()
163
164
    def get_sex(self) -> None:
165
        if "-" in self.first_name:
166
            first_name = self.first_name.split("-")[0]
167
        else:
168
            first_name = self.first_name
169
        d = sex.Detector()
170
        gender = d.get_gender(f"{first_name}")
171
        if "female" in gender:
172
            self.gender = "female"
173
        elif "male" in gender:
174
            self.gender = "male"
175
176
    def get_year_of_birth(self) -> None:
177
        if self.date_of_birth != "unknown":
178
            self.born = self.date_of_birth.split(".")[-1]
179
180
    def get_age(self) -> None:
181
        if self.born != "unknown":
182
            born = str(self.born)
183
            if len(born) > 4:
184
                self.deceased = born.strip()[5:]
185
                self.born = born[:4]
186
            else:
187
                today = datetime.date.today()
188
                self.age = str(int(today.year) - int(born.strip()))
189
190
191
@dataclass
192
class _Politician_default:
193
    electoral_ward: str = field(default="ew")
194
    ward_no: Optional[int] = field(default=None)
195
    voter_count: Optional[int] = field(default=None)
196
    minister: Optional[str] = field(default=None)
197
    offices: List[str] = field(default_factory=lambda: [])
198
    parties: List[str] = field(default_factory=lambda: [])
199
200
    def renamed_wards(self):
201
        wards = {
202
            "Kreis Aachen I": "Aachen III",
203
            "Hochsauerlandkreis II – Soest III": "Hochsauerlandkreis II",
204
            "Kreis Aachen II": "Aachen IV"
205
            if self.last_name in ["Wirtz", "Weidenhaupt"]
206
            else "Kreis Aachen I",
207
        }
208
        if self.electoral_ward in wards.keys():
209
            self.electoral_ward = wards[self.electoral_ward]
210
211
    def scrape_wiki_for_ward(self) -> None:
212
        import requests
213
        from bs4 import BeautifulSoup  # type: ignore
214
215
        URL_base = "https://de.wikipedia.org/wiki/Landtagswahlkreis_{}"
216
        URL = URL_base.format(self.electoral_ward)
217
        req = requests.get(URL)
218
        bsObj = BeautifulSoup(req.text, "lxml")
219
        table = bsObj.find(class_="infobox float-right toptextcells")
220
        self.scrape_wiki_table_for_ward(table)
221
222
    def scrape_wiki_table_for_ward(self, table) -> None:
223
        for td in table.find_all("td"):
224
            if "Wahlkreisnummer" in td.text:
225
                ward_no = td.find_next().text.strip()
226
                ward_no = ward_no.split(" ")[0]
227
                self.ward_no = int(ward_no)
228
            elif "Wahlberechtigte" in td.text:
229
                voter_count = td.find_next().text.strip()
230
                voter_count = self.fix_voter_count(voter_count)
231
                self.voter_count = int(voter_count)
232
233
    def fix_voter_count(self, voter_count):
234
        if voter_count[-1] == "]":
235
            voter_count = voter_count[:-3]
236
        if " " in voter_count:
237
            voter_count = "".join(voter_count.split(" "))
238
        else:
239
            voter_count = "".join(voter_count.split("."))
240
        return voter_count
241
242
243
@dataclass
244
class Politician(
245
    _Peertitle_default,
246
    _Academic_title_default,
247
    _Person_default,
248
    _Politician_default,
249
    _Name_default,
250
    Party,
251
    _Name_base,
252
    AttrDisplay,
253
):
254
    def __post_init__(self):
255
        Name.__post_init__(self)
256
        Academic.__post_init__(self)
257
        Noble.__post_init__(self)
258
        Party.__post_init__(self)
259
        Person.get_sex(self)
260
        Person.get_age(self)
261
        self.change_ward()
262
        if self.party_name in GERMAN_PARTIES:
263
            self.parties.append(
264
                Party(self.party_name, self.party_entry, self.party_exit)
265
            )
266
        if self.minister and self.minister not in self.offices:
267
            self.offices.append(self.minister)
268
269
    def add_Party(
270
        self, party_name, party_entry="unknown", party_exit="unknown"
271
    ):  # noqa
272
        if party_name in GERMAN_PARTIES:
273
            if self.party_is_in_parties(party_name, party_entry, party_exit):
274
                pass
275
            else:
276
                self.parties.append(Party(party_name, party_entry, party_exit))
277
                self.party_name = party_name
278
                self.party_entry = party_entry
279
                self.party_exit = party_exit
280
281
    def align_party_entries(
282
        self, party, party_name, party_entry, party_exit
283
    ) -> Party:  # noqa
284
        if party_entry != "unknown" and party.party_entry == "unknown":
285
            party.party_entry = party_entry
286
        if party_exit != "unknown" and party.party_exit == "unknown":
287
            party.party_exit = party_exit
288
        return party
289
290
    def party_is_in_parties(self, party_name, party_entry, party_exit):
291
        parties_tmp = self.parties[:]
292
        for party in parties_tmp:
293
            if party_name == party.party_name:
294
                party_updated = self.align_party_entries(
295
                    party, party_name, party_entry, party_exit
296
                )
297
                self.parties.remove(party)
298
                self.parties.append(party_updated)
299
                self.party_entry = party_updated.party_entry
300
                self.party_exit = party_updated.party_exit
301
                return True
302
        return False
303
304
    def change_ward(self, ward=None):
305
        if ward:
306
            self.electoral_ward = ward
307
        if self.electoral_ward not in ["ew", "Landesliste"]:
308
            self.renamed_wards()
309
            self.scrape_wiki_for_ward()
310
        else:
311
            self.electoral_ward = "ew"
312
313
314
@dataclass
315
class _MoP_default:
316
    parl_pres: bool = field(default=False)
317
    parl_vicePres: bool = field(default=False)
318
    parliament_entry: str = field(default="unknown")  # date string: "11.3.2015"  # noqa
319
    parliament_exit: str = field(default="unknown")  # dto.
320
    speeches: List[str] = field(
321
        default_factory=lambda: []
322
    )  # identifiers for speeches  # noqa
323
    reactions: List[str] = field(
324
        default_factory=lambda: []
325
    )  # identifiers for reactions
326
    membership: Set[str] = field(
327
        default_factory=lambda: set()
328
    )  # years like ["2010", "2011", ...]
329
330
331
@dataclass
332
class _MoP_base:
333
    legislature: int
334
    state: str  # this would be "NRW", "BY", ...
335
336
337
@dataclass
338
class MoP(_MoP_default, Politician, _MoP_base, AttrDisplay):
339
    def __post_init__(self):
340
        if int(self.legislature) not in range(14, 18):
341
            raise NotInRange("Number for legislature not in range")
342
        else:
343
            self.membership.add(self.legislature)
344
        Politician.__post_init__(self)
345
346
347
if __name__ == "__main__":
348
349
    name = Name("Hans Hermann", "Werner")
350
    print(name)
351
352
    noble = Noble("Dagmara", "Bodelschwingh", peer_title="Gräfin von")
353
    print(noble)
354
355
    academic = Academic("Horst Heiner", "Wiekeiner", academic_title="Dr.")  # noqa
356
    print(academic)
357
358
    person_1 = Person("Sven", "Rübennase", academic_title="MBA", born="1990")  # noqa
359
    print(person_1)
360
361
    politician = Politician(
362
        "Bärbel",
363
        "Gutherz",
364
        "SPD",
365
        academic_title="Dr.",
366
        born="1980",
367
        electoral_ward="Köln I",
368
    )
369
    print(politician)
370
371
    mop = MoP(
372
        14,
373
        "NRW",
374
        "Tom",
375
        "Schwadronius",
376
        "SPD",
377
        party_entry="1990",  # type: ignore
378
        peer_title="Junker von",
379
        born="1950",
380
    )
381
    print(mop)
382
383
    mop.add_Party("Grüne", party_entry="30.11.1999")
384
    mop.change_ward("Düsseldorf II")
385
    print(mop)
386