Accessing time series development data from World Bank API
I would turn your mappings mapping countries and indicators around. Then you can simply do:

COUNTRIES = {"EST": "Baltic States", "LVA": "Baltic States", "LTU": "Baltic States", ...}

df["region"] = df["iso3"].map(COUNTRIES)
df["category"] = df["id"].map(INDICATORS)


If you are too lazy to manually invert the dictionary, or like the current structure more but still want to have the easier usage, just use this one-line function:

def invert(d):
    return {v: k for k, values in d.items() for v in values}




Instead of 

ISO3_PARAM = ';'.join(chain(*COUNTRIES.values()))


Use

ISO3_PARAM = ';'.join(chain.from_iterable(COUNTRIES.values()))


This does not have any memory problems (not that that is a problem here, but it is good practice in general).

And if you follow the previous recommendation, replace values with keys.



I would slightly simplify your page_level function by inlining meta["pages"] and using the params argument of requests.get to pass the parameters

def page_level(indicator: str) -&gt; Iterator:
    """ Iterate over all pages related to specific indicator. """
    url = f"{URL}{ISO3_PARAM}/indicator/{indicator}"
    page = 1
    while True:
        meta, data = requests.get(url, params={"format": "json", "page": page}).json()
        yield from item_level(data)
        page += 1
        if page &gt; meta["pages"]:
            break
        sleep(1)




I must say that while I like that you separated things into their own function, I am not so sure about their names (I know, names are hard!). They convey only at which level a function operates, but not what it does.

My suggestions for names:


indicator_level -> get_dataframe (and directly return the dataframe) or get_data
page_level -> get_indicator_data
item_level -> extract_items