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\$
    – Reinderien
    Commented Jan 1, 2022 at 5:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.