diff --git a/Jupyter/API-tests/News/notebook.ipynb b/Jupyter/API-tests/News/notebook.ipynb index 981ba18..f61b9c6 100644 --- a/Jupyter/API-tests/News/notebook.ipynb +++ b/Jupyter/API-tests/News/notebook.ipynb @@ -607,7 +607,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -638,24 +638,32 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 2, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[{'title': 'BMP Greengas: Gläubiger fordern knapp eine Dreiviertelmilliarde Euro von insolventer EnBW-Tochter', 'link': 'https://www.handelsblatt.com/unternehmen/energie/bmp-greengas-glaeubiger-fordern-mehr-als-700-millionen-euro-von-insolventer-enbw-tochter/29394600.html', 'description': 'BMP Greengas verkaufte Biomethan an Stadtwerke und Energieversorger in ganz Deutschland. Die Insolvenz des Gashändlers könnte die öffentliche Hand Hunderte Millionen Euro kosten.', 'category': 'Energie', 'pubDate': 'Fri, 15 Sep 2023 20:26:51 +0200', 'guid': 'https://www.handelsblatt.com/29394600.html', 'content:encoded': '\"\"BMP Greengas verkaufte Biomethan an Stadtwerke und Energieversorger in ganz Deutschland. Die Insolvenz des Gashändlers könnte die öffentliche Hand Hunderte Millionen Euro kosten.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/biogasanlage/29394638/3-format2020.jpg', '@type': 'image/jpeg', '@length': '852752'}}, {'title': 'Pharma- und Agrarchemiekonzern: Weniger Hierarchien, weniger Manager: Bayer-Chef startet erste Phase des Umbaus', 'link': 'https://www.handelsblatt.com/unternehmen/industrie/pharma-und-agrarchemiekonzern-bayer-chef-bill-anderson-startet-erste-phase-des-umbaus/29393764.html', 'description': 'Bill Anderson hat der Bürokratie im eigenen Konzern den Kampf angesagt. Der neue CEO plant die Straffung der Bayer-Organisation – inklusive Stellenabbau. Führungskräfte sind verunsichert.', 'category': 'Industrie', 'pubDate': 'Fri, 15 Sep 2023 15:24:06 +0200', 'guid': 'https://www.handelsblatt.com/29393764.html', 'content:encoded': '\"\"Bill Anderson hat der Bürokratie im eigenen Konzern den Kampf angesagt. Der neue CEO plant die Straffung der Bayer-Organisation – inklusive Stellenabbau. Führungskräfte sind verunsichert.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/chempark-leverkusen/29394336/2-format2020.jpg', '@type': 'image/jpeg', '@length': '792899'}}, {'title': 'Offshore: Vattenfall will trotz Krise einen Mega-Windpark in der Nordsee bauen', 'link': 'https://www.handelsblatt.com/unternehmen/energie/offshore-vattenfall-will-trotz-krise-einen-mega-windpark-in-der-nordsee-bauen/29393316.html', 'description': 'Kurz nachdem der Konzern ein Windprojekt in Großbritannien gestoppt hat, kündigt er einen neuen Offshore-Park\\xa0in der Nordsee an. Der Unterschied liegt im System.', 'category': 'Energie', 'pubDate': 'Fri, 15 Sep 2023 20:01:15 +0200', 'guid': 'https://www.handelsblatt.com/29393316.html', 'content:encoded': '\"\"Kurz nachdem der Konzern ein Windprojekt in Großbritannien gestoppt hat, kündigt er einen neuen Offshore-Park\\xa0in der Nordsee an. Der Unterschied liegt im System.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/offshore/29394452/2-format2020.jpg', '@type': 'image/jpeg', '@length': '791043'}}, {'title': 'Anwälte: Probleme beseitigen, bevor sie entstehen: Das sind die Top-Dealmaker in Deutschlands Wirtschaftskanzleien', 'link': 'https://www.handelsblatt.com/unternehmen/dienstleister/anwaelte-das-sind-die-top-dealmaker-in-deutschlands-wirtschaftskanzleien/29391254.html', 'description': 'Gute Wirtschaftsanwälte sind heute weit mehr als bloße Rechtsberater. Das Handelsblatt portraitiert die neue Elite unter den Topjuristen.', 'category': 'Dienstleister', 'pubDate': 'Sat, 16 Sep 2023 14:38:56 +0200', 'guid': 'https://www.handelsblatt.com/29391254.html', 'content:encoded': '\"\"Gute Wirtschaftsanwälte sind heute weit mehr als bloße Rechtsberater. Das Handelsblatt portraitiert die neue Elite unter den Topjuristen.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/rechtsanwaelte/29392486/3-format2020.jpg', '@type': 'image/jpeg', '@length': '14415400'}}, {'title': 'Start-up-Check: Buntes Craft Beer aus Berlin: Vorreiter Fuerst Wiacek will auf dem deutschen Markt expandieren', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/start-up-check-craft-beer-vorreiter-fuerst-wiacek-will-auf-dem-deutschen-markt-expandieren/29390886.html', 'description': 'Das Start-up zählt zu den bekanntesten deutschen Namen in der Brauerszene. Mit Geld aus einer Crowdinvesting-Kampagne wollen die Gründer nun das Geschäft auf dem Heimatmarkt vorantreiben.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Sat, 16 Sep 2023 13:53:10 +0200', 'guid': 'https://www.handelsblatt.com/29390886.html', 'content:encoded': '\"\"Das Start-up zählt zu den bekanntesten deutschen Namen in der Brauerszene. Mit Geld aus einer Crowdinvesting-Kampagne wollen die Gründer nun das Geschäft auf dem Heimatmarkt vorantreiben.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/bierdosen-von-fuerst-wiacek/29391684/2-format2020.jpg', '@type': 'image/jpeg', '@length': '428952'}}, {'title': 'Interview: Berliner Hostel-Chef zur Adlon-Chefin: „Wir müssen gucken, ob unsere Gäste bald noch Geld haben, um zu verreisen“', 'link': 'https://www.handelsblatt.com/unternehmen/dienstleister/interview-berliner-hostel-chef-zur-adlon-chefin-wir-muessen-gucken-ob-unsere-gaeste-bald-noch-geld-haben-um-zu-verreisen/29390926.html', 'description': 'Karina Ansos vom Berliner Hotel Adlon und Oliver Winter von den a&o-Hostels sprechen über den zurückliegenden Sommer, leere Betten und die neuen Ansprüche ihrer Gäste.', 'category': 'Dienstleister', 'pubDate': 'Sat, 16 Sep 2023 12:00:00 +0200', 'guid': 'https://www.handelsblatt.com/29390926.html', 'content:encoded': '\"\"Karina Ansos vom Berliner Hotel Adlon und Oliver Winter von den a&o-Hostels sprechen über den zurückliegenden Sommer, leere Betten und die neuen Ansprüche ihrer Gäste.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/das-hotel-adlon-in-berlin/29391130/2-format2020.jpg', '@type': 'image/jpeg', '@length': '505831'}}, {'title': 'Metall- und Elektroindustrie: IG Metall sieht Vier-Tage-Woche nicht als Priorität', 'link': 'https://www.handelsblatt.com/unternehmen/industrie/metall-und-elektroindustrie-ig-metall-sieht-vier-tage-woche-nicht-als-prioritaet/29395094.html', 'description': 'Der derzeitige\\xa0Tarifvertrag\\xa0läuft noch bis Herbst 2024. Die IG Metall wird bei den\\xa0Verhandlungen im November\\xa0den Schwerpunkt\\xa0auf höhere Löhne und Gehälter\\xa0legen.', 'category': 'Industrie', 'pubDate': 'Sat, 16 Sep 2023 11:56:46 +0200', 'guid': 'https://www.handelsblatt.com/29395094.html', 'content:encoded': '\"\"Der derzeitige\\xa0Tarifvertrag\\xa0läuft noch bis Herbst 2024. Die IG Metall wird bei den\\xa0Verhandlungen im November\\xa0den Schwerpunkt\\xa0auf höhere Löhne und Gehälter\\xa0legen.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/ig-metall/29395102/2-format2020.jpg', '@type': 'image/jpeg', '@length': '253352'}}, {'title': 'Autoindustrie: Stellantis bietet in US-Autostreik 19,5 Prozent mehr Lohn', 'link': 'https://www.handelsblatt.com/unternehmen/industrie/autoindustrie-stellantis-bietet-in-us-autostreik-19-5-prozent-mehr-lohn/29395018.html', 'description': 'Zuvor hatte der Autobauer seinen Angestellten einen 17,5 Prozent-Zuschlag zugesichert. Die Gewerkschaft UAW bestreikte daraufhin erstmals gleichzeitig GM, Ford und Chrysler.', 'category': 'Industrie', 'pubDate': 'Sat, 16 Sep 2023 09:20:19 +0200', 'guid': 'https://www.handelsblatt.com/29395018.html', 'content:encoded': '\"\"Zuvor hatte der Autobauer seinen Angestellten einen 17,5 Prozent-Zuschlag zugesichert. Die Gewerkschaft UAW bestreikte daraufhin erstmals gleichzeitig GM, Ford und Chrysler.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/uaw-gewerkschaftsstreik/29395020/2-format2020.jpg', '@type': 'image/jpeg', '@length': '1225813'}}, {'title': 'Private-Equity-Manager: Heuschrecken oder Mehrwert-Schaffer? Das sind die wichtigsten Köpfe der Private-Equity-Branche in Deutschland', 'link': 'https://www.handelsblatt.com/unternehmen/dienstleister/private-equity-manager-das-sind-die-wichtigsten-koepfe-der-private-equity-branche-in-deutschland/29391250.html', 'description': 'Ihr Image ist ausbaufähig, ihr Gewerbe verschwiegen. Erfolgreiche Private-Equity-Manager erwirtschaften Millionen. Wir zeigen, wer das Geschäft vorantreibt.', 'category': 'Dienstleister', 'pubDate': 'Sat, 16 Sep 2023 08:14:23 +0200', 'guid': 'https://www.handelsblatt.com/29391250.html', 'content:encoded': '\"\"Ihr Image ist ausbaufähig, ihr Gewerbe verschwiegen. Erfolgreiche Private-Equity-Manager erwirtschaften Millionen. Wir zeigen, wer das Geschäft vorantreibt.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/private-equity-manager/29392528/2-format2020.jpg', '@type': 'image/jpeg', '@length': '16140205'}}, {'title': 'Vor Börsengang: Sandalen-Hersteller Birkenstock steigert Umsatz', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/vor-boersengang-sandalen-hersteller-birkenstock-steigert-umsatz/29394738.html', 'description': 'Vor allem die gestiegenen Preise haben den Umsatz des Unternehmens deutlich angekurbelt. Am Dienstag hatte Birkenstock seinen Börsengang in den USA angekündigt.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Fri, 15 Sep 2023 19:18:00 +0200', 'guid': 'https://www.handelsblatt.com/29394738.html', 'content:encoded': '\"\"Vor allem die gestiegenen Preise haben den Umsatz des Unternehmens deutlich angekurbelt. Am Dienstag hatte Birkenstock seinen Börsengang in den USA angekündigt.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/birkenstock-/29394746/2-format2020.jpg', '@type': 'image/jpeg', '@length': '567008'}}, {'title': 'Getränkehersteller: Beschäftigte bei Coca-Cola Deutschland erhalten mehr Geld', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/getraenkehersteller-beschaeftigte-bei-coca-cola-deutschland-erhalten-mehr-geld/29394378.html', 'description': 'Beide Seiten profitieren vom Ergebnis der Tarifverhandlungen des Getränkeherstellers. Während das Gehalt der Beschäftigten steigt, weitet der Konzern seine Produktion aus.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Fri, 15 Sep 2023 16:29:01 +0200', 'guid': 'https://www.handelsblatt.com/29394378.html', 'content:encoded': '\"\"Beide Seiten profitieren vom Ergebnis der Tarifverhandlungen des Getränkeherstellers. Während das Gehalt der Beschäftigten steigt, weitet der Konzern seine Produktion aus.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/coca-cola/29394398/4-format2020.jpg', '@type': 'image/jpeg', '@length': '423214'}}, {'title': 'Medien: Disney erhält Zehn-Milliarden-Angebot für TV-Geschäft – Streaming soll teurer werden', 'link': 'https://www.handelsblatt.com/unternehmen/it-medien/disney-disneys-streaming-angebot-soll-deutlich-teurer-werden/29393708.html', 'description': 'Im Juli hat Konzernchef Iger angedeutet, das klassische Fernsehgeschäft veräußern zu wollen. Nun gibt es erste Interessenten. Beim Streaming will der Konzern sparen.', 'category': 'Medien', 'pubDate': 'Fri, 15 Sep 2023 14:16:05 +0200', 'guid': 'https://www.handelsblatt.com/29393708.html', 'content:encoded': '\"\"Im Juli hat Konzernchef Iger angedeutet, das klassische Fernsehgeschäft veräußern zu wollen. Nun gibt es erste Interessenten. Beim Streaming will der Konzern sparen.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/szene-aus-der-star-wars-serie-ahsoka/29394038/2-format2020.jpg', '@type': 'image/jpeg', '@length': '381338'}}, {'title': 'Familienunternehmen: „Unterschiedliche Auffassungen“ über Strategie: Haniel-Chef Thomas Schmidt geht', 'link': 'https://www.handelsblatt.com/unternehmen/mittelstand/familienunternehmer/familienunternehmen-haniel-chef-thomas-schmidt-geht-nachfolgersuche-laeuft/29392702.html', 'description': 'Der Vertrag von Thomas Schmidt wird nicht verlängert. Der Manager sollte Haniel neu aufstellen, brachte aber keine Stabilität in den Konzern. Der Wechsel kommt zu einem heiklen Zeitpunkt.', 'category': 'Familienunternehmer', 'pubDate': 'Fri, 15 Sep 2023 14:00:00 +0200', 'guid': 'https://www.handelsblatt.com/29392702.html', 'content:encoded': '\"\"Der Vertrag von Thomas Schmidt wird nicht verlängert. Der Manager sollte Haniel neu aufstellen, brachte aber keine Stabilität in den Konzern. Der Wechsel kommt zu einem heiklen Zeitpunkt.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/thomas-schmidt/29393538/3-format2020.jpg', '@type': 'image/jpeg', '@length': '628007'}}, {'title': 'Fußball: Hertha-Investor übernimmt Premier-League-Club Everton', 'link': 'https://www.handelsblatt.com/unternehmen/dienstleister/fussball-hertha-investor-uebernimmt-premier-league-club-everton/29393828.html', 'description': 'Der bisherige Besitzer Farhad Moshiri verkauft 777 Partners seine gesamte Beteiligung. Damit gehört der US-Investmentfirma nun eine ganze Reihe internationaler Klubs.', 'category': 'Dienstleister', 'pubDate': 'Fri, 15 Sep 2023 13:06:26 +0200', 'guid': 'https://www.handelsblatt.com/29393828.html', 'content:encoded': '\"\"Der bisherige Besitzer Farhad Moshiri verkauft 777 Partners seine gesamte Beteiligung. Damit gehört der US-Investmentfirma nun eine ganze Reihe internationaler Klubs.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/spiel-von-everton-gegen-manchester-city/29393854/2-format2020.jpg', '@type': 'image/jpeg', '@length': '419103'}}, {'title': 'Pharmakonzern: Novartis-Aktionäre stimmen für Sandoz-Abspaltung', 'link': 'https://www.handelsblatt.com/unternehmen/industrie/pharmakonzern-novartis-aktionaere-stimmen-fuer-sandoz-abspaltung/29393702.html', 'description': 'Die Generikatochter des Schweizer Pharmakonzerns startet wohl im Index mittelgroßer Firmen. Die Schätzungen zum Börsenwert von Sandoz weichen stark voneinander ab.', 'category': 'Industrie', 'pubDate': 'Fri, 15 Sep 2023 12:49:04 +0200', 'guid': 'https://www.handelsblatt.com/29393702.html', 'content:encoded': '\"\"Die Generikatochter des Schweizer Pharmakonzerns startet wohl im Index mittelgroßer Firmen. Die Schätzungen zum Börsenwert von Sandoz weichen stark voneinander ab.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/novartis/29393800/2-format2020.jpg', '@type': 'image/jpeg', '@length': '284440'}}, {'title': 'Fraport: Betreiber von Frankfurter Flughafen verlängert Vertrag mit Chef', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/fraport-betreiber-von-frankfurter-flughafen-verlaengert-vertrag-mit-chef/29393576.html', 'description': 'Stefan Schulte bleibt Vorstandsvorsitzender von Fraport. Er muss in den kommenden Jahren die hohe Verschuldung des Konzerns abbauen – und dessen Weg zur Klimaneutralität ebnen.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Fri, 15 Sep 2023 12:39:52 +0200', 'guid': 'https://www.handelsblatt.com/29393576.html', 'content:encoded': '\"\"Stefan Schulte bleibt Vorstandsvorsitzender von Fraport. Er muss in den kommenden Jahren die hohe Verschuldung des Konzerns abbauen – und dessen Weg zur Klimaneutralität ebnen.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/stefan-schulte-im-terminal-1-des-frankfurter-flughafens/29393602/2-format2020.jpg', '@type': 'image/jpeg', '@length': '509793'}}, {'title': 'Autoindustrie: Preiskampf in China bremst Absatz von Volkswagen', 'link': 'https://www.handelsblatt.com/unternehmen/industrie/autoindustrie-preiskampf-in-china-bremst-absatz-von-volkswagen/29393448.html', 'description': 'Europas größter Autokonzern musste sein Absatzziel für das laufende Jahr bereits nach unten korrigieren. In anderen Märkten entwickelt sich das Geschäft von VW positiv.', 'category': 'Industrie', 'pubDate': 'Fri, 15 Sep 2023 11:55:27 +0200', 'guid': 'https://www.handelsblatt.com/29393448.html', 'content:encoded': '\"\"Europas größter Autokonzern musste sein Absatzziel für das laufende Jahr bereits nach unten korrigieren. In anderen Märkten entwickelt sich das Geschäft von VW positiv.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/volkswagen-werk-in-shanghai/29393522/2-format2020.jpg', '@type': 'image/jpeg', '@length': '28972'}}, {'title': 'Nationalmannschaft: Rettig wird Geschäftsführer Sport beim DFB', 'link': 'https://www.handelsblatt.com/unternehmen/management/nationalmannschaft-andreas-rettig-wird-geschaeftsfuehrer-sport-beim-dfb-/29393462.html', 'description': 'Der Deutsche Fußball-Bund hat einen Nachfolger für Oliver Bierhoff gefunden. Der ehemalige DFL-Geschäftsführer Rettig soll den Bereich der Nationalmannschaft übernehmen.', 'category': 'Management', 'pubDate': 'Fri, 15 Sep 2023 11:38:02 +0200', 'guid': 'https://www.handelsblatt.com/29393462.html', 'content:encoded': '\"\"Der Deutsche Fußball-Bund hat einen Nachfolger für Oliver Bierhoff gefunden. Der ehemalige DFL-Geschäftsführer Rettig soll den Bereich der Nationalmannschaft übernehmen.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/andreas-rettig/29393518/2-format2020.jpg', '@type': 'image/jpeg', '@length': '395783'}}, {'title': 'Rüstungsindustrie: Lufthansa beteiligt sich an Rheinmetall-Konsortium für Kampfjet', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/ruestungsindustrie-lufthansa-beteiligt-sich-an-rheinmetall-konsortium-fuer-kampfjet/29393382.html', 'description': 'Rheinmetall produziert künftig Teile des Rumpfs des Lockheed-Kampfjets. Nun hat die Lufthansa bestätigt, dass sie sich an Fertigung und Wartung der Flugzeuge beteiligt.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Fri, 15 Sep 2023 11:22:21 +0200', 'guid': 'https://www.handelsblatt.com/29393382.html', 'content:encoded': '\"\"Rheinmetall produziert künftig Teile des Rumpfs des Lockheed-Kampfjets. Nun hat die Lufthansa bestätigt, dass sie sich an Fertigung und Wartung der Flugzeuge beteiligt.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/kampfjet-f-35/29393442/2-format2020.jpg', '@type': 'image/jpeg', '@length': '375575'}}, {'title': 'Dieselskandal: Verpflichtender Rückruf droht: Neue Vorwürfe gegen Mercedes im Dieselskandal', 'link': 'https://www.handelsblatt.com/unternehmen/industrie/dieselskandal-mercedes-droht-neuer-behoerdenaerger-wegen-e-klasse/29393172.html', 'description': 'In einer wichtigen Baureihe soll der Konzern mehrere Abschalteinrichtungen genutzt haben. Ein freiwilliges Software-Update von Mercedes könnte dem Kraftfahrt-Bundesamt nicht ausreichen.', 'category': 'Industrie', 'pubDate': 'Fri, 15 Sep 2023 10:31:01 +0200', 'guid': 'https://www.handelsblatt.com/29393172.html', 'content:encoded': '\"\"In einer wichtigen Baureihe soll der Konzern mehrere Abschalteinrichtungen genutzt haben. Ein freiwilliges Software-Update von Mercedes könnte dem Kraftfahrt-Bundesamt nicht ausreichen.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/mercedes-logo/29393210/3-format2020.jpg', '@type': 'image/jpeg', '@length': '534599'}}, {'title': 'Luftfahrt: Lufthansa lässt den Großraumjet A380 länger fliegen', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/luftfahrt-lufthansa-laesst-den-grossraumjet-a380-laenger-fliegen/29393056.html', 'description': 'Der Konzern leidet unter Lieferengpässen bei neuen Flugzeugen. Daher wird der Riesenjet A380 länger im Einsatz sein – und soll nun sogar eine neue Businessklasse bekommen.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Fri, 15 Sep 2023 10:00:00 +0200', 'guid': 'https://www.handelsblatt.com/29393056.html', 'content:encoded': '\"\"Der Konzern leidet unter Lieferengpässen bei neuen Flugzeugen. Daher wird der Riesenjet A380 länger im Einsatz sein – und soll nun sogar eine neue Businessklasse bekommen.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/a380-der-lufthansa/29393088/3-format2020.jpg', '@type': 'image/jpeg', '@length': '297925'}}, {'title': 'Bahn-Pünktlichkeit: Bahn-Pünktlichkeit auf Tiefpunkt: Jeder dritte Passagier war 2022 mit über 15 Minuten Verspätung am Ziel', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/bahn-puenktlichkeit-jeder-dritte-bahnreisende-war-2022-mit-mehr-als-15-minuten-verspaetung-am-ziel/29392970.html', 'description': 'Die pünktliche Ankunft ist entscheidend für ein gutes Bahn-Erlebnis, doch allzu oft scheitert es daran. Ein bestimmter Grund sticht dabei heraus.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Fri, 15 Sep 2023 07:58:20 +0200', 'guid': 'https://www.handelsblatt.com/29392970.html', 'content:encoded': '\"\"Die pünktliche Ankunft ist entscheidend für ein gutes Bahn-Erlebnis, doch allzu oft scheitert es daran. Ein bestimmter Grund sticht dabei heraus.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/ice/29392990/2-format2020.jpg', '@type': 'image/jpeg', '@length': '507442'}}, {'title': 'Unternehmensberater: Netzwerker und Problemlöser: So gewinnen Deutschlands Top-Berater das Vertrauen ihrer Auftraggeber', 'link': 'https://www.handelsblatt.com/unternehmen/dienstleister/unternehmensberater-so-gewinnen-deutschlands-top-berater-das-vertrauen-ihrer-auftraggeber/29391258.html', 'description': 'Kaum ein Konzern kommt ohne Unternehmensberater aus. Doch einige wenige stechen aus der Masse heraus, weil selbst Vorstandschefs auf sie hören. Das sind die wichtigsten Köpfe.', 'category': 'Dienstleister', 'pubDate': 'Fri, 15 Sep 2023 04:00:00 +0200', 'guid': 'https://www.handelsblatt.com/29391258.html', 'content:encoded': '\"\"Kaum ein Konzern kommt ohne Unternehmensberater aus. Doch einige wenige stechen aus der Masse heraus, weil selbst Vorstandschefs auf sie hören. Das sind die wichtigsten Köpfe.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/unternehmensberater/29392472/2-format2020.jpg', '@type': 'image/jpeg', '@length': '8070157'}}, {'title': 'Investmentbanking: Dienstleister statt „Master of the Universe“: Wie eine frische Generation von Bankern ihren Job neu definiert', 'link': 'https://www.handelsblatt.com/unternehmen/dienstleister/investmentbanking-wie-eine-frische-generation-von-bankern-ihren-job-neu-definiert/29391262.html', 'description': 'Dealmaker und Newcomer: Die deutsche Elite der Investmentbanker tickt anders als ihre Vorgänger bis vor wenigen Jahren. Das sind die spannendsten Köpfe.', 'category': 'Dienstleister', 'pubDate': 'Fri, 15 Sep 2023 04:00:00 +0200', 'guid': 'https://www.handelsblatt.com/29391262.html', 'content:encoded': '\"\"Dealmaker und Newcomer: Die deutsche Elite der Investmentbanker tickt anders als ihre Vorgänger bis vor wenigen Jahren. Das sind die spannendsten Köpfe.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/frankfurter-bankenviertel/29393596/2-format2020.jpg', '@type': 'image/jpeg', '@length': '1711319'}}, {'title': 'Management: Deutschlands neue Dealmaker: Wie Profis heute Vertrauen aufbauen und Geschäfte anbahnen', 'link': 'https://www.handelsblatt.com/unternehmen/dienstleister/management-deutschlands-neue-dealmaker-wie-profis-heute-vertrauen-aufbauen-und-geschaefte-anbahnen/29343370.html', 'description': 'Große Bugwelle und eindrucksvoller Titel? Das zieht beim Kontakteknüpfen immer seltener. So knüpfen Deutschlands neue Dealmaker heute ihre Netzwerke.', 'category': 'Dienstleister', 'pubDate': 'Fri, 15 Sep 2023 04:00:00 +0200', 'guid': 'https://www.handelsblatt.com/29343370.html', 'content:encoded': '\"\"Große Bugwelle und eindrucksvoller Titel? Das zieht beim Kontakteknüpfen immer seltener. So knüpfen Deutschlands neue Dealmaker heute ihre Netzwerke.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/die-dealmaker-der-deutschen-wirtschaft/29391896/4-format2020.png', '@type': 'image/png', '@length': '2299085'}}, {'title': 'Logistik: Frühere Haniel-Chefkontrolleurin Nowotne steigt bei der Kühne Holding ein', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/logistik-fruehere-haniel-chefkontrolleurin-doreen-nowotne-steigt-bei-der-kuehne-holding-ein/29392438.html', 'description': 'Die 50-Jährige wechselt ins Management der Beteiligungsgesellschaft von Milliardär Klaus Michael Kühne. Damit dürfte sie direkt mit einem großen Logistik-Deal beschäftigt sein.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Thu, 14 Sep 2023 18:56:10 +0200', 'guid': 'https://www.handelsblatt.com/29392438.html', 'content:encoded': '\"\"Die 50-Jährige wechselt ins Management der Beteiligungsgesellschaft von Milliardär Klaus Michael Kühne. Damit dürfte sie direkt mit einem großen Logistik-Deal beschäftigt sein.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/doreen-nowotne/28296996/4-format2020.jpg', '@type': 'image/jpeg', '@length': '164622'}}, {'title': 'HHLA-Deal: Hapag-Lloyd droht mit Abzug von Transportvolumen aus Hamburg', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/hhla-deal-hapag-lloyd-droht-mit-abzug-von-transportvolumen-aus-hamburg/29392196.html', 'description': 'Der Teilverkauf des Hafenbetreibers HHLA an MSC könnten der Reederei neue Konkurrenz vor der Haustür bescheren. Der Hapag-Chef droht mit Konsequenzen.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Thu, 14 Sep 2023 17:06:25 +0200', 'guid': 'https://www.handelsblatt.com/29392196.html', 'content:encoded': '\"\"Der Teilverkauf des Hafenbetreibers HHLA an MSC könnten der Reederei neue Konkurrenz vor der Haustür bescheren. Der Hapag-Chef droht mit Konsequenzen.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/reederei-hapag-lloyd/29392238/2-format2020.jpg', '@type': 'image/jpeg', '@length': '465839'}}, {'title': 'Tabakbranche: Zigaretten-Nachfrage sinkt, elektronische Alternativen sind im Trend', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/tabakbranche-zigaretten-nachfrage-sinkt-elektronische-alternativen-sind-im-trend/29392122.html', 'description': 'E-Zigaretten werden in Deutschland immer beliebter. Experten warnen allerdings davor, die gesundheitlichen Folgen zu unterschätzen. Auch von Umweltschützern gibt es Kritik.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Thu, 14 Sep 2023 16:51:57 +0200', 'guid': 'https://www.handelsblatt.com/29392122.html', 'content:encoded': '\"\"E-Zigaretten werden in Deutschland immer beliebter. Experten warnen allerdings davor, die gesundheitlichen Folgen zu unterschätzen. Auch von Umweltschützern gibt es Kritik.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/e-zigarette/29392160/2-format2020.jpg', '@type': 'image/jpeg', '@length': '1303227'}}, {'title': 'Logistik: Auch Eurogate-Aktionär erwägt Gegenangebot zum HHLA-Deal', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/logistik-auch-eurogate-aktionaer-erwaegt-gegenangebot-zum-hhla-deal/29392116.html', 'description': 'Nach dem Milliardär Kühne reagiert auch der Unternehmer Thomas Eckelmann mit scharfer Kritik am Teilverkauf des Hafenbetreibers. Er bringt ebenfalls eine Gegenofferte ins Spiel.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Thu, 14 Sep 2023 16:40:00 +0200', 'guid': 'https://www.handelsblatt.com/29392116.html', 'content:encoded': '\"\"Nach dem Milliardär Kühne reagiert auch der Unternehmer Thomas Eckelmann mit scharfer Kritik am Teilverkauf des Hafenbetreibers. Er bringt ebenfalls eine Gegenofferte ins Spiel.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/eurogate-containerschiff/29392164/2-format2020.jpg', '@type': 'image/jpeg', '@length': '797989'}}, {'title': 'Erneuerbare Energien: Vattenfall plant weiteren Windpark in der Nordsee bei Borkum', 'link': 'https://www.handelsblatt.com/unternehmen/energie/erneuerbare-energien-vattenfall-plant-weiteren-windpark-in-der-nordsee-bei-borkum/29391822.html', 'description': 'Es ist bereits das zweite Offshore-Projekt des schwedischen Konzerns in der Nähe der Insel. Zusammen sollen beide Anlagen Strom für mehr als 1,7 Millionen Haushalte produzieren.', 'category': 'Energie', 'pubDate': 'Thu, 14 Sep 2023 15:22:23 +0200', 'guid': 'https://www.handelsblatt.com/29391822.html', 'content:encoded': '\"\"Es ist bereits das zweite Offshore-Projekt des schwedischen Konzerns in der Nähe der Insel. Zusammen sollen beide Anlagen Strom für mehr als 1,7 Millionen Haushalte produzieren.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/offshore-windpark/29391886/3-format2020.jpg', '@type': 'image/jpeg', '@length': '523248'}}, {'title': 'Elektromobilität: Volkswagen baut in Zwickau Stellen ab – Zukunft von 2000 weiteren Befristeten ungewiss', 'link': 'https://www.handelsblatt.com/unternehmen/industrie/elektromobilitaet-volkswagen-baut-in-zwickau-hunderte-befristete-stellen-ab/29390984.html', 'description': 'Die Elektrowende bei Volkswagen stockt – das schlägt sich auch auf die Produktion nieder. Erste befristet Beschäftigte an VWs Haupt-Elektrostandort müssen nun gehen.', 'category': 'Industrie', 'pubDate': 'Thu, 14 Sep 2023 15:09:09 +0200', 'guid': 'https://www.handelsblatt.com/29390984.html', 'content:encoded': '\"\"Die Elektrowende bei Volkswagen stockt – das schlägt sich auch auf die Produktion nieder. Erste befristet Beschäftigte an VWs Haupt-Elektrostandort müssen nun gehen.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/vw-produktion-in-zwickau/29393960/2-format2020.jpg', '@type': 'image/jpeg', '@length': '562332'}}, {'title': 'Rechtsstreit: Gute Aussichten für VW in Streit um italienische Millionenstrafe', 'link': 'https://www.handelsblatt.com/unternehmen/industrie/rechtsstreit-gute-aussichten-fuer-vw-in-streit-um-italienische-millionenstrafe/29391826.html', 'description': 'Der Autobauer konnte im Verfahren um eine mögliche Doppelbestrafung einen Teilerfolg erzielen. In dem Rechtsstreit geht es um mutmaßlich illegale Abschalteinrichtungen.', 'category': 'Industrie', 'pubDate': 'Thu, 14 Sep 2023 15:04:00 +0200', 'guid': 'https://www.handelsblatt.com/29391826.html', 'content:encoded': '\"\"Der Autobauer konnte im Verfahren um eine mögliche Doppelbestrafung einen Teilerfolg erzielen. In dem Rechtsstreit geht es um mutmaßlich illegale Abschalteinrichtungen.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/volkswagen/29391842/2-format2020.jpg', '@type': 'image/jpeg', '@length': '408014'}}, {'title': 'Bärchenwurst-Produzent: Deutscher Hersteller beantragt als erster in der EU Zulassung von Fleisch aus Zellkulturen', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/baerchenwurst-firma-erstmals-verkauf-von-laborfleisch-in-der-eu-beantragt/29391370.html', 'description': 'Fleisch, das im Bioreaktor gezüchtet wird, ist bisher nur in Singapur und den USA zugelassen. Nun will ein deutsches Familienunternehmen in der EU zum Pionier werden.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Thu, 14 Sep 2023 14:00:00 +0200', 'guid': 'https://www.handelsblatt.com/29391370.html', 'content:encoded': '\"\"Fleisch, das im Bioreaktor gezüchtet wird, ist bisher nur in Singapur und den USA zugelassen. Nun will ein deutsches Familienunternehmen in der EU zum Pionier werden.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/symbolbild-hotdogs/29391650/3-format2020.jpg', '@type': 'image/jpeg', '@length': '478283'}}, {'title': 'Branchenverband VCI: „Keine Erholung in Sicht“: Chemieindustrie erwartet weitere Einbußen', 'link': 'https://www.handelsblatt.com/unternehmen/industrie/branchenverband-vci-keine-erholung-in-sicht-chemieindustrie-erwartet-weitere-einbussen/29391256.html', 'description': 'Die deutsche Chemieindustrie drosselt ihre Produktion noch stärker als erwartet. In besonders betroffenen Bereichen steht bereits Kurzarbeit auf der Agenda.', 'category': 'Industrie', 'pubDate': 'Thu, 14 Sep 2023 11:32:00 +0200', 'guid': 'https://www.handelsblatt.com/29391256.html', 'content:encoded': '\"\"Die deutsche Chemieindustrie drosselt ihre Produktion noch stärker als erwartet. In besonders betroffenen Bereichen steht bereits Kurzarbeit auf der Agenda.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/chemieanlage-in-sachsen/29391314/2-format2020.jpg', '@type': 'image/jpeg', '@length': '289018'}}, {'title': 'Einzelhandel: Versteckte Preiserhöhungen: Carrefour prangert Nestlé, Pepsi und Unilever an', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/einzelhandel-carrefour-prangert-nestle-pepsi-und-unilever-wegen-versteckter-preiserhoehungen-an/29391006.html', 'description': 'Frankreich kämpft seit Monaten gegen steigende Lebensmittelpreise. Nun warnt die Kette Carrefour die Kunden vor gewissen Produkten. Das soll den Druck auf die Großkonzerne erhöhen.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Thu, 14 Sep 2023 10:35:49 +0200', 'guid': 'https://www.handelsblatt.com/29391006.html', 'content:encoded': '\"\"Frankreich kämpft seit Monaten gegen steigende Lebensmittelpreise. Nun warnt die Kette Carrefour die Kunden vor gewissen Produkten. Das soll den Druck auf die Großkonzerne erhöhen.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/carrefour-markt/29391138/3-format2020.jpg', '@type': 'image/jpeg', '@length': '541675'}}, {'title': 'Industriekonzern: Thyssen-Krupp macht Klimaschutz zum Geschäft: Konzern bündelt grüne Technologien', 'link': 'https://www.handelsblatt.com/unternehmen/industrie/greentech-thyssen-krupp-buendelt-technologien-in-neuer-sparte-decarbon/29390636.html', 'description': 'Thyssen-Krupp gründet eine neue Sparte. Bei der Umsetzung des Effizienzprogramms für den Konzern schlägt Vorstandschef Lopez zudem einen anderen Kurs ein als seine Vorgängerin.', 'category': 'Industrie', 'pubDate': 'Thu, 14 Sep 2023 08:02:42 +0200', 'guid': 'https://www.handelsblatt.com/29390636.html', 'content:encoded': '\"\"Thyssen-Krupp gründet eine neue Sparte. Bei der Umsetzung des Effizienzprogramms für den Konzern schlägt Vorstandschef Lopez zudem einen anderen Kurs ein als seine Vorgängerin.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/thyssen-krupp-setzt-auf-gruen/29390700/2-format2020.jpg', '@type': 'image/jpeg', '@length': '515385'}}, {'title': 'Gas: Chevron-Streik in Australien könnte sich auf LNG-Versorgung in Europa auswirken', 'link': 'https://www.handelsblatt.com/unternehmen/energie/gas-chevron-streik-in-australien-koennte-sich-auf-lng-versorgung-in-europa-auswirken/29390458.html', 'description': 'Zwei Wochen sollen zwei LNG-Projekte bestreikt werden. Dass der Ausfall länger anhält und zu einem anhaltenden Anstieg der Gaspreise führt, sei jedoch unwahrscheinlich.', 'category': 'Energie', 'pubDate': 'Thu, 14 Sep 2023 03:30:00 +0200', 'guid': 'https://www.handelsblatt.com/29390458.html', 'content:encoded': '\"\"Zwei Wochen sollen zwei LNG-Projekte bestreikt werden. Dass der Ausfall länger anhält und zu einem anhaltenden Anstieg der Gaspreise führt, sei jedoch unwahrscheinlich.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/chevron/29390470/2-format2020.jpg', '@type': 'image/jpeg', '@length': '501369'}}, {'title': 'Biotechkonzern: Moderna punktet mit Daten zu mRNA-Grippeimpfstoff – Aktie steigt um sieben Prozent', 'link': 'https://www.handelsblatt.com/unternehmen/industrie/biotechkonzern-moderna-punktet-mit-daten-zu-mrna-grippeimpfstoff/29389474.html', 'description': 'Aufgrund der sinkenden Nachfrage nach Covid-Vakzinen setzt der US-Konzern auf neue Mittel. Der Grippeimpfstoff könnte nun schon in einem Jahr auf den Markt kommen.', 'category': 'Industrie', 'pubDate': 'Wed, 13 Sep 2023 16:35:04 +0200', 'guid': 'https://www.handelsblatt.com/29389474.html', 'content:encoded': '\"\"Aufgrund der sinkenden Nachfrage nach Covid-Vakzinen setzt der US-Konzern auf neue Mittel. Der Grippeimpfstoff könnte nun schon in einem Jahr auf den Markt kommen.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/moderna/29389522/3-format2020.jpg', '@type': 'image/jpeg', '@length': '285836'}}, {'title': 'E-Autobauer: Vietnam: VinFast will in Asien expandieren', 'link': 'https://www.handelsblatt.com/unternehmen/industrie/e-autobauer-vietnam-vinfast-will-in-asien-expandieren/29388566.html', 'description': 'Das Unternehmen plant eine Expansion in sieben asiatische Märkte und ein Werk in Indonesien. Zudem will der Autobauer noch weitere Märkte im Ausland erschließen.', 'category': 'Industrie', 'pubDate': 'Wed, 13 Sep 2023 12:06:00 +0200', 'guid': 'https://www.handelsblatt.com/29388566.html', 'content:encoded': '\"\"Das Unternehmen plant eine Expansion in sieben asiatische Märkte und ein Werk in Indonesien. Zudem will der Autobauer noch weitere Märkte im Ausland erschließen.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/-vinfast-/29388568/2-format2020.jpg', '@type': 'image/jpeg', '@length': '613900'}}, {'title': 'Hamburger Hafen: „Diese Lösung ist ein Affront“ – Kühne erwägt Gegenofferte für Hamburger Hafenbetreiber', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/hamburger-hafen-kuehne-erwaegt-gegenofferte-fuer-hamburger-hafenbetreiber-hhla/29388256.html', 'description': 'Der Milliardär will sich mit dem Teilverkauf des Hamburger Hafenbetreibers an MSC nicht abfinden. Dabei könnte Kühne auch über seine eigene Holding eingreifen.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Wed, 13 Sep 2023 11:04:29 +0200', 'guid': 'https://www.handelsblatt.com/29388256.html', 'content:encoded': '\"\"Der Milliardär will sich mit dem Teilverkauf des Hamburger Hafenbetreibers an MSC nicht abfinden. Dabei könnte Kühne auch über seine eigene Holding eingreifen.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/containerschiff-von-msc-in-hamburg/29388468/2-format2020.jpg', '@type': 'image/jpeg', '@length': '624341'}}, {'title': 'Luftfahrt: Führung der Kabinengewerkschaft UFO tritt zurück', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/luftfahrt-fuehrung-der-kabinengewerkschaft-ufo-tritt-zurueck/29388142.html', 'description': 'Beide UFO-Vorsitzende haben nach eigenen Angaben einen neuen Job. Ihre Aufgabe, die angeschlagene Arbeitnehmervertretung auf Kurs zu bringen, sei erfüllt.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Wed, 13 Sep 2023 10:30:00 +0200', 'guid': 'https://www.handelsblatt.com/29388142.html', 'content:encoded': '\"\"Beide UFO-Vorsitzende haben nach eigenen Angaben einen neuen Job. Ihre Aufgabe, die angeschlagene Arbeitnehmervertretung auf Kurs zu bringen, sei erfüllt.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/daniel-kassa-mbuambi/29388200/2-format2020.jpg', '@type': 'image/jpeg', '@length': '24852'}}, {'title': 'E-Autos: Sorge vor Jobabbau bei VW in Zwickau – Betriebsversammlung geplant', 'link': 'https://www.handelsblatt.com/unternehmen/industrie/e-autos-sorge-vor-jobabbau-bei-vw-in-zwickau-betriebsversammlung-geplant/29388148.html', 'description': 'Die geringe Nachfrage nach Elektroautos könnte VW-Mitarbeitende mit befristeten Verträgen ihre Anstellung kosten. Vertrauensleute der IG Metall wenden sich jetzt mit einem Brief an die Geschäftsführung.', 'category': 'Industrie', 'pubDate': 'Wed, 13 Sep 2023 10:00:00 +0200', 'guid': 'https://www.handelsblatt.com/29388148.html', 'content:encoded': '\"\"Die geringe Nachfrage nach Elektroautos könnte VW-Mitarbeitende mit befristeten Verträgen ihre Anstellung kosten. Vertrauensleute der IG Metall wenden sich jetzt mit einem Brief an die Geschäftsführung.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/vw-werk-in-zwickau/29388166/2-format2020.jpg', '@type': 'image/jpeg', '@length': '629358'}}, {'title': 'Automatisierung: ABB baut neues Roboterwerk: „Produktion in Europa wird nur mit mehr Automatisierung überleben“', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/automatisierung-abb-baut-neues-roboterwerk-in-schweden/29384396.html', 'description': 'Der Konzern investiert 280 Millionen Dollar in einen neuen Robotik-Campus. Experten rechnen mit weiter wachsender Nachfrage in Europa – noch haben sich einige Hoffnungen aber nicht erfüllt.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Wed, 13 Sep 2023 09:00:00 +0200', 'guid': 'https://www.handelsblatt.com/29384396.html', 'content:encoded': '\"\"Der Konzern investiert 280 Millionen Dollar in einen neuen Robotik-Campus. Experten rechnen mit weiter wachsender Nachfrage in Europa – noch haben sich einige Hoffnungen aber nicht erfüllt.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/roboter-von-abb-im-einsatz/29387076/2-format2020.jpg', '@type': 'image/jpeg', '@length': '1040732'}}, {'title': 'Truck-Hersteller: Teurer als gedacht – Entwicklung selbstfahrender Lkw geht nur langsam voran', 'link': 'https://www.handelsblatt.com/unternehmen/industrie/truck-hersteller-teurer-als-gedacht-entwicklung-selbstfahrender-lkw-geht-nur-langsam-voran/29386888.html', 'description': 'Autonomes Fahren auf der Straße kommt auch bei Lkw\\xa0später als geplant. Die Branche klagt über hohe Entwicklungskosten und den fehlenden politischen Willen.', 'category': 'Industrie', 'pubDate': 'Wed, 13 Sep 2023 04:00:00 +0200', 'guid': 'https://www.handelsblatt.com/29386888.html', 'content:encoded': '\"\"Autonomes Fahren auf der Straße kommt auch bei Lkw\\xa0später als geplant. Die Branche klagt über hohe Entwicklungskosten und den fehlenden politischen Willen.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/selbstfahrender-lkw-von-daimler/29387132/2-format2020.jpg', '@type': 'image/jpeg', '@length': '314682'}}, {'title': 'IPO: Birkenstock beantragt Börsengang in den USA – Als Luxusmarke winkt eine hohe Bewertung', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/ipo-birkenstock-beantragt-boersengang-in-den-usa/29387586.html', 'description': 'Der deutsche Sandalenhersteller geht in der zweiten Oktoberwoche an die New Yorker Börse. Ein französischer Luxuskonzern spielt dabei eine entscheidende Rolle.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Tue, 12 Sep 2023 23:41:00 +0200', 'guid': 'https://www.handelsblatt.com/29387586.html', 'content:encoded': '\"\"Der deutsche Sandalenhersteller geht in der zweiten Oktoberwoche an die New Yorker Börse. Ein französischer Luxuskonzern spielt dabei eine entscheidende Rolle.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/sandalette-von-birkenstock/29388854/3-format2020.jpg', '@type': 'image/jpeg', '@length': '323255'}}, {'title': 'Bernard Looney: Verstöße gegen Verhaltenskodex: BP-Chef tritt mit sofortiger Wirkung zurück', 'link': 'https://www.handelsblatt.com/unternehmen/energie/oelkonzern-bp-chef-looney-tritt-mit-sofortiger-wirkung-zurueck/29387528.html', 'description': 'Bernard Looney hatte frühere Beziehungen zu Arbeitskollegen nicht vollständig transparent gemacht. Finanzchef Murray Auchincloss wird übergangsweise den Vorstandsvorsitz übernehmen.', 'category': 'Energie', 'pubDate': 'Tue, 12 Sep 2023 21:27:00 +0200', 'guid': 'https://www.handelsblatt.com/29387528.html', 'content:encoded': '\"\"Bernard Looney hatte frühere Beziehungen zu Arbeitskollegen nicht vollständig transparent gemacht. Finanzchef Murray Auchincloss wird übergangsweise den Vorstandsvorsitz übernehmen.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/bernard-looney/29387530/2-format2020.jpg', '@type': 'image/jpeg', '@length': '348761'}}, {'title': 'Syntellix AG: Verdacht auf Insolvenzverschleppung: Ermittlungen gegen Utz Claassen', 'link': 'https://www.handelsblatt.com/unternehmen/industrie/syntellix-ag-verdacht-auf-insolvenzverschleppung-ermittlungen-gegen-utz-claassen/29386436.html', 'description': 'Mitarbeiter warten auf Gehälter, Anwälte auf Honorare. Jetzt ist der Vorstandschef im Visier der Justiz. Er weist die Vorwürfe als haltlos zurück.', 'category': 'Industrie', 'pubDate': 'Tue, 12 Sep 2023 18:31:56 +0200', 'guid': 'https://www.handelsblatt.com/29386436.html', 'content:encoded': '\"\"Mitarbeiter warten auf Gehälter, Anwälte auf Honorare. Jetzt ist der Vorstandschef im Visier der Justiz. Er weist die Vorwürfe als haltlos zurück.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/utz-claassen/29389318/2-format2020.jpg', '@type': 'image/jpeg', '@length': '518491'}}, {'title': 'Luftfahrt: Lufthansa lässt 20 Flugzeuge wegen Triebwerksproblem am Boden', 'link': 'https://www.handelsblatt.com/unternehmen/handel-konsumgueter/luftfahrt-lufthansa-laesst-20-flugzeuge-wegen-triebwerksproblem-am-boden/29387380.html', 'description': 'Wegen Mängeln müssen zahlreiche Triebwerke des Herstellers Pratt & Whitney überarbeitet werden. Das trifft auch die größte deutsche Fluggesellschaft.', 'category': 'Handel + Konsumgüter', 'pubDate': 'Tue, 12 Sep 2023 18:17:09 +0200', 'guid': 'https://www.handelsblatt.com/29387380.html', 'content:encoded': '\"\"Wegen Mängeln müssen zahlreiche Triebwerke des Herstellers Pratt & Whitney überarbeitet werden. Das trifft auch die größte deutsche Fluggesellschaft.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/lufthansa-flugzeug-des-modells-a320-neo/29389166/2-format2020.jpg', '@type': 'image/jpeg', '@length': '275726'}}, {'title': 'Pay-TV-Sender: Barny Mills steigt zum Chef von Sky Deutschland auf', 'link': 'https://www.handelsblatt.com/unternehmen/it-medien/pay-tv-sender-barny-mills-steigt-zum-chef-von-sky-deutschland-auf/29387120.html', 'description': 'Der bisherige Finanzvorstand wird Nachfolger von Devesh Raj. Dem Bezahlsender setzt vor allem die Konkurrenz durch Streaming-Dienste stark zu.', 'category': 'Medien', 'pubDate': 'Tue, 12 Sep 2023 17:04:57 +0200', 'guid': 'https://www.handelsblatt.com/29387120.html', 'content:encoded': '\"\"Der bisherige Finanzvorstand wird Nachfolger von Devesh Raj. Dem Bezahlsender setzt vor allem die Konkurrenz durch Streaming-Dienste stark zu.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/hauptsitz-von-sky-deutschland-in-unterfoehring-bei-muenchen/29387158/2-format2020.jpg', '@type': 'image/jpeg', '@length': '756209'}}, {'title': 'Detecon: „Das ist nicht unser Wachstumsweg“: Neuer Chef will Telekom-Beratertochter breiter aufstellen', 'link': 'https://www.handelsblatt.com/unternehmen/dienstleister/juergen-schaefer-neuer-chef-will-telekom-beratertochter-detecon-breiter-aufstellen-/29380426.html', 'description': 'Mit oder ohne die Telekom? Der ehemalige Roland-Berger-Berater Jürgen Schäfer will mehr Synergien mit dem Mutterkonzern nutzen – und gleichzeitig externe Kunden gewinnen.', 'category': 'Dienstleister', 'pubDate': 'Tue, 12 Sep 2023 15:47:29 +0200', 'guid': 'https://www.handelsblatt.com/29380426.html', 'content:encoded': '\"\"Mit oder ohne die Telekom? Der ehemalige Roland-Berger-Berater Jürgen Schäfer will mehr Synergien mit dem Mutterkonzern nutzen – und gleichzeitig externe Kunden gewinnen.', 'enclosure': {'@url': 'https://www.handelsblatt.com/images/juergen-schaefer/29384144/3-format2020.jpg', '@type': 'image/jpeg', '@length': '253142'}}]\n" + ] + } + ], "source": [ "handelsblatt = HandelsblattRSS()\n", "\n", "items = handelsblatt.get_news_for_category()\n", + "print(items)\n", + "# from utils.mongodb.mongo import MongoConnector, MongoNewsService\n", "\n", - "from utils.mongodb.mongo import MongoConnector, MongoNewsService\n", + "# connector = MongoConnector(\n", + "# hostname=\"trisnol.tech\",\n", + "# database=\"transparenzregister\",\n", + "# username=\"root\",\n", + "# password=\"pR0R0v2e2\",\n", + "# )\n", "\n", - "connector = MongoConnector(\n", - " hostname=\"trisnol.tech\",\n", - " database=\"transparenzregister\",\n", - " username=\"root\",\n", - " password=\"pR0R0v2e2\",\n", - ")\n", - "\n", - "service = MongoNewsService(connector)" + "# service = MongoNewsService(connector)" ] }, { @@ -870,7 +878,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.7" + "version": "3.11.3" }, "orig_nbformat": 4 }, diff --git a/Jupyter/API-tests/Unternehmensregister/notebook.ipynb b/Jupyter/API-tests/Unternehmensregister/notebook.ipynb index f0eb465..1b5e7a5 100644 --- a/Jupyter/API-tests/Unternehmensregister/notebook.ipynb +++ b/Jupyter/API-tests/Unternehmensregister/notebook.ipynb @@ -487,6 +487,17 @@ "num_files" ] }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import glob\n", + "import xmltodict" + ] + }, { "cell_type": "code", "execution_count": 3, @@ -3905,11 +3916,6 @@ } ], "source": [ - "import json\n", - "import glob\n", - "import xmltodict\n", - "\n", - "\n", "def transform_xml_to_json(source_dir: str, target_dir: str):\n", " for source_path in [\n", " os.path.normpath(i) for i in glob.glob(source_dir + \"**/*.xml\", recursive=True)\n", @@ -3935,7 +3941,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -3957,41 +3963,62 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 18, "metadata": {}, "outputs": [], "source": [ - "from models.Company import Company\n", + "import re\n", + "from aki_prj23_transparenzregister.models.company import Company\n", "\n", "\n", "def parse_stakeholder(data: dict) -> list:\n", " if \"Natuerliche_Person\" in data[\"Beteiligter\"]:\n", - " return {\n", - " \"name\": {\n", - " \"firstname\": data[\"Beteiligter\"][\"Natuerliche_Person\"][\"Voller_Name\"][\n", - " \"Vorname\"\n", - " ],\n", - " \"lastname\": data[\"Beteiligter\"][\"Natuerliche_Person\"][\"Voller_Name\"][\n", + " # It's a Compnay serving as a \"Kommanditist\" or similar\n", + " if data[\"Beteiligter\"][\"Natuerliche_Person\"][\"Voller_Name\"][\"Vorname\"] is None:\n", + " return {\n", + " \"description\": data[\"Beteiligter\"][\"Natuerliche_Person\"][\"Voller_Name\"][\n", " \"Nachname\"\n", " ],\n", - " },\n", - " \"date_of_birth\": data[\"Beteiligter\"][\"Natuerliche_Person\"][\"Geburt\"][\n", - " \"Geburtsdatum\"\n", - " ]\n", - " if \"Geburt\" in data[\"Beteiligter\"][\"Natuerliche_Person\"]\n", - " else None,\n", - " \"location\": {\n", - " \"city\": data[\"Beteiligter\"][\"Natuerliche_Person\"][\"Anschrift\"][-1][\n", - " \"Ort\"\n", + " \"location\": {\n", + " \"city\": data[\"Beteiligter\"][\"Natuerliche_Person\"][\"Anschrift\"][-1][\n", + " \"Ort\"\n", + " ]\n", + " if type(data[\"Beteiligter\"][\"Natuerliche_Person\"][\"Anschrift\"])\n", + " == list\n", + " else data[\"Beteiligter\"][\"Natuerliche_Person\"][\"Anschrift\"][\"Ort\"]\n", + " },\n", + " \"role\": data[\"Rolle\"][\"Rollenbezeichnung\"][\"content\"],\n", + " \"type\": \"Company\",\n", + " }\n", + " else:\n", + " return {\n", + " \"name\": {\n", + " \"firstname\": data[\"Beteiligter\"][\"Natuerliche_Person\"][\n", + " \"Voller_Name\"\n", + " ][\"Vorname\"],\n", + " \"lastname\": data[\"Beteiligter\"][\"Natuerliche_Person\"][\n", + " \"Voller_Name\"\n", + " ][\"Nachname\"],\n", + " },\n", + " \"date_of_birth\": data[\"Beteiligter\"][\"Natuerliche_Person\"][\"Geburt\"][\n", + " \"Geburtsdatum\"\n", " ]\n", - " if type(data[\"Beteiligter\"][\"Natuerliche_Person\"][\"Anschrift\"]) == list\n", - " else data[\"Beteiligter\"][\"Natuerliche_Person\"][\"Anschrift\"][\"Ort\"]\n", - " },\n", - " \"role\": data[\"Rolle\"][\"Rollenbezeichnung\"][\"content\"],\n", - " }\n", + " if \"Geburt\" in data[\"Beteiligter\"][\"Natuerliche_Person\"]\n", + " else None,\n", + " \"location\": {\n", + " \"city\": data[\"Beteiligter\"][\"Natuerliche_Person\"][\"Anschrift\"][-1][\n", + " \"Ort\"\n", + " ]\n", + " if type(data[\"Beteiligter\"][\"Natuerliche_Person\"][\"Anschrift\"])\n", + " == list\n", + " else data[\"Beteiligter\"][\"Natuerliche_Person\"][\"Anschrift\"][\"Ort\"]\n", + " },\n", + " \"role\": data[\"Rolle\"][\"Rollenbezeichnung\"][\"content\"],\n", + " \"type\": \"Person\",\n", + " }\n", " if \"Organisation\" in data[\"Beteiligter\"]:\n", " return {\n", - " \"role\": \"Organisation\",\n", + " \"role\": data[\"Rolle\"][\"Rollenbezeichnung\"][\"content\"],\n", " \"description\": data[\"Beteiligter\"][\"Organisation\"][\"Bezeichnung\"][\n", " \"Bezeichnung_Aktuell\"\n", " ],\n", @@ -4009,6 +4036,7 @@ " \"Postleitzahl\"\n", " ],\n", " },\n", + " \"type\": \"Company\",\n", " }\n", "\n", "\n", @@ -4111,6 +4139,156 @@ " ][\"Organisation\"][\"Bezeichnung\"][\"Bezeichnung_Aktuell\"]\n", "\n", "\n", + "# TODO Not present in all companies - possibly map using name of company ...\n", + "def map_rechtsform(company_name: str, data: dict) -> str:\n", + " try:\n", + " return data[\"XJustiz_Daten\"][\"Fachdaten_Register\"][\"Basisdaten_Register\"][\n", + " \"Rechtstraeger\"\n", + " ][\"Rechtsform\"][\"content\"]\n", + " except:\n", + " if (\n", + " company_name.endswith(\"GmbH\")\n", + " or company_name.endswith(\"UG\")\n", + " or company_name.endswith(\"UG (haftungsbeschränkt)\")\n", + " ):\n", + " return \"Gesellschaft mit beschränkter Haftung\"\n", + " elif company_name.endswith(\"SE\"):\n", + " return \"Europäische Aktiengesellschaft (SE)\"\n", + " elif company_name.endswith(\"KG\"):\n", + " return \"Kommanditgesellschaft\"\n", + " return None\n", + "\n", + "\n", + "def map_stammkapital(data: dict, company_type: str) -> str:\n", + " capital = {\"Zahl\": 0, \"Waehrung\": \"\"}\n", + " if company_type == \"Kommanditgesellschaft\":\n", + " if \"Zusatzangaben\" not in data[\"XJustiz_Daten\"][\"Fachdaten_Register\"]:\n", + " return None\n", + " capital_type = \"Hafteinlage\"\n", + " base = data[\"XJustiz_Daten\"][\"Fachdaten_Register\"][\"Zusatzangaben\"][\n", + " \"Personengesellschaft\"\n", + " ][\"Zusatz_KG\"][\"Daten_Kommanditist\"]\n", + " if isinstance(base, list):\n", + " for entry in base:\n", + " # TODO link to persons using Ref_Rollennummer then extract [\"Hafteinlage\"] as below\n", + " capital[\"Zahl\"] = capital[\"Zahl\"] + float(entry[\"Hafteinlage\"][\"Zahl\"])\n", + " # TODO Improve multi assignment\n", + " capital[\"Waehrung\"] = entry[\"Hafteinlage\"][\"Waehrung\"]\n", + " elif type(base) == \"dict\":\n", + " capital = base[\"Hafteinlage\"]\n", + " elif company_type in [\n", + " \"Gesellschaft mit beschränkter Haftung\",\n", + " \"Europäische Aktiengesellschaft (SE)\",\n", + " \"Aktiengesellschaft\",\n", + " \"Kommanditgesellschaft auf Aktien\",\n", + " \"Rechtsform ausländischen Rechts HRB\",\n", + " ]:\n", + " if \"Zusatzangaben\" not in data[\"XJustiz_Daten\"][\"Fachdaten_Register\"]:\n", + " return None\n", + " if (\n", + " \"Zusatz_GmbH\"\n", + " in data[\"XJustiz_Daten\"][\"Fachdaten_Register\"][\"Zusatzangaben\"][\n", + " \"Kapitalgesellschaft\"\n", + " ]\n", + " ):\n", + " capital_type = \"Stammkapital\"\n", + " capital = data[\"XJustiz_Daten\"][\"Fachdaten_Register\"][\"Zusatzangaben\"][\n", + " \"Kapitalgesellschaft\"\n", + " ][\"Zusatz_GmbH\"][\"Stammkapital\"]\n", + " elif (\n", + " \"Zusatz_Aktiengesellschaft\"\n", + " in data[\"XJustiz_Daten\"][\"Fachdaten_Register\"][\"Zusatzangaben\"][\n", + " \"Kapitalgesellschaft\"\n", + " ]\n", + " ):\n", + " capital_type = \"Grundkapital\"\n", + " capital = data[\"XJustiz_Daten\"][\"Fachdaten_Register\"][\"Zusatzangaben\"][\n", + " \"Kapitalgesellschaft\"\n", + " ][\"Zusatz_Aktiengesellschaft\"][\"Grundkapital\"][\"Hoehe\"]\n", + " elif company_type in [\n", + " \"Einzelkaufmann\",\n", + " \"Einzelkauffrau\",\n", + " \"eingetragene Genossenschaft\",\n", + " \"Partnerschaft\",\n", + " \"Einzelkaufmann / Einzelkauffrau\",\n", + " \"Offene Handelsgesellschaft\",\n", + " \"Partnerschaftsgesellschaft\",\n", + " None,\n", + " ]:\n", + " return None\n", + " else:\n", + " return None\n", + " return {\n", + " \"value\": float(capital[\"Zahl\"]),\n", + " \"currency\": capital[\"Waehrung\"],\n", + " \"type\": capital_type,\n", + " }\n", + "\n", + "\n", + "def map_geschaeftszweck(data: dict) -> str:\n", + " try:\n", + " return data[\"XJustiz_Daten\"][\"Fachdaten_Register\"][\"Basisdaten_Register\"][\n", + " \"Gegenstand_oder_Geschaeftszweck\"\n", + " ]\n", + " except:\n", + " return None\n", + "\n", + "\n", + "from datetime import datetime\n", + "\n", + "\n", + "def transform_date_to_iso(date: str) -> str:\n", + " regex_yy = r\"^\\d{1,2}\\.\\d{1,2}\\.\\d{2}$\"\n", + "\n", + " if re.match(regex_yy, date):\n", + " input_format = \"%d.%m.%y\"\n", + " else:\n", + " input_format = \"%d.%m.%Y\"\n", + " date_temp = datetime.strptime(date, input_format)\n", + " return date_temp.strftime(\"%Y-%m-%d\")\n", + "\n", + "\n", + "# TODO transform date to iso format (YYYY-MM-DD)\n", + "def map_founding_date(data: dict) -> str:\n", + " text = str(data)\n", + " entry_date = re.findall(\n", + " r\".Tag der ersten Eintragung:(\\\\n| )?(\\d{1,2}\\.\\d{1,2}\\.\\d{2,4})\", text\n", + " )\n", + " if len(entry_date) == 1:\n", + " return transform_date_to_iso(entry_date[0][1])\n", + "\n", + " entry_date = re.findall(\n", + " r\".Gesellschaftsvertrag vom (\\d{1,2}\\.\\d{1,2}\\.\\d{2,4})\", text\n", + " )\n", + " if len(entry_date) == 1:\n", + " return transform_date_to_iso(entry_date[0])\n", + "\n", + " if \"Eintragungstext\" in data[\"XJustiz_Daten\"][\"Fachdaten_Register\"][\"Auszug\"]:\n", + " if (\n", + " type(\n", + " data[\"XJustiz_Daten\"][\"Fachdaten_Register\"][\"Auszug\"][\"Eintragungstext\"]\n", + " )\n", + " == \"list\"\n", + " ):\n", + " temp = data[\"XJustiz_Daten\"][\"Fachdaten_Register\"][\"Auszug\"][\n", + " \"Eintragungstext\"\n", + " ][0][\"Text\"]\n", + " results = re.findall(r\"\\d{1,2}\\.\\d{1,2}\\.\\d{2,4}\", temp)\n", + " if len(temp) == 1:\n", + " return transform_date_to_iso(results[0])\n", + " if (\n", + " \"Gruendungsmetadaten\"\n", + " in data[\"XJustiz_Daten\"][\"Fachdaten_Register\"][\"Basisdaten_Register\"]\n", + " ):\n", + " temp = data[\"XJustiz_Daten\"][\"Fachdaten_Register\"][\"Basisdaten_Register\"][\n", + " \"Gruendungsmetadaten\"\n", + " ][\"Gruendungsdatum\"]\n", + " return temp\n", + " # No reliable answer\n", + " # raise ValueError()\n", + " return None\n", + "\n", + "\n", "def map_unternehmensregister_json(data: dict) -> dict:\n", " result = {\"relationships\": []}\n", "\n", @@ -4148,6 +4326,11 @@ " result[\"last_update\"] = data[\"XJustiz_Daten\"][\"Fachdaten_Register\"][\"Auszug\"][\n", " \"letzte_Eintragung\"\n", " ]\n", + " # TODO New features --> to be tested\n", + " result[\"company_type\"] = map_rechtsform(result[\"name\"], data)\n", + " result[\"capital\"] = map_stammkapital(data, result[\"company_type\"])\n", + " result[\"business_purpose\"] = map_geschaeftszweck(data)\n", + " result[\"founding_date\"] = map_founding_date(data)\n", "\n", " for i in range(\n", " 2, len(data[\"XJustiz_Daten\"][\"Grunddaten\"][\"Verfahrensdaten\"][\"Beteiligung\"])\n", @@ -4161,90 +4344,118 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "import glob\n", + "import dataclasses\n", + "from tqdm import tqdm" + ] + }, + { + "cell_type": "code", + "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - " 0%| | 0/3381 [00:00= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\")"} mypy = {version = ">=0.910", optional = true, markers = "python_version >= \"3\" and extra == \"mypy\""} sqlalchemy2-stubs = {version = "*", optional = true, markers = "extra == \"mypy\""} @@ -5779,10 +5791,21 @@ files = [ [package.dependencies] h11 = ">=0.9.0,<1" +[[package]] +name = "xmltodict" +version = "0.13.0" +description = "Makes working with XML feel like you are working with JSON" +optional = false +python-versions = ">=3.4" +files = [ + {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, + {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, +] + [extras] ingest = ["selenium"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "cb71ea0797629bb28e89620e47e3b79dd04718e4e5bd75404b15e8e7ab2cf653" +content-hash = "2496706146d1d83ba9f22d7d4ddc9de7019803cc9c6ebeccb2372610ec1cf736" diff --git a/pyproject.toml b/pyproject.toml index 0d730db..104b700 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ version = "0.1.0" [tool.poetry.dependencies] SQLAlchemy = {version = "^1.4.49", extras = ["mypy"]} +aenum = "^3.1.15" cachetools = "^5.3.1" dash = "^2.13.0" dash-bootstrap-components = "^1.5.0" @@ -50,6 +51,7 @@ python-dotenv = "^1.0.0" seaborn = "^0.12.2" selenium = "^4.12.0" tqdm = "^4.66.1" +xmltodict = "^0.13.0" [tool.poetry.extras] ingest = ["selenium"] diff --git a/src/aki_prj23_transparenzregister/models/auditor.py b/src/aki_prj23_transparenzregister/models/auditor.py index 28856a8..1e36b22 100644 --- a/src/aki_prj23_transparenzregister/models/auditor.py +++ b/src/aki_prj23_transparenzregister/models/auditor.py @@ -10,7 +10,7 @@ class Auditor: company: str | None def to_dict(self) -> dict: - """_summary_. + """Transform to dict. Returns: dict: _description_ diff --git a/src/aki_prj23_transparenzregister/models/company.py b/src/aki_prj23_transparenzregister/models/company.py index 11c0a5b..6cadbdc 100644 --- a/src/aki_prj23_transparenzregister/models/company.py +++ b/src/aki_prj23_transparenzregister/models/company.py @@ -2,29 +2,92 @@ from dataclasses import asdict, dataclass from enum import Enum +from aenum import MultiValueEnum -class RelationshipRoleEnum(Enum): - """_summary_. - Args: - Enum (_type_): _description_ - """ +class RelationshipRoleEnum(str, MultiValueEnum): + """Roles taken by entities in relationships to a Company.""" - STAKEHOLDER = "" ORGANISATION = "ORGANISATION" + KOMMANDITIST = "Kommanditist(in)", "Kommanditist" + GESCHAEFTSFUEHRER = "Geschäftsführer(in)", "Geschäftsführer" + PROKURIST = "Prokurist(in)", "Prokurist" + VORSTAND = "Vorstand" + INHABER = "Inhaber(in)", "Inhaber" + HAFTENDER_GESELLSCHAFTER = ( + "Persönlich haftende(r) Gesellschafter(in)", + "Persönlich haftender Gesellschafter", + ) + LIQUIDATOR = "Liquidator(in)", "Liquidator" + PARTNER = "Partner(in)", "Partner" + DIREKTOR = "Geschäftsführende(r) Direktor(in)", "Geschäftsführender Direktor" + LEITUNG = "Mitglied des Leitungsorgans" + VORSTANDSVORSITZENDER = "Vorstandsvorsitzende(r)", "Vorstandsvorsitzender" + NACHFOLGER = "Rechtsnachfolger" + STAENDIGER_VERTRETER = "Ständige(r) Vertreter(in)" + SONSTIGER_VERTRETER = "Sonstige(r) Vertreter(in)", "Sonstiger Vertreter" + GESCHAEFTSLEITER = "Geschäftsleiter(in)", "Geschäftsleiter" + ZWEIGNIEDERLASSUNG = "Zweigniederlassung" + HAUPTNIEDERLASSUNG = "Hauptniederlassung" + + +class CompanyTypeEnum(str, MultiValueEnum): + """Type of Company.""" + + GMBH = "Gesellschaft mit beschränkter Haftung" + SE = "Europäische Aktiengesellschaft (SE)" + KG = "Kommanditgesellschaft" + EINZELKAUFMANN = ( + "Einzelkaufmann", + "Einzelkauffrau", + "Einzelkaufmann / Einzelkauffrau", + ) + EG = "eingetragene Genossenschaft" + AG = "Aktiengesellschaft" + PARTNERSCHAFTSGESELLSCHAFT = "Partnerschaftsgesellschaft" + PARTNERGESELLSCHAFT = "Partnergesellschaft" + PARTNERSCHAFT = "Partnerschaft" + KGaA = "Kommanditgesellschaft auf Aktien" + OHG = "Offene Handelsgesellschaft" + AUSLAENDISCHE_RECHTSFORM = "Rechtsform ausländischen Rechts HRB" + JURISTISCHE_PERSON = "HRA Juristische Person" + + +@dataclass +class DistrictCourt: + """DistrictCourt.""" + + name: str + city: str + + def to_dict(self) -> dict: + """Transform to dict. + + Returns: + dict: Dictionary + """ + return asdict(self) @dataclass class CompanyID: - """_summary_.""" + """CompanyID.""" - district_court: str + district_court: DistrictCourt hr_number: str + def to_dict(self) -> dict: + """Transform to dict. + + Returns: + dict: Dictionary + """ + return asdict(self) + @dataclass class Location: - """_summary_.""" + """Location.""" city: str street: str | None = None @@ -32,12 +95,43 @@ class Location: zip_code: str | None = None +class CompanyRelationshipEnum(str, Enum): + """Type of companyrelations.""" + + PERSON = "Person" + COMPANY = "Company" + + @dataclass class CompanyRelationship: - """_summary_.""" + """Relation of a Company to a person or another company.""" role: RelationshipRoleEnum location: Location + type: CompanyRelationshipEnum # noqa: A003 + + +@dataclass +class PersonName: + """Combination of first and lastname as a class.""" + + firstname: str + lastname: str + + +@dataclass +class PersonToCompanyRelationship(CompanyRelationship): + """Extension of CompanyRelationship with extras for Person.""" + + name: PersonName + date_of_birth: str + + +@dataclass +class CompanyToCompanyRelationship(CompanyRelationship): + """Extension of CompanyRelationship with extras for Company.""" + + name: str class FinancialKPIEnum(Enum): @@ -85,10 +179,33 @@ class YearlyResult: kpis: dict[FinancialKPIEnum, float] +class CurrencyEnum(str, MultiValueEnum): + """Enum of possible currencies.""" + + EURO = "EUR" + DEUTSCHE_MARK = "DM", "DEM" + KEINE_ANGABE = "" + + +class CapitalTypeEnum(str, Enum): + """Enum of possible capital types.""" + + HAFTEINLAGE = "Hafteinlage" + STAMMKAPITAL = "Stammkapital" + GRUNDKAPITAL = "Grundkapital" + + +@dataclass +class Capital: + """Capital of company.""" + + value: float + currency: CurrencyEnum + type: CapitalTypeEnum # noqa: A003 + + @dataclass class Company: - """_summary_.""" - """Company dataclass.""" id: CompanyID @@ -96,8 +213,12 @@ class Company: name: str last_update: str relationships: list[CompanyRelationship] - # yearly_results: list[FinancialResults] + # yearly_results: list[FinancialResults]] | None + company_type: CompanyTypeEnum | None = None + capital: Capital | None = None + business_purpose: str | None = None + founding_date: str | None = None def to_dict(self) -> dict: - """_summary_.""" + """Transform class to dict.""" return asdict(self) diff --git a/src/aki_prj23_transparenzregister/utils/data_extraction/unternehmensregister/__init__.py b/src/aki_prj23_transparenzregister/utils/data_extraction/unternehmensregister/__init__.py new file mode 100644 index 0000000..9cc8256 --- /dev/null +++ b/src/aki_prj23_transparenzregister/utils/data_extraction/unternehmensregister/__init__.py @@ -0,0 +1 @@ +"""Everything regarding data extraction from the Unternehmensregister.""" diff --git a/Jupyter/API-tests/Unternehmensregister/main.py b/src/aki_prj23_transparenzregister/utils/data_extraction/unternehmensregister/extract.py similarity index 98% rename from Jupyter/API-tests/Unternehmensregister/main.py rename to src/aki_prj23_transparenzregister/utils/data_extraction/unternehmensregister/extract.py index 4d8e8c6..6fd3174 100644 --- a/Jupyter/API-tests/Unternehmensregister/main.py +++ b/src/aki_prj23_transparenzregister/utils/data_extraction/unternehmensregister/extract.py @@ -1,20 +1,18 @@ """Unternehmensregister Scraping.""" import glob -import logging import multiprocessing import os from pathlib import Path +from loguru import logger from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as ec from selenium.webdriver.support.ui import WebDriverWait from tqdm import tqdm -logger = logging.getLogger() - -def scrape(query: str, download_dir: list[str]): +def scrape(query: str, download_dir: list[str]) -> None: """Fetch results from Unternehmensregister for given query. Args: @@ -152,7 +150,7 @@ def get_num_files(path: str, pattern: str = "*.xml") -> int: return len(glob.glob1(path, pattern)) -def rename_latest_file(path: str, filename: str, pattern: str = "*.xml"): +def rename_latest_file(path: str, filename: str, pattern: str = "*.xml") -> None: """Rename file in dir with latest change date. Args: diff --git a/src/aki_prj23_transparenzregister/utils/data_extraction/unternehmensregister/load.py b/src/aki_prj23_transparenzregister/utils/data_extraction/unternehmensregister/load.py new file mode 100644 index 0000000..621b723 --- /dev/null +++ b/src/aki_prj23_transparenzregister/utils/data_extraction/unternehmensregister/load.py @@ -0,0 +1,30 @@ +"""Load processed Unternehmensregister data into MongoDB.""" +import glob +import json +import os + +from tqdm import tqdm + +from aki_prj23_transparenzregister.config.config_providers import JsonFileConfigProvider +from aki_prj23_transparenzregister.models.company import Company +from aki_prj23_transparenzregister.utils.mongo.company_mongo_service import ( + CompanyMongoService, +) +from aki_prj23_transparenzregister.utils.mongo.connector import ( + MongoConnector, +) + +if __name__ == "__main__": + provider = JsonFileConfigProvider("secrets.json") + conn_string = provider.get_mongo_connection_string() + connector = MongoConnector(conn_string) + service = CompanyMongoService(connector) + + base_path = "./Jupyter/API-tests/Unternehmensregister/data/Unternehmensregister" + for file in tqdm(glob.glob1(f"{base_path}/transformed", "*.json")): + path = os.path.join(f"{base_path}/transformed", file) + with open(path, encoding="utf-8") as file_object: + data = json.loads(file_object.read()) + company: Company = Company(**data) + + service.migrations_of_base_data(company) diff --git a/src/aki_prj23_transparenzregister/utils/data_extraction/unternehmensregister/transform.py b/src/aki_prj23_transparenzregister/utils/data_extraction/unternehmensregister/transform.py new file mode 100644 index 0000000..8129c28 --- /dev/null +++ b/src/aki_prj23_transparenzregister/utils/data_extraction/unternehmensregister/transform.py @@ -0,0 +1,481 @@ +"""Transform raw Unternehmensregister export (*.xml) to processed .json files for loading.""" +import dataclasses +import glob +import json +import os +import re +import sys + +import xmltodict +from tqdm import tqdm + +from aki_prj23_transparenzregister.models.company import ( + Capital, + CapitalTypeEnum, + Company, + CompanyID, + CompanyRelationship, + CompanyRelationshipEnum, + CompanyToCompanyRelationship, + CompanyTypeEnum, + CurrencyEnum, + DistrictCourt, + Location, + PersonName, + PersonToCompanyRelationship, + RelationshipRoleEnum, +) +from aki_prj23_transparenzregister.utils.string_tools import transform_date_to_iso + + +def transform_xml_to_json(source_dir: str, target_dir: str) -> None: + """Convert all xml files in a directory to json files. + + Args: + source_dir (str): Directory hosting the xml files + target_dir (str): Target directory to move json files to + """ + for source_path in [ + os.path.normpath(i) for i in glob.glob(source_dir + "**/*.xml", recursive=True) + ]: + target_path = os.path.join( + target_dir, source_path.split(os.sep)[-1].replace(".xml", ".json") + ) + + with open(source_path, encoding="utf-8") as source_file: + # deepcode ignore HandleUnicode: Weird XML format no other solution + data = xmltodict.parse(source_file.read().encode()) + with open(target_path, "w", encoding="utf-8") as json_file: + json_file.write(json.dumps(data)) + + +def parse_stakeholder(data: dict) -> CompanyRelationship | None: + """Extract the company stakeholder/relation from a single "Beteiligung". + + Args: + data (dict): Data export + + Returns: + CompanyRelationship | None: Relationship if it could be processed + """ + if "Natuerliche_Person" in data["Beteiligter"]: + # It's a Compnay serving as a "Kommanditist" or similar + if data["Beteiligter"]["Natuerliche_Person"]["Voller_Name"]["Vorname"] is None: + return CompanyToCompanyRelationship( + **{ + "name": data["Beteiligter"]["Natuerliche_Person"]["Voller_Name"][ + "Nachname" + ], + "location": Location( + **{ + "city": data["Beteiligter"]["Natuerliche_Person"][ + "Anschrift" + ][-1]["Ort"] + if isinstance( + data["Beteiligter"]["Natuerliche_Person"]["Anschrift"], + list, + ) + else data["Beteiligter"]["Natuerliche_Person"]["Anschrift"][ + "Ort" + ] + } + ), + "role": RelationshipRoleEnum( + data["Rolle"]["Rollenbezeichnung"]["content"] + ), + "type": CompanyRelationshipEnum.COMPANY, + } + ) + return PersonToCompanyRelationship( + **{ + "name": PersonName( + **{ + "firstname": data["Beteiligter"]["Natuerliche_Person"][ + "Voller_Name" + ]["Vorname"], + "lastname": data["Beteiligter"]["Natuerliche_Person"][ + "Voller_Name" + ]["Nachname"], + } + ), + "date_of_birth": data["Beteiligter"]["Natuerliche_Person"]["Geburt"][ + "Geburtsdatum" + ] + if "Geburt" in data["Beteiligter"]["Natuerliche_Person"] + else None, + "location": Location( + **{ + "city": data["Beteiligter"]["Natuerliche_Person"]["Anschrift"][ + -1 + ]["Ort"] + if isinstance( + data["Beteiligter"]["Natuerliche_Person"]["Anschrift"], list + ) + else data["Beteiligter"]["Natuerliche_Person"]["Anschrift"][ + "Ort" + ] + } + ), + "role": RelationshipRoleEnum( + data["Rolle"]["Rollenbezeichnung"]["content"] + ), + "type": CompanyRelationshipEnum.PERSON, + } + ) + if "Organisation" in data["Beteiligter"]: + return CompanyToCompanyRelationship( + **{ + "role": RelationshipRoleEnum( + data["Rolle"]["Rollenbezeichnung"]["content"] + ), + "name": data["Beteiligter"]["Organisation"]["Bezeichnung"][ + "Bezeichnung_Aktuell" + ], + "location": Location( + **{ + "city": data["Beteiligter"]["Organisation"]["Anschrift"]["Ort"], + "street": data["Beteiligter"]["Organisation"]["Anschrift"][ + "Strasse" + ] + if "Strasse" in data["Beteiligter"]["Organisation"]["Anschrift"] + else None, + "house_number": data["Beteiligter"]["Organisation"][ + "Anschrift" + ]["Hausnummer"] + if "Hausnummer" + in data["Beteiligter"]["Organisation"]["Anschrift"] + else None, + "zip_code": data["Beteiligter"]["Organisation"]["Anschrift"][ + "Postleitzahl" + ] + if "Postleitzahl" + in data["Beteiligter"]["Organisation"]["Anschrift"] + else None, + } + ), + "type": CompanyRelationshipEnum.COMPANY, + } + ) + return None + + +def loc_from_beteiligung(data: dict) -> Location: + """Extract the company location from the first relationship in the export. + + Args: + data (dict): Data export + + Returns: + Location: location + """ + return Location( + **{ + "city": data["XJustiz_Daten"]["Grunddaten"]["Verfahrensdaten"][ + "Beteiligung" + ][0]["Beteiligter"]["Organisation"]["Anschrift"]["Ort"], + "zip_code": data["XJustiz_Daten"]["Grunddaten"]["Verfahrensdaten"][ + "Beteiligung" + ][0]["Beteiligter"]["Organisation"]["Anschrift"]["Postleitzahl"], + "street": data["XJustiz_Daten"]["Grunddaten"]["Verfahrensdaten"][ + "Beteiligung" + ][0]["Beteiligter"]["Organisation"]["Anschrift"]["Strasse"] + if "Strasse" + in data["XJustiz_Daten"]["Grunddaten"]["Verfahrensdaten"]["Beteiligung"][0][ + "Beteiligter" + ]["Organisation"]["Anschrift"] + else None, + "house_number": data["XJustiz_Daten"]["Grunddaten"]["Verfahrensdaten"][ + "Beteiligung" + ][0]["Beteiligter"]["Organisation"]["Anschrift"]["Hausnummer"] + if "Hausnummer" + in data["XJustiz_Daten"]["Grunddaten"]["Verfahrensdaten"]["Beteiligung"][0][ + "Beteiligter" + ]["Organisation"]["Anschrift"] + else None, + } + ) + + +def name_from_beteiligung(data: dict) -> str: + """Extract the Company name from an Unternehmensregister export by using the first relationship found. + + Args: + data (dict): Data export + + Returns: + str: Company name + """ + return data["XJustiz_Daten"]["Grunddaten"]["Verfahrensdaten"]["Beteiligung"][0][ + "Beteiligter" + ]["Organisation"]["Bezeichnung"]["Bezeichnung_Aktuell"] + + +def map_rechtsform(company_name: str, data: dict) -> CompanyTypeEnum | None: + """Extracts the company type from a given Unternehmensregister export. + + Args: + company_name (str): Name of the company as a fallback solution + data (dict): Data export + + Returns: + CompanyTypeEnum | None: Company type if found + """ + try: + return CompanyTypeEnum( + data["XJustiz_Daten"]["Fachdaten_Register"]["Basisdaten_Register"][ + "Rechtstraeger" + ]["Rechtsform"]["content"] + ) + except KeyError: + if ( + company_name.endswith("GmbH") + or company_name.endswith("UG") + or company_name.endswith("UG (haftungsbeschränkt)") + ): + return CompanyTypeEnum("Gesellschaft mit beschränkter Haftung") + if company_name.endswith("SE"): + return CompanyTypeEnum("Europäische Aktiengesellschaft (SE)") + if company_name.endswith("KG"): + return CompanyTypeEnum("Kommanditgesellschaft") + return None + + +def map_capital(data: dict, company_type: CompanyTypeEnum) -> Capital | None: + """Extracts the company capital from the given Unternehmensregister export. + + Args: + data (dict): Data export + company_type (CompanyTypeEnum): Type of company (e.g., 'Gesellschaft mit beschränkter Haftung') + + Returns: + Capital | None: Company Capital if found + """ + # Early return + if "Zusatzangaben" not in data["XJustiz_Daten"]["Fachdaten_Register"]: + return None + capital: dict = {"Zahl": 0.0, "Waehrung": ""} + if company_type == CompanyTypeEnum.KG: + capital_type = "Hafteinlage" + base = data["XJustiz_Daten"]["Fachdaten_Register"]["Zusatzangaben"][ + "Personengesellschaft" + ]["Zusatz_KG"]["Daten_Kommanditist"] + if isinstance(base, list): + for entry in base: + # TODO link to persons using Ref_Rollennummer then extract ["Hafteinlage"] as below + capital["Zahl"] = capital["Zahl"] + float(entry["Hafteinlage"]["Zahl"]) + capital["Waehrung"] = entry["Hafteinlage"]["Waehrung"] + elif isinstance(base, dict): + capital = base["Hafteinlage"] + elif company_type in [ + CompanyTypeEnum.GMBH, + CompanyTypeEnum.SE, + CompanyTypeEnum.AG, + CompanyTypeEnum.KGaA, + CompanyTypeEnum.AUSLAENDISCHE_RECHTSFORM, + CompanyTypeEnum.OHG, + ]: + if ( + "Kapitalgesellschaft" + not in data["XJustiz_Daten"]["Fachdaten_Register"]["Zusatzangaben"] + ): + base = data["XJustiz_Daten"]["Fachdaten_Register"]["Zusatzangaben"][ + "Personengesellschaft" + ] + else: + base = data["XJustiz_Daten"]["Fachdaten_Register"]["Zusatzangaben"][ + "Kapitalgesellschaft" + ] + if "Zusatz_GmbH" in base: + capital_type = "Stammkapital" + capital = base["Zusatz_GmbH"]["Stammkapital"] + elif "Zusatz_Aktiengesellschaft" in base: + capital_type = "Grundkapital" + capital = base["Zusatz_Aktiengesellschaft"]["Grundkapital"]["Hoehe"] + elif company_type in [ + CompanyTypeEnum.EINZELKAUFMANN, + CompanyTypeEnum.EG, + CompanyTypeEnum.PARTNERSCHAFT, + CompanyTypeEnum.PARTNERGESELLSCHAFT, + CompanyTypeEnum.PARTNERSCHAFTSGESELLSCHAFT, + None, + ]: + return None + # Catch entries having the dict but with null values + if not all(capital.values()): + return None + return Capital( + **{ # type: ignore + "value": float(capital["Zahl"]), + "currency": CurrencyEnum(capital["Waehrung"]), + "type": CapitalTypeEnum(capital_type), + } + ) + + +def map_business_purpose(data: dict) -> str | None: + """Extracts the "Geschäftszweck" from a given Unternehmensregister export. + + Args: + data (dict): Data export + + Returns: + str | None: Business purpose if found + """ + try: + return data["XJustiz_Daten"]["Fachdaten_Register"]["Basisdaten_Register"][ + "Gegenstand_oder_Geschaeftszweck" + ] + except KeyError: + return None + + +def map_founding_date(data: dict) -> str | None: + """Extracts the founding date from a given Unternehmensregister export. + + Args: + data (dict): Data export + + Returns: + str | None: Founding date if found + """ + text = str(data) + entry_date = re.findall( + r".Tag der ersten Eintragung:(\\n| )?(\d{1,2}\.\d{1,2}\.\d{2,4})", text + ) + if len(entry_date) == 1: + return transform_date_to_iso(entry_date[0][1]) + + entry_date = re.findall( + r".Gesellschaftsvertrag vom (\d{1,2}\.\d{1,2}\.\d{2,4})", text + ) + if len(entry_date) == 1: + return transform_date_to_iso(entry_date[0]) + if ( + "Gruendungsmetadaten" + in data["XJustiz_Daten"]["Fachdaten_Register"]["Basisdaten_Register"] + ): + return data["XJustiz_Daten"]["Fachdaten_Register"]["Basisdaten_Register"][ + "Gruendungsmetadaten" + ]["Gruendungsdatum"] + # No reliable answer + return None + + +def map_company_id(data: dict) -> CompanyID: + """Retrieve Company ID from export. + + Args: + data (dict): Data export + + Returns: + CompanyID: ID of the company + """ + return CompanyID( + **{ + "hr_number": data["XJustiz_Daten"]["Grunddaten"]["Verfahrensdaten"][ + "Instanzdaten" + ]["Aktenzeichen"], + "district_court": DistrictCourt( + **{ + "name": data["XJustiz_Daten"]["Grunddaten"]["Verfahrensdaten"][ + "Beteiligung" + ][1]["Beteiligter"]["Organisation"]["Bezeichnung"][ + "Bezeichnung_Aktuell" + ] + if "Organisation" + in data["XJustiz_Daten"]["Grunddaten"]["Verfahrensdaten"][ + "Beteiligung" + ][1]["Beteiligter"] + else data["XJustiz_Daten"]["Grunddaten"]["Verfahrensdaten"][ + "Beteiligung" + ][1]["Beteiligter"]["Natuerliche_Person"]["Voller_Name"][ + "Nachname" + ], + "city": data["XJustiz_Daten"]["Grunddaten"]["Verfahrensdaten"][ + "Beteiligung" + ][1]["Beteiligter"]["Organisation"]["Sitz"]["Ort"] + if "Organisation" + in data["XJustiz_Daten"]["Grunddaten"]["Verfahrensdaten"][ + "Beteiligung" + ][1]["Beteiligter"] + else data["XJustiz_Daten"]["Grunddaten"]["Verfahrensdaten"][ + "Beteiligung" + ][1]["Beteiligter"]["Natuerliche_Person"]["Anschrift"]["Ort"], + } + ), + } + ) + + +def map_last_update(data: dict) -> str: + """Extract last update date from export. + + Args: + data (dict): Unternehmensregister export + + Returns: + str: Last update date + """ + return data["XJustiz_Daten"]["Fachdaten_Register"]["Auszug"]["letzte_Eintragung"] + + +def map_unternehmensregister_json(data: dict) -> Company: + """Processes the Unternehmensregister structured export to a Company by using several helper methods. + + Args: + data (dict): Data export + + Returns: + Company: Transformed data + """ + result: dict = {"relationships": []} + + # TODO Refactor mapping - this is a nightmare... + result["id"] = map_company_id(data) + result["name"] = name_from_beteiligung(data) + + result["location"] = loc_from_beteiligung(data) + result["last_update"] = map_last_update(data) + + result["company_type"] = map_rechtsform(result["name"], data) + result["capital"] = map_capital(data, result["company_type"]) + result["business_purpose"] = map_business_purpose(data) + result["founding_date"] = map_founding_date(data) + + for i in range( + 2, len(data["XJustiz_Daten"]["Grunddaten"]["Verfahrensdaten"]["Beteiligung"]) + ): + people = parse_stakeholder( + data["XJustiz_Daten"]["Grunddaten"]["Verfahrensdaten"]["Beteiligung"][i] + ) + result["relationships"].append(people) + return Company(**result) + + +if __name__ == "__main__": + from loguru import logger + + # transform_xml_to_json( + # "./data/Unternehmensregister/scraping/", "./data/Unternehmensregister/export/" + # ) + base_path = "./Jupyter/API-tests/Unternehmensregister/data/Unternehmensregister" + for file in tqdm(glob.glob1(f"{base_path}/export", "*.json")): + path = os.path.join(f"{base_path}/export", file) + with open(path, encoding="utf-8") as file_object: + try: + data = json.loads(file_object.read()) + company: Company = map_unternehmensregister_json(data) + + name = "".join(e for e in company.name if e.isalnum())[:50] + + with open( + f"{base_path}/transformed/{name}.json", + "w+", + encoding="utf-8", + ) as export_file: + json.dump( + dataclasses.asdict(company), export_file, ensure_ascii=False + ) + except Exception: + logger.error(f"Error in processing {path}") + sys.exit(1) diff --git a/src/aki_prj23_transparenzregister/utils/mongo/company_mongo_service.py b/src/aki_prj23_transparenzregister/utils/mongo/company_mongo_service.py index c2641bb..03354f4 100644 --- a/src/aki_prj23_transparenzregister/utils/mongo/company_mongo_service.py +++ b/src/aki_prj23_transparenzregister/utils/mongo/company_mongo_service.py @@ -9,10 +9,10 @@ from aki_prj23_transparenzregister.utils.mongo.connector import MongoConnector class CompanyMongoService: - """_summary_.""" + """Wrapper for MongoDB regarding management of Company documents.""" def __init__(self, connector: MongoConnector): - """_summary_. + """Constructor. Args: connector (MongoConnector): _description_ @@ -21,26 +21,40 @@ class CompanyMongoService: self.lock = Lock() # Create a lock for synchronization def get_all(self) -> list[Company]: - """_summary_. + """Get all Company documents. Returns: - list[Company]: _description_ + list[Company]: List of retrieved companies """ with self.lock: result = self.collection.find() return list(result) - def get_by_id(self, id: str) -> Company | None: - """_summary_. + def get_by_id(self, id: dict) -> dict | None: + """Get a Company document by the given id. Args: - id (str): _description_ + id (CompanyID): CompanyID Returns: - Company | None: _description_ + dict | None: Company if found """ with self.lock: - result = list(self.collection.find({"id": id})) + result = list( + self.collection.find( + { + "id": { + "$eq": { + "hr_number": id["hr_number"], + "district_court": { + "name": id["district_court"]["name"], + "city": id["district_court"]["city"], + }, + } + } + } + ) + ) if len(result) == 1: return result[0] return None @@ -81,7 +95,7 @@ class CompanyMongoService: return list(self.collection.find({"yearly_results": {"$gt": {}}})) def insert(self, company: Company) -> InsertOneResult: - """_summary_. + """Insert a new Company document. Args: company (Company): _description_ @@ -106,3 +120,21 @@ class CompanyMongoService: return self.collection.update_one( {"_id": ObjectId(_id)}, {"$set": {"yearly_results": yearly_results}} ) + + def migrations_of_base_data(self, data: Company) -> InsertOneResult | UpdateResult: + """Updates or inserts a document of type company depending on whether an entry with the same id (CompanyID) can be found. + + Args: + data (Company): Company related data to persist + + Returns: + InsertOneResult | UpdateResult: Result depending on action + """ + entry = self.get_by_id(data.id.to_dict()) + if entry is None: + return self.insert(data) + statement = {"$set": dict(data.to_dict().items())} + with self.lock: + return self.collection.update_one( + {"_id": ObjectId(entry["_id"])}, statement + ) diff --git a/src/aki_prj23_transparenzregister/utils/mongo/connector.py b/src/aki_prj23_transparenzregister/utils/mongo/connector.py index 0a84806..18763cf 100644 --- a/src/aki_prj23_transparenzregister/utils/mongo/connector.py +++ b/src/aki_prj23_transparenzregister/utils/mongo/connector.py @@ -6,7 +6,7 @@ import pymongo @dataclass class MongoConnection: - """_summary_.""" + """Wrapper for MongoDB connection string.""" hostname: str database: str @@ -36,7 +36,7 @@ class MongoConnector: """Wrapper for establishing a connection to a MongoDB instance.""" def __init__(self, connection: MongoConnection): - """_summary_. + """Wrapper for MongoDB collection. Args: connection (MongoConnection): Wrapper for connection string diff --git a/src/aki_prj23_transparenzregister/utils/mongo/news_mongo_service.py b/src/aki_prj23_transparenzregister/utils/mongo/news_mongo_service.py index 0857207..2bf80c3 100644 --- a/src/aki_prj23_transparenzregister/utils/mongo/news_mongo_service.py +++ b/src/aki_prj23_transparenzregister/utils/mongo/news_mongo_service.py @@ -6,14 +6,10 @@ from aki_prj23_transparenzregister.utils.mongo.connector import MongoConnector class MongoNewsService: - """_summary_. - - Args: - NewsServiceInterface (_type_): _description_ - """ + """Wrapper for MongoDB regarding News documents.""" def __init__(self, connector: MongoConnector): - """_summary_. + """Constructor. Args: connector (MongoConnector): _description_ @@ -21,7 +17,7 @@ class MongoNewsService: self.collection = connector.database["news"] def get_all(self) -> list[News]: - """_summary_. + """Get all News documents. Returns: list[News]: _description_ @@ -30,7 +26,7 @@ class MongoNewsService: return [MongoEntryTransformer.transform_outgoing(elem) for elem in result] def get_by_id(self, id: str) -> News | None: - """_summary_. + """Get a News document by the given id. Args: id (str): _description_ @@ -44,7 +40,7 @@ class MongoNewsService: return None def insert(self, news: News) -> InsertOneResult: - """_summary_. + """Insert a new News document. Args: news (News): _description_ @@ -56,11 +52,7 @@ class MongoNewsService: class MongoEntryTransformer: - """_summary_. - - Returns: - _type_: _description_ - """ + """Transform a dict to News entity and back.""" @staticmethod def transform_ingoing(news: News) -> dict: diff --git a/src/aki_prj23_transparenzregister/utils/string_tools.py b/src/aki_prj23_transparenzregister/utils/string_tools.py index be399f0..f56fbc7 100644 --- a/src/aki_prj23_transparenzregister/utils/string_tools.py +++ b/src/aki_prj23_transparenzregister/utils/string_tools.py @@ -1,4 +1,6 @@ """Contains functions fot string manipulation.""" +import re +from datetime import datetime def simplify_string(string_to_simplify: str | None) -> str | None: @@ -16,3 +18,19 @@ def simplify_string(string_to_simplify: str | None) -> str | None: else: raise TypeError("The string to simplify is not a string.") return string_to_simplify if string_to_simplify else None + + +def transform_date_to_iso(date: str) -> str: + """Transform a date in `DD.MM.YY(YY)` to `YYYY-MM-DD`. + + Args: + date (str): Input date + + Returns: + str: ISO date + """ + regex_yy = r"^\d{1,2}\.\d{1,2}\.\d{2}$" + + input_format = "%d.%m.%y" if re.match(regex_yy, date) else "%d.%m.%Y" + date_temp = datetime.strptime(date, input_format) + return date_temp.strftime("%Y-%m-%d") diff --git a/tests/models/company_test.py b/tests/models/company_test.py index a044492..9f70590 100644 --- a/tests/models/company_test.py +++ b/tests/models/company_test.py @@ -1,26 +1,43 @@ """Test Models.company.""" -from aki_prj23_transparenzregister.models.company import Company, CompanyID, Location +from aki_prj23_transparenzregister.models.company import ( + Capital, + CapitalTypeEnum, + Company, + CompanyID, + CompanyTypeEnum, + CurrencyEnum, + DistrictCourt, + Location, +) def test_to_dict() -> None: """Tests if the version tag is entered.""" - company_id = CompanyID("The Shire", "420") + district_court = DistrictCourt("abc", "abc") + company_id = CompanyID(district_court=district_court, hr_number="HRB 123") location = Location( city="Insmouth", house_number="19", street="Harbor", zip_code="1890" ) + capital = Capital( + currency=CurrencyEnum.DEUTSCHE_MARK, type=CapitalTypeEnum.GRUNDKAPITAL, value=42 # type: ignore + ) company = Company( id=company_id, last_update="Tomorrow", location=location, name="BLANK GmbH", relationships=[], + business_purpose="Blockchain and NFTs", + capital=capital, + company_type=CompanyTypeEnum.AG, # type: ignore + founding_date="Yesterday", ) assert company.to_dict() == { "id": { - "district_court": company_id.district_court, + "district_court": district_court.to_dict(), "hr_number": company_id.hr_number, }, "last_update": company.last_update, @@ -32,4 +49,12 @@ def test_to_dict() -> None: }, "name": "BLANK GmbH", "relationships": [], + "business_purpose": "Blockchain and NFTs", + "capital": { + "value": capital.value, + "currency": capital.currency, + "type": capital.type, + }, + "company_type": company.company_type, + "founding_date": "Yesterday", } diff --git a/tests/utils/data_extraction/unternehmensregister/extract_test.py b/tests/utils/data_extraction/unternehmensregister/extract_test.py new file mode 100644 index 0000000..14f3763 --- /dev/null +++ b/tests/utils/data_extraction/unternehmensregister/extract_test.py @@ -0,0 +1,89 @@ +"""Testing utisl/data_extraction/unternehmensregister/extract.py.""" +import os +from tempfile import TemporaryDirectory + +from aki_prj23_transparenzregister.utils.data_extraction.unternehmensregister import ( + extract, +) + + +def prepare_temporary_dir(directory: str, formats: list[str]) -> None: + for index in range(len(formats)): + test_file = os.path.join(directory, f"file-{index}.{formats[index]}") + with open(test_file, "w") as file: + file.write(f"Hello There {index}") + + +def test_rename_latest_file() -> None: + import time + + with TemporaryDirectory(dir="./") as temp_dir: + # Create some test files in the temporary directory + test_file1 = os.path.join(temp_dir, "file1.xml") + test_file2 = os.path.join(temp_dir, "file2.xml") + test_file3 = os.path.join(temp_dir, "file3.xml") + + # Create files with different modification times + with open(test_file1, "w") as f: + f.write("Content 1") + time.sleep(0.15) + with open(test_file2, "w") as f: + f.write("Content 2") + time.sleep(0.15) + with open(test_file3, "w") as f: + f.write("Content 3") + time.sleep(0.15) + + # Rename the latest file to 'new_file.xml' + extract.rename_latest_file(temp_dir, "new_file.xml") + # Verify that 'file3.xml' is renamed to 'new_file.xml' + assert not os.path.exists(test_file3) + assert os.path.exists(os.path.join(temp_dir, "new_file.xml")) + + # Verify that 'file1.xml' and 'file2.xml' are still present + assert os.path.exists(test_file1) + assert os.path.exists(test_file2) + + # Verify that renaming with a different pattern works + with open(test_file1, "w") as f: + f.write("Content 4") + with open(os.path.join(temp_dir, "file4.txt"), "w") as f: + f.write("Content 5") + + # Rename the latest .txt file to 'new_file.txt' + extract.rename_latest_file(temp_dir, "new_file.txt", pattern="*.txt") + + # Verify that 'file4.txt' is renamed to 'new_file.txt' + assert not os.path.exists(os.path.join(temp_dir, "file4.txt")) + assert os.path.exists(os.path.join(temp_dir, "new_file.txt")) + + # Verify that 'file1.xml' is still present and unchanged + with open(test_file1) as f: + assert f.read() == "Content 4" + + +def test_get_num_files_default_pattern() -> None: + with TemporaryDirectory(dir="./") as temp_dir: + prepare_temporary_dir(temp_dir, ["xml", "xml", "xml"]) + + expected_result = 3 + assert extract.get_num_files(temp_dir) == expected_result + + +def test_get_num_files_different_pattern() -> None: + with TemporaryDirectory(dir="./") as temp_dir: + prepare_temporary_dir(temp_dir, ["xml", "txt", "json"]) + + num_files = extract.get_num_files(temp_dir, "*.txt") + assert num_files == 1 + + +def test_wait_for_download_condition() -> None: + with TemporaryDirectory(dir="./") as temp_dir: + prepare_temporary_dir(temp_dir, ["xml", "txt"]) + assert extract.wait_for_download_condition(temp_dir, 2) is False + + +def test_scrape() -> None: + with TemporaryDirectory(dir="./") as temp_dir: + extract.scrape("GEA Farm Technologies GmbH", [temp_dir]) diff --git a/tests/utils/data_extraction/unternehmensregister/load_test.py b/tests/utils/data_extraction/unternehmensregister/load_test.py new file mode 100644 index 0000000..6f6b58b --- /dev/null +++ b/tests/utils/data_extraction/unternehmensregister/load_test.py @@ -0,0 +1,8 @@ +"""Test load utils from Unternehmensregister.""" +from aki_prj23_transparenzregister.utils.data_extraction.unternehmensregister import ( + load, +) + + +def test_smoke() -> None: + assert load diff --git a/tests/utils/data_extraction/unternehmensregister/transform_test.py b/tests/utils/data_extraction/unternehmensregister/transform_test.py new file mode 100644 index 0000000..e1a68ec --- /dev/null +++ b/tests/utils/data_extraction/unternehmensregister/transform_test.py @@ -0,0 +1,592 @@ +"""Testing utils/data_extraction/unternehmensregister/transform.py.""" +import json +import os +from tempfile import TemporaryDirectory +from unittest.mock import Mock, patch + +from aki_prj23_transparenzregister.models.company import ( + Capital, + CapitalTypeEnum, + Company, + CompanyID, + CompanyRelationshipEnum, + CompanyToCompanyRelationship, + CompanyTypeEnum, + CurrencyEnum, + DistrictCourt, + Location, + PersonName, + PersonToCompanyRelationship, + RelationshipRoleEnum, +) +from aki_prj23_transparenzregister.utils.data_extraction.unternehmensregister import ( + transform, +) + + +def test_transform_xml_to_json() -> None: + with TemporaryDirectory(dir="./") as temp_source_dir: + with open(os.path.join(temp_source_dir, "test.xml"), "w") as file: + xml_input = """ + + Hello World! + + """ + file.write(xml_input) + with TemporaryDirectory(dir="./") as temp_target_dir: + transform.transform_xml_to_json(temp_source_dir, temp_target_dir) + with open(os.path.join(temp_target_dir, "test.json")) as file: + json_output = json.load(file) + assert json_output == {"test": {"message": "Hello World!"}} + + +def test_parse_stakeholder_org_hidden_in_person() -> None: + data = { + "Beteiligter": { + "Natuerliche_Person": { + "Voller_Name": {"Vorname": None, "Nachname": "Some Company KG"}, + "Anschrift": {"Ort": "Area 51"}, + } + }, + "Rolle": {"Rollenbezeichnung": {"content": "Kommanditist(in)"}}, + } + expected_result = CompanyToCompanyRelationship( + role=RelationshipRoleEnum.KOMMANDITIST, # type: ignore + name="Some Company KG", + type=CompanyRelationshipEnum.COMPANY, + location=Location(**{"city": "Area 51"}), + ) + assert transform.parse_stakeholder(data) == expected_result + + +def test_parse_stakeholder_person() -> None: + data = { + "Beteiligter": { + "Natuerliche_Person": { + "Voller_Name": {"Vorname": "Stephen", "Nachname": "King"}, + "Anschrift": {"Ort": "Maine"}, + "Geburt": {"Geburtsdatum": "1947-09-21"}, + } + }, + "Rolle": {"Rollenbezeichnung": {"content": "Geschäftsleiter(in)"}}, + } + expected_result = PersonToCompanyRelationship( + role=RelationshipRoleEnum.GESCHAEFTSLEITER, # type: ignore + date_of_birth="1947-09-21", + name=PersonName(**{"firstname": "Stephen", "lastname": "King"}), + type=CompanyRelationshipEnum.PERSON, + location=Location(**{"city": "Maine"}), + ) + assert transform.parse_stakeholder(data) == expected_result + + +def test_parse_stakeholder_org() -> None: + data = { + "Beteiligter": { + "Organisation": { + "Bezeichnung": {"Bezeichnung_Aktuell": "Transparenzregister kG"}, + "Anschrift": { + "Ort": "Iserlohn", + "Strasse": "Hauptstrasse", + "Hausnummer": "42", + "Postleitzahl": "58636", + }, + "Geburt": {"Geburtsdatum": "1947-09-21"}, + } + }, + "Rolle": {"Rollenbezeichnung": {"content": "Geschäftsführender Direktor"}}, + } + expected_result = CompanyToCompanyRelationship( + name="Transparenzregister kG", + role=RelationshipRoleEnum.DIREKTOR, # type: ignore + type=CompanyRelationshipEnum.COMPANY, + location=Location( + **{ + "city": "Iserlohn", + "zip_code": "58636", + "house_number": "42", + "street": "Hauptstrasse", + } + ), + ) + assert transform.parse_stakeholder(data) == expected_result + + +def test_parse_stakeholder_no_result() -> None: + data: dict = {"Beteiligter": {}} + assert transform.parse_stakeholder(data) is None + + +def test_loc_from_beteiligung() -> None: + data = { + "XJustiz_Daten": { + "Grunddaten": { + "Verfahrensdaten": { + "Beteiligung": [ + { + "Beteiligter": { + "Beteiligtennummer": "1", + "Organisation": { + "Bezeichnung": { + "Bezeichnung_Aktuell": "1 A Autenrieth Kunststofftechnik GmbH & Co. KG" + }, + "Sitz": { + "Ort": "Heroldstatt", + "Staat": { + "@xsi:type": "WL_Staaten", + "@wl_version": "1.5", + "@wl_fassung": "2", + "content": "DE", + }, + }, + "Anschrift": { + "Strasse": "Gewerbestraße", + "Hausnummer": "8", + "Postleitzahl": "72535", + "Ort": "Heroldstatt", + }, + }, + } + }, + ] + } + } + } + } + + expected_result = Location( + city="Heroldstatt", house_number="8", street="Gewerbestraße", zip_code="72535" + ) + assert transform.loc_from_beteiligung(data) == expected_result + + +def test_name_from_beteiligung() -> None: + data = { + "XJustiz_Daten": { + "Grunddaten": { + "Verfahrensdaten": { + "Beteiligung": [ + { + "Beteiligter": { + "Beteiligtennummer": "1", + "Organisation": { + "Bezeichnung": { + "Bezeichnung_Aktuell": "1 A Autenrieth Kunststofftechnik GmbH & Co. KG" + }, + }, + } + }, + ] + } + } + } + } + + expected_result = "1 A Autenrieth Kunststofftechnik GmbH & Co. KG" + assert transform.name_from_beteiligung(data) == expected_result + + +def test_map_rechtsform() -> None: + data = { + "XJustiz_Daten": { + "Fachdaten_Register": { + "Basisdaten_Register": { + "Aktuelles_Satzungsdatum": "1952-07-15", + "Rechtstraeger": { + "Rechtsform": { + "content": "Gesellschaft mit beschränkter Haftung" + }, + }, + } + } + } + } + expected_result = "Gesellschaft mit beschränkter Haftung" + assert transform.map_rechtsform("", data) == expected_result + + +def test_map_rechtsform_from_name() -> None: + data = [ + ("GEA Farm Technologies GmbH", "Gesellschaft mit beschränkter Haftung"), + ("Atos SE", "Europäische Aktiengesellschaft (SE)"), + ("Bilkenroth KG", "Kommanditgesellschaft"), + ("jfoiahfo8sah 98548902 öhz ö", None), + ] + + for company_name, expected_result in data: + assert transform.map_rechtsform(company_name, {}) == expected_result + + +def test_map_capital_kg_single() -> None: + capital = Capital( + currency=CurrencyEnum.EURO, value=69000, type=CapitalTypeEnum.HAFTEINLAGE # type: ignore + ) + data = { + "XJustiz_Daten": { + "Fachdaten_Register": { + "Zusatzangaben": { + "Personengesellschaft": { + "Zusatz_KG": { + "Daten_Kommanditist": { + "Hafteinlage": { + "Zahl": str(capital.value), + "Waehrung": capital.currency, + }, + } + } + } + } + } + } + } + + result = transform.map_capital(data, CompanyTypeEnum.KG) # type: ignore + assert result == capital + + +def test_map_capital_kg_sum() -> None: + capital = Capital( + currency=CurrencyEnum.EURO, value=20000, type=CapitalTypeEnum.HAFTEINLAGE # type: ignore + ) + data = { + "XJustiz_Daten": { + "Fachdaten_Register": { + "Zusatzangaben": { + "Personengesellschaft": { + "Zusatz_KG": { + "Daten_Kommanditist": [ + { + "Hafteinlage": { + "Zahl": str(10000), + "Waehrung": capital.currency, + } + }, + { + "Hafteinlage": { + "Zahl": str(10000), + "Waehrung": capital.currency, + }, + }, + ] + } + } + } + } + } + } + + result = transform.map_capital(data, CompanyTypeEnum.KG) # type: ignore + assert result == capital + + +def test_map_capital_no_fachdaten() -> None: + data: dict = {"XJustiz_Daten": {"Fachdaten_Register": {}}} + + result = transform.map_capital(data, CompanyTypeEnum.KG) # type: ignore + assert result is None + + +def test_map_capital_gmbh() -> None: + capital = Capital( + currency=CurrencyEnum.DEUTSCHE_MARK, value=42, type=CapitalTypeEnum.STAMMKAPITAL # type: ignore + ) + data = { + "XJustiz_Daten": { + "Fachdaten_Register": { + "Zusatzangaben": { + "Kapitalgesellschaft": { + "Zusatz_GmbH": { + "Stammkapital": { + "Zahl": str(capital.value), + "Waehrung": capital.currency, + }, + } + } + } + } + } + } + + result = transform.map_capital(data, CompanyTypeEnum.GMBH) # type: ignore + assert result == capital + + +def test_map_capital_ag() -> None: + capital = Capital( + currency=CurrencyEnum.DEUTSCHE_MARK, value=42, type=CapitalTypeEnum.GRUNDKAPITAL # type: ignore + ) + data = { + "XJustiz_Daten": { + "Fachdaten_Register": { + "Zusatzangaben": { + "Kapitalgesellschaft": { + "Zusatz_Aktiengesellschaft": { + "Grundkapital": { + "Hoehe": { + "Zahl": str(capital.value), + "Waehrung": capital.currency, + } + }, + } + } + } + } + } + } + + result = transform.map_capital(data, CompanyTypeEnum.SE) # type: ignore + assert result == capital + + +def test_map_capital_personengesellschaft() -> None: + capital = Capital( + currency=CurrencyEnum.DEUTSCHE_MARK, value=42, type=CapitalTypeEnum.STAMMKAPITAL # type: ignore + ) + data = { + "XJustiz_Daten": { + "Fachdaten_Register": { + "Zusatzangaben": { + "Personengesellschaft": { + "Zusatz_GmbH": { + "Stammkapital": { + "Zahl": str(capital.value), + "Waehrung": capital.currency, + }, + } + } + } + } + } + } + + result = transform.map_capital(data, CompanyTypeEnum.OHG) # type: ignore + assert result == capital + + +def test_map_capital_einzelkaufmann() -> None: + capital = Capital( + currency=CurrencyEnum.DEUTSCHE_MARK, value=42, type=CapitalTypeEnum.STAMMKAPITAL # type: ignore + ) + data = { + "XJustiz_Daten": { + "Fachdaten_Register": { + "Zusatzangaben": { + "Personengesellschaft": { + "Zusatz_GmbH": { + "Stammkapital": { + "Zahl": str(capital.value), + "Waehrung": capital.currency, + }, + } + } + } + } + } + } + + result = transform.map_capital(data, CompanyTypeEnum.EINZELKAUFMANN) # type: ignore + assert result is None + + +def test_map_capital_partial_null_values() -> None: + capital = Capital( + currency=CurrencyEnum.DEUTSCHE_MARK, value=42, type=CapitalTypeEnum.STAMMKAPITAL # type: ignore + ) + data = { + "XJustiz_Daten": { + "Fachdaten_Register": { + "Zusatzangaben": { + "Personengesellschaft": { + "Zusatz_GmbH": { + "Stammkapital": { + "Zahl": None, + "Waehrung": capital.currency, + }, + } + } + } + } + } + } + + result = transform.map_capital(data, CompanyTypeEnum.OHG) # type: ignore + assert result is None + + +def test_map_business_purpose() -> None: + business_purpose = "Handel mit Betäubungsmitteln aller Art" + data = { + "XJustiz_Daten": { + "Fachdaten_Register": { + "Basisdaten_Register": { + "Gegenstand_oder_Geschaeftszweck": business_purpose + } + } + } + } + + result = transform.map_business_purpose(data) + assert result == business_purpose + + +def test_map_business_purpose_no_result() -> None: + data: dict = {"XJustiz_Daten": {}} + + result = transform.map_business_purpose(data) + assert result is None + + +def test_map_founding_date_from_tag_der_ersten_eintragung() -> None: + data = { + "some entry": "Tag der ersten Eintragung: 01.05.2004", + "some other entry": "hfjdoöiashföahöf iodsazo8 5z4o fdsha8oü gfdsö", + } + expected_result = "2004-05-01" + result = transform.map_founding_date(data) + assert result == expected_result + + +def test_map_founding_date_from_gesellschaftsvertrag() -> None: + data = { + "some entry": "hfjdoöiashföahöf iodsazo8 5z4o fdsha8oü gfdsö", + "some other entry": "Das Wesen der Rekursion ist der Selbstaufruf Gesellschaftsvertrag vom 22.12.1996 Hallo Welt", + } + expected_result = "1996-12-22" + result = transform.map_founding_date(data) + assert result == expected_result + + +def test_map_founding_date_from_gruendungsdatum() -> None: + data = { + "XJustiz_Daten": { + "Fachdaten_Register": { + "Basisdaten_Register": { + "Gruendungsmetadaten": {"Gruendungsdatum": "1998-01-01"} + } + } + } + } + expected_result = "1998-01-01" + result = transform.map_founding_date(data) + assert result == expected_result + + +def test_map_founding_date_no_result() -> None: + data: dict = {"XJustiz_Daten": {"Fachdaten_Register": {"Basisdaten_Register": {}}}} + result = transform.map_founding_date(data) + assert result is None + + +def test_map_company_id() -> None: + district_court = DistrictCourt("Amtsgericht Ulm", "Ulm") + company_id = CompanyID(district_court, "HRA 4711") + data = { + "XJustiz_Daten": { + "Grunddaten": { + "@XJustizVersion": "1.20.0", + "Verfahrensdaten": { + "Instanzdaten": { + "Aktenzeichen": company_id.hr_number, + }, + "Beteiligung": [ + {}, + { + "Beteiligter": { + "Organisation": { + "Bezeichnung": { + "Bezeichnung_Aktuell": district_court.name + }, + "Sitz": { + "Ort": district_court.city, + }, + } + }, + }, + ], + }, + }, + } + } + result = transform.map_company_id(data) + assert result == company_id + + +def test_map_last_update() -> None: + date = "2024-01-01" + data = { + "XJustiz_Daten": {"Fachdaten_Register": {"Auszug": {"letzte_Eintragung": date}}} + } + result = transform.map_last_update(data) + assert result == date + + +@patch( + "aki_prj23_transparenzregister.utils.data_extraction.unternehmensregister.transform.map_company_id" +) +@patch( + "aki_prj23_transparenzregister.utils.data_extraction.unternehmensregister.transform.name_from_beteiligung" +) +@patch( + "aki_prj23_transparenzregister.utils.data_extraction.unternehmensregister.transform.loc_from_beteiligung" +) +@patch( + "aki_prj23_transparenzregister.utils.data_extraction.unternehmensregister.transform.map_last_update" +) +@patch( + "aki_prj23_transparenzregister.utils.data_extraction.unternehmensregister.transform.map_rechtsform" +) +@patch( + "aki_prj23_transparenzregister.utils.data_extraction.unternehmensregister.transform.map_capital" +) +@patch( + "aki_prj23_transparenzregister.utils.data_extraction.unternehmensregister.transform.map_business_purpose" +) +@patch( + "aki_prj23_transparenzregister.utils.data_extraction.unternehmensregister.transform.map_founding_date" +) +@patch( + "aki_prj23_transparenzregister.utils.data_extraction.unternehmensregister.transform.parse_stakeholder" +) +def test_map_unternehmensregister_json( # noqa: PLR0913 + mock_map_parse_stakeholder: Mock, + mock_map_founding_date: Mock, + mock_map_business_purpose: Mock, + mock_map_capital: Mock, + mock_map_rechtsform: Mock, + mock_map_last_update: Mock, + mock_loc_from_beteiligung: Mock, + mock_map_name_from_beteiligung: Mock, + mock_map_company_id: Mock, +) -> None: + expected_result = Company( + **{ # type: ignore + "id": Mock(), + "name": Mock(), + "location": Mock(), + "last_update": Mock(), + "company_type": Mock(), + "capital": Mock(), + "business_purpose": Mock(), + "founding_date": Mock(), + "relationships": [Mock()], + } + ) + + mock_map_company_id.return_value = expected_result.id + mock_map_name_from_beteiligung.return_value = expected_result.name + mock_loc_from_beteiligung.return_value = expected_result.location + mock_map_last_update.return_value = expected_result.last_update + mock_map_rechtsform.return_value = expected_result.company_type + mock_map_capital.return_value = expected_result.capital + mock_map_business_purpose.return_value = expected_result.business_purpose + mock_map_founding_date.return_value = expected_result.founding_date + mock_map_parse_stakeholder.return_value = expected_result.relationships[0] + + data: dict = { + "XJustiz_Daten": { + "Grunddaten": {"Verfahrensdaten": {"Beteiligung": [{}, {}, {}]}} + } + } + + result = transform.map_unternehmensregister_json(data) + assert result == expected_result diff --git a/tests/utils/mongo/company_mongo_service_test.py b/tests/utils/mongo/company_mongo_service_test.py index aa75949..7b99a07 100644 --- a/tests/utils/mongo/company_mongo_service_test.py +++ b/tests/utils/mongo/company_mongo_service_test.py @@ -3,7 +3,12 @@ from unittest.mock import Mock import pytest -from aki_prj23_transparenzregister.models.company import Company, CompanyID, Location +from aki_prj23_transparenzregister.models.company import ( + Company, + CompanyID, + DistrictCourt, + Location, +) from aki_prj23_transparenzregister.utils.mongo.company_mongo_service import ( CompanyMongoService, ) @@ -73,7 +78,8 @@ def test_by_id_no_result(mock_mongo_connector: Mock, mock_collection: Mock) -> N mock_mongo_connector.database = {"companies": mock_collection} service = CompanyMongoService(mock_mongo_connector) mock_collection.find.return_value = [] - assert service.get_by_id("Does not exist") is None + id = CompanyID(DistrictCourt("a", "b"), "c").to_dict() + assert service.get_by_id(id) is None def test_by_id_result(mock_mongo_connector: Mock, mock_collection: Mock) -> None: @@ -81,13 +87,14 @@ def test_by_id_result(mock_mongo_connector: Mock, mock_collection: Mock) -> None Args: mock_mongo_connector (Mock): Mocked MongoConnector library - mock_collection (Mock): Mocked pymongo collection + mock_collection (Mock): Mocked pymongo collection. """ mock_mongo_connector.database = {"companies": mock_collection} service = CompanyMongoService(mock_mongo_connector) mock_entry = {"id": "Does exist", "vaue": 42} mock_collection.find.return_value = [mock_entry] - assert service.get_by_id("Does exist") == mock_entry + id = CompanyID(DistrictCourt("a", "b"), "c").to_dict() + assert service.get_by_id(id) == mock_entry def test_insert(mock_mongo_connector: Mock, mock_collection: Mock) -> None: @@ -103,7 +110,7 @@ def test_insert(mock_mongo_connector: Mock, mock_collection: Mock) -> None: mock_collection.insert_one.return_value = mock_result assert ( service.insert( - Company(CompanyID("", ""), Location("Hier und Dort"), "", "", []) + Company(CompanyID("", ""), Location("Hier und Dort"), "", "", []) # type: ignore ) == mock_result ) diff --git a/tests/utils/string_tools_test.py b/tests/utils/string_tools_test.py index 26a7b1b..e19b488 100644 --- a/tests/utils/string_tools_test.py +++ b/tests/utils/string_tools_test.py @@ -33,3 +33,15 @@ def test_simplify_string_type_error(value: Any) -> None: """Tests if the type error is thrown when the value is the wrong type.""" with pytest.raises(TypeError): assert string_tools.simplify_string(value) + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("10.10.1111", "1111-10-10"), + ("10.10.98", "1998-10-10"), + ], +) +def test_transform_date_to_iso(value: str, expected: str) -> None: + result = string_tools.transform_date_to_iso(value) + assert result == expected