3
\$\begingroup\$

I reverse engineered the API from adac.de and wrote a client to query it for traffic news (constructions sites, traffic jams etc.). Here 's what I came up with:

#! /usr/bin/env python3 """ADAC traffic news API Client.""" from argparse import ArgumentParser, Namespace from functools import cache from hashlib import md5 from json import dumps from typing import Any from requests import post __all__ = ['get_traffic_news'] URL = 'https://www.adac.de/bff' QUERY = '''query TrafficNews($filter: TrafficNewsFilterInput!) { trafficNews(filter: $filter) { ...TrafficNewsItems __typename } } fragment TrafficNewsItems on TrafficNews { size items { ...TrafficNewsItem __typename } __typename } fragment TrafficNewsItem on TrafficNewsItem { id type details street timeLoss streetSign { streetNumber country __typename } headline { __typename ...TrafficNewsDirectionHeadline ...TrafficNewsNonDirectionHeadline } __typename } fragment TrafficNewsDirectionHeadline on TrafficNewsDirectionHeadline { from to __typename } fragment TrafficNewsNonDirectionHeadline on TrafficNewsNonDirectionHeadline { text __typename } ''' @cache def md5hash(string: str) -> str: """Hashes the given string and return the hex digest.""" return md5(string.encode()).hexdigest() def get_headers(query: dict[str, Any]) -> dict[str, str]: """Returns the headers for the request.""" return { 'content-type': 'application/json', # We need to provide a hash to distinguish queries with different # parameters from each other. Otherwise the API will return the result # of last query regardless of the sent parameters. 'x-graphql-query-hash': md5hash(dumps(query)) } def news_query(state: str, *, country: str = 'D', street: str = '', construction_sites: bool = False, traffic_news: bool = True, page_number: int = 1) -> dict[str, str]: """Returns a traffic news query.""" return { 'operationName': 'TrafficNews', 'variables': { 'filter': { 'country': { 'country': country, 'federalState': state, 'street': street, 'showConstructionSites': construction_sites, 'showTrafficNews': traffic_news, 'pageNumber': page_number } } }, 'query': QUERY } def get_traffic_news( state: str, *, country: str = 'D', street: str = '', construction_sites: bool = False, traffic_news: bool = True, page_number: int = 1) -> dict[str, Any]: """Returns a traffic news dict.""" query = news_query( state, country=country, street=street, traffic_news=traffic_news, construction_sites=construction_sites, page_number=page_number ) return post(URL, json=query, headers=get_headers(query)).json() def get_args(*, description: str = __doc__) -> Namespace: """Return the parsed command line arguments.""" parser = ArgumentParser(description=description) parser.add_argument('state') parser.add_argument('-C', '--country', metavar='country', default='D') parser.add_argument('-s', '--street', metavar='street') parser.add_argument('-n', '--no-traffic-news', action='store_true') parser.add_argument('-c', '--construction-sites', action='store_true') parser.add_argument('-p', '--page', type=int, metavar='n', default=1) return parser.parse_args() def main() -> None: """Runs the script.""" args = get_args() json = get_traffic_news( args.state, country=args.country, street=args.street, traffic_news=not args.no_traffic_news, page_number=args.page, construction_sites=args.construction_sites ) print(dumps(json, indent=2)) if __name__ == '__main__': main() 

Any feedback is welcome.

\$\endgroup\$
0

2 Answers 2

4
\$\begingroup\$

Caching your MD5 hash is premature optimisation, and indeed there are more important things you should be caring about. For instance, you're serialising JSON twice; instead you should use a Requests prepared request that does the serialisation once. Even this is insignificant in comparison to the time in flight on the network.

I don't know that __all__ is all that important to define for something that isn't a module. If you made a proper module with an __init__.py and __main__.py perhaps that would change.

QUERY can be condensed somewhat while still staying legible, since whitespace is insignificant in GraphQL.

Don't set content-type - Requests does that for you when it sees the json kwarg.

The number of parameters on news_query is bordering on needing a class instance for convenience. A named tuple will be lightweight for this purpose.

It's marginally less common to differentiate command-line switches by capitalisation and more common to just choose a different letter from the long-form argument name.

Add some help for arguments that need it, particularly country.

I don't know why you've set state as a mandatory parameter, because it doesn't seem that way in the API.

Either as an alternative, or as a straight-up replacement, your command-line program should be outputting results in human-friendly, localised text rather than machine-friendly JSON. I'm on the fence as to whether the command-line arguments should also be localised to German; for now I've left them as English but (perhaps incongruously) shown the result headers in German.

Remove the space on the inside of your shebang line.

Remove your redundant metavar declarations.

Add first-class support for a Requests Session, even if you only use it once here. It will make writing a library easier, if that ever happens.

Page number is an internal API implementation detail and should not be exposed to the user. Instead, offer them an optional max-items count, and depaginate.

Note that you're not forming your hash the way this site does. This site hashes the result of the following expression in https://www.adac.de/assets/ui/client....js:

JSON.stringify({ operationName: i, variables: a, query: o, uri: c, environment: e, previewMode: n, noCacheValue: l }) 

which produces (after reformatting)

{ "operationName": "StreetSuggestions", "variables": { "filter": "A12", "country": "D", "type": "Highway" }, "query": { "kind": "Document", "definitions": [ { "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "StreetSuggestions" }, "variableDefinitions": [ { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "filter" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } }, "directives": [] }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "country" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } }, "directives": [] }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "type" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "StreetType" } } }, "directives": [] } ], "directives": [], "selectionSet": { "kind": "SelectionSet", "selections": [ { "kind": "Field", "name": { "kind": "Name", "value": "streets" }, "arguments": [ { "kind": "Argument", "name": { "kind": "Name", "value": "filter" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "filter" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "country" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "country" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "type" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "type" } } } ], "directives": [], "selectionSet": { "kind": "SelectionSet", "selections": [ { "kind": "FragmentSpread", "name": { "kind": "Name", "value": "StreetSuggestion" }, "directives": [] }, { "kind": "Field", "name": { "kind": "Name", "value": "__typename" } } ] } } ] } }, { "kind": "FragmentDefinition", "name": { "kind": "Name", "value": "StreetSuggestion" }, "typeCondition": { "kind": "NamedType", "name": { "kind": "Name", "value": "Street" } }, "directives": [], "selectionSet": { "kind": "SelectionSet", "selections": [ { "kind": "Field", "name": { "kind": "Name", "value": "name" }, "arguments": [], "directives": [] }, { "kind": "Field", "name": { "kind": "Name", "value": "type" }, "arguments": [], "directives": [] }, { "kind": "Field", "name": { "kind": "Name", "value": "country" }, "arguments": [], "directives": [] }, { "kind": "Field", "name": { "kind": "Name", "value": "__typename" } } ] } } ], "loc": { "start": 0, "end": 264 } }, "environment": "prod", "previewMode": false } 

This might not matter.

Suggested

#!/usr/bin/env python3 """ADAC traffic news API Client.""" from argparse import ArgumentParser, Namespace from hashlib import md5 from itertools import count, islice from sys import stdout from typing import Any, NamedTuple, Optional, Iterator, TextIO from requests import Session, Request GRAPHQL = '''query TrafficNews($filter: TrafficNewsFilterInput!) { trafficNews(filter: $filter) {...TrafficNewsItems} } fragment TrafficNewsItems on TrafficNews { size items {...TrafficNewsItem} } fragment TrafficNewsItem on TrafficNewsItem { id type details street timeLoss streetSign {streetNumber country} headline { ...TrafficNewsDirectionHeadline ...TrafficNewsNonDirectionHeadline } } fragment TrafficNewsDirectionHeadline on TrafficNewsDirectionHeadline { from to } fragment TrafficNewsNonDirectionHeadline on TrafficNewsNonDirectionHeadline { text } ''' class NewsRequest(NamedTuple): country: str = 'D' state: str = '' street: str = '' construction_sites: bool = False traffic_news: bool = True @classmethod def from_args(cls, args: Namespace) -> 'NewsRequest': return cls( country=args.country, state=args.state, street=args.street, construction_sites=args.construction_sites, ) def query(self, page: int) -> dict[str, Any]: return { 'operationName': 'TrafficNews', 'variables': { 'filter': { 'country': { 'country': self.country, 'federalState': self.state, 'street': self.street, 'showConstructionSites': self.construction_sites, 'showTrafficNews': self.traffic_news, 'pageNumber': page, } } }, 'query': GRAPHQL, } class NewsResponse(NamedTuple): id: int type: str country: Optional[str] street: str street_number: Optional[str] headline: Optional[str] details: str @classmethod def from_json(cls, json: dict[str, Any]) -> 'NewsResponse': street_info = json.get('streetSign') or {} return cls( id=json['id'], type=json['type'], details=json['details'], street_number=street_info.get('streetNumber'), street=json['street'], country=street_info.get('country'), headline=json['headline'].get('text'), ) def print(self, f: TextIO = stdout) -> None: print(f'Sorte: {self.type}', file=f) if self.country: print(f'Land: {self.country}', file=f) if self.street_number: print(f'Straße: {self.street_number} {self.street}', file=f) else: print(f'Straße: {self.street}', file=f) if self.headline: print(f'Überschrift: {self.headline}', file=f) print(f'Einzelheiten: {self.details}\n', file=f) def get_traffic_news_page( session: Session, news_request: NewsRequest, page: int, ) -> dict[str, Any]: request = Request( method='POST', url='https://www.adac.de/bff', headers={'Accept': 'application/json'}, json=news_request.query(page), ) prepared = session.prepare_request(request) prepared.headers['x-graphql-query-hash'] = md5(prepared.body).hexdigest() with session.send(prepared) as response: response.raise_for_status() return response.json()['data']['trafficNews'] def get_traffic_news( session: Session, request: NewsRequest, ) -> Iterator[NewsResponse]: # There are apparently 10 items per page, but let's not need to rely on this n_items = 0 for page in count(1): data = get_traffic_news_page(session, request, page) for news in data['items']: yield NewsResponse.from_json(news) n_items += 1 if n_items >= data['size']: break def get_args(*, description: str = __doc__) -> Namespace: parser = ArgumentParser(description=description) parser.add_argument( '-c', '--country', default='D', help='Country, one of: D (Germany), A (Austria), I (Italy), CH (Switzerland)', ) parser.add_argument('-s', '--state') parser.add_argument('-r', '--street') parser.add_argument('-t', '--traffic-news', action='store_true') parser.add_argument('-o', '--construction-sites', action='store_true') parser.add_argument('-m', '--max-items', type=int) return parser.parse_args() def main() -> None: args = get_args() request = NewsRequest.from_args(args) with Session() as session: all_news = get_traffic_news(session, request) if args.max_items is not None: all_news = islice(all_news, args.max_items) for news in all_news: news.print() if __name__ == '__main__': main() 

Output

This is the fully-depaginated output with all default arguments.

Sorte: verkehrsmeldung Land: Deutschland Straße: 1 A1 Einzelheiten: Zwischen Köln-Lövenich und Köln-Bocklemünd, Gefahr durch defektes Fahrzeug auf der rechten Seite liegengebliebenes Motorrad Sorte: verkehrsmeldung Land: Deutschland Straße: 1 A1 Überschrift: Dortmund - Köln Einzelheiten: In beiden Richtungen, Leverkusener Brücke, gesperrt für LKW über 3.5 t, vorübergehende Begrenzung der Breite auf 2.3 m, bis 31.12.2025 Sorte: verkehrsmeldung Land: Deutschland Straße: 1 A1 Einzelheiten: Zwischen Lengerich und Kreuz Lotte/Osnabrück, Gefahr durch ein totes Tier auf der Fahrbahn Sorte: verkehrsmeldung Land: Deutschland Straße: 2 A2 Einzelheiten: Ausfahrt zur Raststätte Schafstrift, Tank- und Rastanlage, Raststätte geschlossen, bis 28.02.2022 ca. 18:00 Uhr Wasserschaden in der Raststätte Sorte: verkehrsmeldung Land: Deutschland Straße: 3 A3 Einzelheiten: Zwischen Anschlussstelle Goldbach und Anschlussstelle Aschaffenburg-Ost, Verkehrsstörung, mittlere Geschwindigkeit 30 km/h Sorte: verkehrsmeldung Land: Deutschland Straße: 3 A3 Einzelheiten: Zwischen Aschaffenburg-Ost und Aschaffenburg-West, Unfall mit mehreren Fahrzeugen, linker Fahrstreifen blockiert, mittlerer Fahrstreifen blockiert, Kräfte zur Störungsbeseitigung sind vor Ort, langsam fahren Sorte: stau Land: Deutschland Straße: 5 A5 Einzelheiten: Zwischen Kronau und Kreuz Walldorf, 3 km Stau Sorte: verkehrsmeldung Land: Deutschland Straße: 8 A8 Einzelheiten: Zwischen Dreieck Friedrichsthal und Elversberg, linker Fahrstreifen gesperrt, bis 05.02.2022 Mitternacht Sorte: verkehrsmeldung Land: Deutschland Straße: 8 A8 Einzelheiten: Zwischen Grenzübergang Bad Reichenhall und Anschlussstelle Bad Reichenhall, Verkehrsstörung, mittlere Geschwindigkeit 30 km/h Sorte: stau Land: Deutschland Straße: 8 A8 Einzelheiten: Zwischen Anschlussstelle Irschenberg und Anschlussstelle Weyarn, 6 km Stau, Verkehrsstörung, mittlere Geschwindigkeit 10 km/h, mindestens 33 Minuten Zeitverlust Sorte: verkehrsmeldung Land: Deutschland Straße: 9 A9 Einzelheiten: Zwischen Rudolphstein und Bad Lobenstein, defekter LKW auf dem Standstreifen, bitte vorsichtig fahren Sorte: verkehrsmeldung Land: Deutschland Straße: 24 A24 Einzelheiten: Zwischen Neuruppin und Neuruppin-Süd, 1 defekter PKW auf dem Standstreifen, Gefahr durch 2 Personen auf dem Standstreifen Sorte: verkehrsmeldung Land: Deutschland Straße: 43 A43 Überschrift: Recklinghausen - Wuppertal Einzelheiten: In beiden Richtungen, zwischen Kreuz Recklinghausen und Kreuz Herne, gesperrt für LKW über 3.5 t Sorte: verkehrsmeldung Land: Deutschland Straße: 44 A44 Einzelheiten: Ausfahrt Heiligenhaus, Unfall, Ausfahrt gesperrt Sorte: stau Land: Deutschland Straße: 45 A45 Einzelheiten: Zwischen Anschlussstelle Hagen-Süd und Anschlussstelle Lüdenscheid-Nord, 1 km Stau, Verkehrsstörung, mittlere Geschwindigkeit 10 km/h, mindestens 5 Minuten Zeitverlust Sorte: verkehrsmeldung Land: Deutschland Straße: 73 A73 Einzelheiten: Einfahrt Buttenheim, Unfall im Kurvenbereich Sorte: stau Land: Deutschland Straße: 95 A95 Einzelheiten: Zwischen Anschlussstelle München-Fürstenried und Anschlussstelle München-Sendling-Süd, 3 km stockender Verkehr, Verkehrsstörung, mittlere Geschwindigkeit 30 km/h, mindestens 3 Minuten Zeitverlust Sorte: verkehrsmeldung Straße: B1 Überschrift: Potsdam Glienicker Brücke Einzelheiten: Gefahr durch 1 Person auf der Fahrbahn, fahren Sie bitte besonders vorsichtig Sorte: stau Straße: B2 Einzelheiten: Tunnel Farchant, Stau, Blockabfertigung Sorte: verkehrsmeldung Straße: B2 Einzelheiten: Zwischen Garmisch-Partenkirchen Nord und Übergang Anschluss A95, dichter Verkehr, mindestens 11 Minuten Zeitverlust Sorte: vollsperrung Straße: B2 Überschrift: B5 Berlin, Straße des 17. Juni Einzelheiten: Zwischen Kreisverkehr Großer Stern und Scheidemannstraße gesperrt, Veranstaltung, bis 02.01.2022 23:59 Uhr Sorte: verkehrsmeldung Straße: B2 Überschrift: B2, Ebertstraße Einzelheiten: Zwischen Einmündung Dorotheenstraße und Friedrichstraße, dichter Verkehr, mindestens 6 Minuten Zeitverlust Sorte: vollsperrung Straße: B2 Überschrift: Berlin, Greifswalder Straße Einzelheiten: Zwischen Storkower Straße und Thomas-Mann-Straße gesperrt, Wasserrohrbruch Sorte: verkehrsmeldung Straße: B5 Überschrift: B5, Barmbeker Straße Einzelheiten: Zwischen Kreuzung Borgweg und Weidestraße, dichter Verkehr, mindestens 3 Minuten Zeitverlust Sorte: vollsperrung Straße: B5 Überschrift: Straße des 17. Juni zwischen Yitzhak-Rabin-Straße und Scheidemannstraße in beiden Richtungen Einzelheiten: Gesperrt, Veranstaltung, bis 02.01.2022 23:59 Uhr Sorte: verkehrsmeldung Straße: B9 Einzelheiten: Ausfahrt Waldsee, Gefahr durch Gegenstände auf der Fahrbahn (ein PKW-Rad) Sorte: verkehrsmeldung Straße: B9 Einzelheiten: Ausfahrt Limburgerhof-Neuhofen, Gefahr durch Gegenstände auf der Fahrbahn (ein PKW-Rad) Sorte: verkehrsmeldung Straße: B10 Überschrift: B10, Uferstraße Einzelheiten: Zwischen Anschlussstelle Stuttgart-Ost und Rosensteinbrücke, dichter Verkehr, mindestens 10 Minuten Zeitverlust Sorte: verkehrsmeldung Straße: B19 Einzelheiten: Zwischen Fischen und Anschlussstelle Sonthofen, dichter Verkehr, mindestens 8 Minuten Zeitverlust Sorte: verkehrsmeldung Straße: B23 Einzelheiten: Zwischen Grainau und Garmisch-Partenkirchen, dichter Verkehr, mindestens 7 Minuten Zeitverlust Sorte: verkehrsmeldung Straße: B42 Einzelheiten: Zwischen Königswinter und Anschlussstelle Oberdollendorf, dichter Verkehr, mindestens 4 Minuten Zeitverlust Sorte: verkehrsmeldung Straße: B59 Überschrift: B59, Venloer Straße Einzelheiten: Zwischen Kreuzung Innere Kanalstraße und Äußere Kanalstraße, dichter Verkehr, mindestens 5 Minuten Zeitverlust Sorte: verkehrsmeldung Straße: B73 Überschrift: B73, Buxtehuder Straße Einzelheiten: Zwischen Kreuzung Schloßmühlendamm und Moorburger Straße, dichter Verkehr, mindestens 9 Minuten Zeitverlust Sorte: verkehrsmeldung Straße: B246 Einzelheiten: Zwischen Schönhagen und Berliner Straße, dichter Verkehr, mindestens 3 Minuten Zeitverlust Sorte: verkehrsmeldung Straße: B292 Überschrift: Sinsheim - Mosbach Einzelheiten: Zwischen Bahnhof Finkenhof und B27, B37, Mosbacher Kreuz/Abzweig B37, in beiden Richtungen, die Geschwindigkeit ist begrenzt, zulässige Höchstgeschwindigkeit von 30 km/h, vorübergehende Begrenzung der Fahrbahnbreite auf 3,50 m, Straßenschäden auf Brücken, bis 03.01.2022 Mitternacht Sorte: verkehrsmeldung Straße: B96a Überschrift: Berlin Elsenbrücke Einzelheiten: In beiden Richtungen, Fahrbahn auf einen Fahrstreifen verengt, Staugefahr Brückenschäden Sorte: vollsperrung Straße: Überschrift: Berlin Einzelheiten: Conrad-Blenkle-Straße zwischen Kniprodestraße und Erich-Boltze-Straße in beiden Richtungen, gesperrt, Wasserrohrbruch, bis 31.03.2022 Sorte: vollsperrung Straße: Überschrift: Stadtgebiet Hamburg Einzelheiten: Grelckstraße, von Stapelstraße zur Rütersbarg, Richtungsfahrbahn gesperrt, Einbahnstraßenregelung Richtung Stapelstraße, bis 22.04.2022 18:00 Uhr, Mo-Fr zwischen 06:00 Uhr und 18:00 Uhr Sorte: vollsperrung Straße: Überschrift: Stadtgebiet Duisburg Einzelheiten: Schifferstraße, von Max-Peters-Straße zur Am Innenhof, Richtungsfahrbahn gesperrt, gesperrt für LKW über 7.5 t, Einbahnstraßenregelung Richtung Max-Peters-Straße Sorte: vollsperrung Straße: Überschrift: Stadtgebiet Duisburg Einzelheiten: An der Cölve, in beiden Richtungen, zwischen Altenbruchstraße und Güterstraße, Brücke gesperrt, eine Umleitung ist eingerichtet, bis auf weiteres Sorte: verkehrsmeldung Straße: S2073 Einzelheiten: Zwischen Kleinpienzenau und Weyarn, dichter Verkehr, mindestens 10 Minuten Zeitverlust Sorte: verkehrsmeldung Straße: L52 Überschrift: Mecumstraße Einzelheiten: Zwischen Kreuzung Auf m Hennekamp und Herzogstraße, dichter Verkehr, mindestens 3 Minuten Zeitverlust Sorte: verkehrsmeldung Straße: GN2 Überschrift: Würzburger Straße und Fürth-Fürberg Einzelheiten: In beiden Richtungen, Achtung, Ihnen kommt ein Falschfahrer entgegen, nicht überholen, Vorsicht auf beiden Richtungsfahrbahnen Sorte: vollsperrung Straße: GF4 Überschrift: Sachsenhäuserufer bis Schaumainkai Einzelheiten: Zwischen Alte Brücke und Untermainbrücke gesperrt, bis 03.01.2022 08:00 Uhr Sachsenhausen, Sperrung Sachsenhäuser Ufer und Schaumainkai (südliches Mainufer), Sachsenhäuser Ufer und Schaumainkai zwischen Alter Brücke und Untermainbrücke werden für den Jahreswechsel gesperrt. Verkehrsteilnehmer werden gebeten den genannten Bereich weiträumig zu umfahren. Vom 30.12.2021 bis 03.01.2022 ca. 07:00 Uhr. Sorte: vollsperrung Straße: L1070 Überschrift: Ebertstraße zwischen Hannah-Arendt-Straße und Scheidemannstraße in beiden Richtungen Einzelheiten: Gesperrt, Veranstaltung, bis 02.01.2022 23:59 Uhr Sorte: verkehrsmeldung Straße: Überschrift: Höchst Mainfähre Höchst Einzelheiten: In beiden Richtungen, Personenfähre außer Betrieb, bis 19.01.2022 Mitternacht Sorte: verkehrsmeldung Straße: Überschrift: Fähre „Arneburg“ bei Arneburg Einzelheiten: In beiden Richtungen, über die Elbe, Einstellung des Fährbetriebs -Ende der Saison 2021, von 01.12.2021 bis 01.03.2022 Sorte: verkehrsmeldung Straße: L111 Überschrift: Deutzer Brücke Einzelheiten: In beiden Richtungen, gesperrt für LKW über 3.5 t, bis auf weiteres Sorte: verkehrsmeldung Straße: L1075 Überschrift: Rummelsburger Straße bis Minna-Todenhagen-Straße zwischen Rummelsburger Straße und Minna-Todenhagen-Straße in beiden Richtungen Einzelheiten: Staugefahr, Verkehrsbehinderung, Baustelle, bis voraussichtlich 31.03.2022 Nur eine Linksabbiegespur von der Rummelsburger Straße auf die Minna-Todenhagen-Straße! Nur eine Rechsabbiegespur von der Minna-Todenhagen-Straße auf die Rummelsburger Straße! Sorte: vollsperrung Straße: GHB08 Überschrift: Am Wall Einzelheiten: Von Kreuzung Ostertorstraße zum Herdentor, Richtungsfahrbahn gesperrt, Einbahnstraßenregelung Richtung Ostertorstraße, bis 31.03.2022 Sorte: vollsperrung Straße: GF96 Überschrift: Untermainkai bis Mainkai Einzelheiten: Zwischen Neue Mainzer Straße und Schöne Aussicht gesperrt, bis 03.01.2022 08:00 Uhr Innenstadt, Sperrung Mainkai und Untermainkai (nördliches Mainufer), Mainkai und Untermainkai zwischen Alter Brücke und Untermainbrücke werden für den Jahreswechsel gesperrt. Verkehrsteilnehmer werden gebeten den genannten Bereich weiträumig zu umfahren. Vom 30.12.2021 bis 03.01.2022 ca. 07:00 Uhr. 
\$\endgroup\$
3
\$\begingroup\$

CLI-program feedback

You have used argparse. This is ok, but very basic. There are at least two options that are way easier to read and write: typer and click. Click comes from the Flask ecosystem and is battle-proven. Typer is rather new and in the pydantic ecosystem, but makes excellent use of type annotations. This makes it super easy to read. I try to use typer more often, but I'm very used to click.

Type annotation feedback

  1. dict[str, Any] is only ok for Python 3.9+. In earlier versions you have to write from typing import Dict and Dict[str, Any]. I typically try to support the earlier 2 Python versions, meaning I would try to support Python 3.8 at the moment.
  2. Try to avoid dict[str, Any] and dict[str, str]. You can use TypedDict and pydantic in many cases.

Especially the get_traffic_news could make great use of Pydantic!

GraphQL feedback

You have a single giant string QUERY in your code. That is hard to read and cannot be checked by mypy. Maybe there is a GraphQL query building library (just like ORMs such as SQLAlchemy or query builders like pypika for SQL). I'm not familiar enough with the GraphQL / Python ecosystem to recommend one, though. Let us know if you got one! https://softwarerecs.stackexchange.com/ might help.

Caching of md5hash

Just don't do that. It's not worth it. MD5 is super fast to execute. You might even make it slower. But for sure you make the code more complex than it needs to be.

General style convention feedback

This is general advice I give most of the time if general feedback is asked:

  1. Running the code formatter black (online version) over your code makes it instantly look more professional. It applies several PEP8 conventions directly. Same goes for isort, although isort is not that well-known
  2. Install flake8, flake8-comprehensions, flake8-bugbear and maybe my plugin flake8-simplify: pip install flake8 flake8-comprehensions flake8-bugbear flake8-simplify. Then run flake8 yourcodefile.py. Fix the issues.
  3. Try pyupgrade on your code.
\$\endgroup\$
6
  • \$\begingroup\$ Thanks for the review. Built-in types can be used for type hinting since Python 3.8. And I use both flake8 and pylint on my code base(s), which do not yield any issues with the above code base. Could you give some examples what you would improve regarding the style? \$\endgroup\$ Commented Dec 31, 2021 at 12:32
  • \$\begingroup\$ The very last section was only for the process, less about your specific code. It's easy to write ~100 lines rather clean. It's hard if more people contribute / the project grows 1000x. I didn't run those over your code to check. \$\endgroup\$ Commented Dec 31, 2021 at 12:42
  • \$\begingroup\$ I've fixed the comment regarding dict[str, str] vs Dict[str, str] - thanks for the hint! \$\endgroup\$ Commented Dec 31, 2021 at 12:44
  • 1
    \$\begingroup\$ Regarding a query builder for GraphQL, I only found strawberry as a potential library. But to be honest, pulling in another third party library (next to the commonly available requests) dependency for a 144-line script seems like overkill. \$\endgroup\$ Commented Dec 31, 2021 at 12:53
  • 1
    \$\begingroup\$ I mostly disagree with your GraphQL feedback. Other than minor reformatting, this string should be considered opaque, as it's what the API passes and the design internals are unpublished. \$\endgroup\$ Commented Jan 1, 2022 at 5:18

You must log in to answer this question.