Completed
Push — master ( f300a0...c42fdb )
by Oliver
02:57
created

person._Academic_title_default.degree_title()   B

Complexity

Conditions 6

Size

Total Lines 15
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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