Completed
Push — master ( bad14e...d8600a )
by Oliver
01:51
created

person._Academic_title_default.degree_title()   A

Complexity

Conditions 2

Size

Total Lines 4
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 2
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
            title = self.academic_title
111
            self.academic_title = self.title_repair(title)
112
113
    def title_repair(self, title) -> str:
114
        if ".D" in title:
115
            title = ". ".join(c for c in title.split("."))
116
        if ".A" in title:
117
            title = ". ".join(c for c in title.split("."))
118
        if title.endswith("Dr"):
119
            title = title[:-2] + "Dr."
120
        while "  " in title:
121
            title = title.replace("  ", " ")
122
        title = title.strip()
123
124
        return title
125
126
127
@dataclass
128
class Academic(_Academic_title_default, Name, AttrDisplay):
129
    def __post_init__(self):
130
        Name.__post_init__(self)
131
        self.degree_title()
132
133
134
@dataclass
135
class _Person_default:
136
    gender: str = field(default="unknown")
137
    born: str = field(default="unknown")
138
    date_of_birth: str = field(default="unknown")
139
    age: str = field(default="unknown")
140
    deceased: str = field(default="unknown")
141
    profession: str = field(default="unknown")
142
143
144
@dataclass
145
class Person(
146
    _Peertitle_default,
147
    _Academic_title_default,
148
    _Person_default,
149
    Name,
150
    AttrDisplay,  # noqa
151
):
152
    def __post_init__(self):
153
        Name.__post_init__(self)
154
        Academic.__post_init__(self)
155
        self.get_sex()
156
        self.get_year_of_birth()
157
        self.get_age()
158
159
    def get_sex(self) -> None:
160
        if "-" in self.first_name:
161
            first_name = self.first_name.split("-")[0]
162
        else:
163
            first_name = self.first_name
164
        d = sex.Detector()
165
        gender = d.get_gender(f"{first_name}")
166
        if "female" in gender:
167
            self.gender = "female"
168
        elif "male" in gender:
169
            self.gender = "male"
170
171
    def get_year_of_birth(self) -> None:
172
        if self.date_of_birth != "unknown":
173
            self.born = self.date_of_birth.split(".")[-1]
174
175
    def get_age(self) -> None:
176
        if self.born != "unknown":
177
            born = str(self.born)
178
            if len(born) > 4:
179
                self.deceased = born.strip()[5:]
180
                self.born = born[:4]
181
            else:
182
                today = datetime.date.today()
183
                self.age = str(int(today.year) - int(born.strip()))
184
185
186
@dataclass
187
class _Politician_default:
188
    electoral_ward: str = field(default="ew")
189
    ward_no: Optional[int] = field(default=None)
190
    voter_count: Optional[int] = field(default=None)
191
    minister: Optional[str] = field(default=None)
192
    offices: List[str] = field(default_factory=lambda: [])
193
    parties: List[str] = field(default_factory=lambda: [])
194
195
    def renamed_wards(self):
196
        wards = {
197
            "Kreis Aachen I": "Aachen III",
198
            "Hochsauerlandkreis II – Soest III": "Hochsauerlandkreis II",
199
            "Kreis Aachen II": "Aachen IV"
200
            if self.last_name in ["Wirtz", "Weidenhaupt"]
201
            else "Kreis Aachen I",
202
        }
203
        if self.electoral_ward in wards.keys():
204
            self.electoral_ward = wards[self.electoral_ward]
205
206
    def scrape_wiki_for_ward(self):
207
        import requests
208
        from bs4 import BeautifulSoup  # type: ignore
209
210
        URL_base = "https://de.wikipedia.org/wiki/Landtagswahlkreis_{}"
211
        URL = URL_base.format(self.electoral_ward)
212
        req = requests.get(URL)
213
        bsObj = BeautifulSoup(req.text, "lxml")
214
        table = bsObj.find(class_="infobox float-right toptextcells")
215
        for td in table.find_all("td"):
216
            if "Wahlkreisnummer" in td.text:
217
                ward_no = td.find_next().text.strip()
218
                ward_no = ward_no.split(" ")[0]
219
                self.ward_no = int(ward_no)
220
            elif "Wahlberechtigte" in td.text:
221
                voter_count = td.find_next().text.strip()
222
                if voter_count[-1] == "]":
223
                    voter_count = voter_count[:-3]
224
                if " " in voter_count:
225
                    voter_count = "".join(voter_count.split(" "))
226
                else:
227
                    voter_count = "".join(voter_count.split("."))
228
                self.voter_count = int(voter_count)
229
230
231
@dataclass
232
class Politician(
233
    _Peertitle_default,
234
    _Academic_title_default,
235
    _Person_default,
236
    _Politician_default,
237
    _Name_default,
238
    Party,
239
    _Name_base,
240
    AttrDisplay,
241
):
242
    def __post_init__(self):
243
        Name.__post_init__(self)
244
        Academic.__post_init__(self)
245
        Noble.__post_init__(self)
246
        Party.__post_init__(self)
247
        Person.get_sex(self)
248
        Person.get_age(self)
249
        self.change_ward()
250
        if self.party_name in GERMAN_PARTIES:
251
            self.parties.append(
252
                Party(self.party_name, self.party_entry, self.party_exit)
253
            )
254
        if self.minister and self.minister not in self.offices:
255
            self.offices.append(self.minister)
256
257
    def add_Party(
258
        self, party_name, party_entry="unknown", party_exit="unknown"
259
    ):  # noqa
260
        if party_name in GERMAN_PARTIES:
261
            if self.party_is_in_parties(party_name, party_entry, party_exit):
262
                pass
263
            else:
264
                self.parties.append(Party(party_name, party_entry, party_exit))
265
                self.party_name = party_name
266
                self.party_entry = party_entry
267
                self.party_exit = party_exit
268
269
    def align_party_entries(
270
        self, party, party_name, party_entry, party_exit
271
    ) -> Party:  # noqa
272
        if party_entry != "unknown" and party.party_entry == "unknown":
273
            party.party_entry = party_entry
274
        if party_exit != "unknown" and party.party_exit == "unknown":
275
            party.party_exit = party_exit
276
        return party
277
278
    def party_is_in_parties(self, party_name, party_entry, party_exit):
279
        parties_tmp = self.parties[:]
280
        for party in parties_tmp:
281
            if party_name == party.party_name:
282
                party_updated = self.align_party_entries(
283
                    party, party_name, party_entry, party_exit
284
                )
285
                self.parties.remove(party)
286
                self.parties.append(party_updated)
287
                self.party_entry = party_updated.party_entry
288
                self.party_exit = party_updated.party_exit
289
                return True
290
        return False
291
292
    def change_ward(self, ward=None):
293
        if ward:
294
            self.electoral_ward = ward
295
        if self.electoral_ward not in ["ew", "Landesliste"]:
296
            self.renamed_wards()
297
            self.scrape_wiki_for_ward()
298
        else:
299
            self.electoral_ward = "ew"
300
301
302
@dataclass
303
class _MdL_default:
304
    parl_pres: bool = field(default=False)
305
    parl_vicePres: bool = field(default=False)
306
    parliament_entry: str = field(default="unknown")  # date string: "11.3.2015"  # noqa
307
    parliament_exit: str = field(default="unknown")  # dto.
308
    speeches: List[str] = field(
309
        default_factory=lambda: []
310
    )  # identifiers for speeches  # noqa
311
    reactions: List[str] = field(
312
        default_factory=lambda: []
313
    )  # identifiers for reactions
314
    membership: Set[str] = field(
315
        default_factory=lambda: set()
316
    )  # years like ["2010", "2011", ...]
317
318
319
@dataclass
320
class _MdL_base:
321
    legislature: int
322
    state: str  # this would be "NRW", "BY", ...
323
324
325
@dataclass
326
class MdL(_MdL_default, Politician, _MdL_base, AttrDisplay):
327
    def __post_init__(self):
328
        if int(self.legislature) not in range(14, 18):
329
            raise NotInRange("Number for legislature not in range")
330
        else:
331
            self.membership.add(self.legislature)
332
        Politician.__post_init__(self)
333
334
335
if __name__ == "__main__":
336
337
    name = Name("Hans Hermann", "Werner")
338
    print(name)
339
340
    noble = Noble("Dagmara", "Bodelschwingh", peer_title="Gräfin von")
341
    print(noble)
342
343
    academic = Academic("Horst Heiner", "Wiekeiner", academic_title="Dr.")  # noqa
344
    print(academic)
345
346
    person_1 = Person("Sven", "Rübennase", academic_title="MBA", born="1990")  # noqa
347
    print(person_1)
348
349
    politician = Politician(
350
        "Bärbel",
351
        "Gutherz",
352
        "SPD",
353
        academic_title="Dr.",
354
        born="1980",
355
        electoral_ward="Köln I",
356
    )
357
    print(politician)
358
359
    mdl = MdL(
360
        14,
361
        "NRW",
362
        "Tom",
363
        "Schwadronius",
364
        "SPD",
365
        party_entry="1990",  # type: ignore
366
        peer_title="Junker von",
367
        born="1950",
368
    )
369
    print(mdl)
370
371
    mdl.add_Party("Grüne", party_entry="30.11.1999")
372
    mdl.change_ward("Düsseldorf II")
373
    print(mdl)
374