Python movie API tutorial

Build a small Python command line tool that takes a movie title, searches for it, then fetches details, cast and ratings in one extra call. You need Python 3.10 or newer, the requests library and an amodb key. It takes about 15 minutes and ends with a complete script.

.py Python Updated View as Markdown

You will build movie.py, a command line tool. You pass it a title, it searches, picks the first match and prints year, runtime, genres, top cast, the IMDb rating and a poster URL. It needs Python 3.10 or newer, the requests library and an amodb key. Plan on 15 minutes.

Step 1: Get an API key

amodb is prepaid with no free tier, so pick a plan on pricing first. The 20, 40 and 120 requests per minute plans all return the same data. Then create a key in your account dashboard. A key is 32 lowercase hex characters.

Export it as an environment variable so it stays out of your source code:

On Windows PowerShell the same line is $env:AMODB_KEY = "YOUR_API_KEY".

Shell
export AMODB_KEY="YOUR_API_KEY"

Step 2: Set up the project

Create a virtual environment and install requests:

On Windows run .venv\Scripts\activate instead of the second line.

Shell
python -m venv .venv
source .venv/bin/activate
pip install requests

Step 3: Send the first request

The search endpoint is GET /3/search/movie with a query parameter. This snippet prints the HTTP status and the id of the best match. Always pass a timeout, because requests waits forever by default.

The reply has the TMDB shape: a results array sorted by relevance, plus page and total_results. See search for every filter.

Python
import os
import requests

res = requests.get(
    "https://api.amodb.dev/3/search/movie",
    params={"query": "fight club", "language": "en-US"},
    headers={"Authorization": f"Bearer {os.environ['AMODB_KEY']}"},
    timeout=10,
)
print(res.status_code, res.json()["results"][0]["id"])

Step 4: Fetch details, cast and ratings

Search results are short. Use the id with GET /3/movie/{movie_id} and add append_to_response=credits,ratings to get cast and the IMDb, Kinopoisk, Metacritic, Rotten Tomatoes and TMDB scores in the same request. Each score is an object with value and votes, or null when the source is not known, so test before you print. Details are on the movies and ratings pages. In the final script session is a requests.Session that already carries the Authorization header.

Python
params = {"language": "en-US", "append_to_response": "credits,ratings"}
movie = session.get(f"{BASE}/movie/{movie_id}", params=params, timeout=10).json()
imdb = (movie.get("ratings") or {}).get("imdb")

Step 5: Handle errors and rate limits

Check the status before you parse the body. Errors use the TMDB body with status_code and status_message.

StatusMeaningWhat the script does
401Invalid or suspended keyStops and says to check AMODB_KEY. Retrying cannot help.
404No such idPrints the status_message.
429Over your requests per minuteSleeps for Retry-After seconds (cap 30), then retries up to twice.
504Backend timeoutPrinted as an error. Run the command again.
Network failureNo connection or timeoutCaught as requests.RequestException and reported.

The retry loop is short:

More codes are listed on errors, and the window rules are on rate limits.

Python
for attempt in range(3):
    res = session.get(url, params=params, timeout=10)
    if res.status_code != 429 or attempt == 2:
        break
    time.sleep(min(int(res.headers.get("Retry-After", "1")), 30))

Step 6: Cache responses on disk

The edge keeps details for 6 hours and search for 10 minutes. Your own cache saves the round trip and spends no request from your limit. The final script stores each reply in a file named after a hash of the path and parameters, and reuses the file while it is younger than the TTL.

Step 7: The complete program

Save this as movie.py and run python movie.py "fight club".

Python
import hashlib
import json
import os
import sys
import time
from pathlib import Path

import requests

BASE = "https://api.amodb.dev/3"
POSTER = "https://img.amodb.dev/t/p/w342"
CACHE_DIR = Path.home() / ".cache" / "amodb-movie-cli"
SEARCH_TTL = 10 * 60
DETAILS_TTL = 6 * 60 * 60


class ApiError(Exception):
    pass


def error_message(res):
    try:
        return res.json()["status_message"]
    except (ValueError, KeyError):
        return res.reason


def get(session, path, params, ttl):
    CACHE_DIR.mkdir(parents=True, exist_ok=True)
    stamp = f"{path}?{json.dumps(params, sort_keys=True)}"
    cache_file = CACHE_DIR / (hashlib.sha256(stamp.encode()).hexdigest() + ".json")
    if cache_file.exists() and time.time() - cache_file.stat().st_mtime < ttl:
        return json.loads(cache_file.read_text(encoding="utf-8"))

    for attempt in range(3):
        try:
            res = session.get(BASE + path, params=params, timeout=10)
        except requests.RequestException as exc:
            raise ApiError(f"network error: {exc}") from exc
        if res.status_code != 429 or attempt == 2:
            break
        time.sleep(min(int(res.headers.get("Retry-After", "1")), 30))

    if res.status_code == 401:
        raise ApiError("401: the key is invalid or suspended. Check AMODB_KEY.")
    if res.status_code != 200:
        raise ApiError(f"{res.status_code}: {error_message(res)}")
    cache_file.write_text(res.text, encoding="utf-8")
    return res.json()


def search_movie(session, title):
    data = get(session, "/search/movie", {"query": title, "language": "en-US"}, SEARCH_TTL)
    if not data["results"]:
        raise ApiError(f"no movie found for {title!r}")
    return data["results"][0]["id"]


def movie_details(session, movie_id):
    params = {"language": "en-US", "append_to_response": "credits,ratings"}
    return get(session, f"/movie/{movie_id}", params, DETAILS_TTL)


def print_card(movie):
    year = (movie.get("release_date") or "")[:4] or "unknown year"
    genres = ", ".join(g["name"] for g in movie["genres"])
    cast = ", ".join(p["name"] for p in movie["credits"]["cast"][:3])
    imdb = (movie.get("ratings") or {}).get("imdb")
    print(f"{movie['title']} ({year}), {movie.get('runtime') or '?'} min")
    print(f"Genres: {genres or 'none listed'}")
    print(f"Cast: {cast or 'none listed'}")
    print(f"IMDb: {imdb['value']} ({imdb['votes']} votes)" if imdb else "IMDb: not known")
    if movie.get("poster_path"):
        print(f"Poster: {POSTER}{movie['poster_path']}")
    print(movie.get("overview") or "No overview in this language.")


def main():
    if len(sys.argv) < 2:
        sys.exit('Usage: python movie.py "fight club"')
    key = os.environ.get("AMODB_KEY")
    if not key:
        sys.exit("Set AMODB_KEY first.")
    session = requests.Session()
    session.headers["Authorization"] = f"Bearer {key}"
    try:
        movie_id = search_movie(session, " ".join(sys.argv[1:]))
        print_card(movie_details(session, movie_id))
    except ApiError as exc:
        sys.exit(f"error: {exc}")


if __name__ == "__main__":
    main()

Next steps

  • Print a poster in another size. The image host takes any of the sizes listed on images, and needs no key.
  • Add a year filter to the search call, or switch to discover for filtered lists.
  • Show a mood meter next to the rating by adding moods to append_to_response. Each entry in moods.results has a name and an avg on a 0 to 10 scale.
  • Read the quickstart for the same flow in curl, then try the JavaScript tutorial for a batch version.