readabs.read_abs_cat

Download timeseries data from the Australian Bureau of Statistics.

Download timeseries data from the Australian Bureau of Statistics (ABS) for a specified ABS catalogue identifier.

  1"""Download *timeseries* data from the Australian Bureau of Statistics.
  2
  3Download timeseries data from the Australian Bureau of Statistics (ABS)
  4for a specified ABS catalogue identifier.
  5"""
  6
  7import calendar
  8from functools import cache
  9from pathlib import Path
 10from typing import Any, Unpack
 11
 12import pandas as pd
 13from pandas import DataFrame
 14
 15from readabs.abs_meta_data import metacol
 16from readabs.grab_abs_url import grab_abs_url, grab_abs_zip
 17from readabs.read_support import HYPHEN, ReadArgs
 18
 19# Constants
 20MAX_DATETIME_CHARS = 20
 21TABLE_DESC_ROW = 4
 22TABLE_DESC_COL = 1
 23
 24
 25# --- functions ---
 26# - public -
 27@cache  # minimise slowness for any repeat business
 28def read_abs_cat(
 29    cat: str,
 30    url: str = "",
 31    **kwargs: Unpack[ReadArgs],
 32) -> tuple[dict[str, DataFrame], DataFrame]:
 33    """For a specific catalogue identifier, return the complete ABS Catalogue information as DataFrames.
 34
 35    This function returns the complete ABS Catalogue information as a
 36    python dictionary of pandas DataFrames, as well as the associated metadata
 37    in a separate DataFrame. The function automates the collection of zip and
 38    excel files from the ABS website. If necessary, these files are downloaded,
 39    and saved into a cache directory. The files are then parsed to extract time
 40    series data, and the associated metadata.
 41
 42    By default, the cache directory is `./.readabs_cache/`. You can change the
 43    default directory name by setting the shell environment variable
 44    `READABS_CACHE_DIR` with the name of the preferred directory.
 45
 46    Parameters
 47    ----------
 48    cat : str
 49        The ABS Catalogue Number for the data to be downloaded and made
 50        available by this function. This argument must be specified in the
 51        function call.
 52
 53    **kwargs : Unpack[ReadArgs]
 54        The following parameters may be passed as optional keyword arguments.
 55
 56    url : str = ""
 57        The URL of an ABS landing page. Use this for discontinued series
 58        that are no longer in the ABS Time Series Directory. If provided,
 59        data will be retrieved from this URL instead of looking up the
 60        catalogue number. Example:
 61        `read_abs_cat(cat="8501.0", url="https://www.abs.gov.au/.../jun-2025")`
 62
 63    keep_non_ts : bool = False
 64        A flag for whether to keep the non-time-series tables
 65        that might form part of an ABS catalogue item. Normally, the
 66        non-time-series information is ignored, and not made available to
 67        the user.
 68
 69    history : str = ""
 70        Provide a month-year string to extract historical ABS data.
 71        For example, you can set history="dec-2023" to the get the ABS data
 72        for a catalogue identifier that was originally published in respect
 73        of Q4 of 2023. Note: not all ABS data sources are structured so that
 74        this technique works in every case; but most are.
 75
 76    verbose : bool = False
 77        Setting this to true may help diagnose why something
 78        might be going wrong with the data retrieval process.
 79
 80    ignore_errors : bool = False
 81        Normally, this function will cease downloading when
 82        an error in encountered. However, sometimes the ABS website has
 83        malformed links, and changing this setting is necessitated. (Note:
 84        if you drop a message to the ABS, they will usually fix broken
 85        links with a business day).
 86
 87    get_zip : bool = True
 88        Download the excel files in .zip files.
 89
 90    get_excel_if_no_zip : bool = True
 91        Only try to download .xlsx files if there are no zip
 92        files available to be downloaded. Only downloading individual excel
 93        files when there are no zip files to download can speed up the
 94        download process.
 95
 96    get_excel : bool = False
 97        The default value means that excel files are not
 98        automatically download. Note: at least one of `get_zip`,
 99        `get_excel_if_no_zip`, or `get_excel` must be true. For most ABS
100        catalogue items, it is sufficient to just download the one zip
101        file. But note, some catalogue items do not have a zip file.
102        Others have quite a number of zip files.
103
104    single_excel_only : str = ""
105        If this argument is set to a table name (without the
106        .xlsx extension), only that excel file will be downloaded. If
107        set, and only a limited subset of available data is needed,
108        this can speed up download times significantly. Note: overrides
109        `get_zip`, `get_excel_if_no_zip`, `get_excel` and `single_zip_only`.
110
111    selected_excel : tuple[str, ...] = ()
112        If set to a tuple of table names (without the .xlsx extension),
113        only those excel files will be downloaded. Useful when several
114        specific tables are needed and downloading the full zip would
115        be wasteful. Example:
116        `selected_excel=("62020001", "62020017", "62020X28")`.
117        Must be a tuple (not a list) because `read_abs_cat` uses an
118        internal cache that requires hashable arguments. Note: overrides
119        `get_zip`, `get_excel_if_no_zip`, `get_excel` and `single_zip_only`
120        when at least one matching file is found.
121
122    single_zip_only : str = ""
123        If this argument is set to a zip file name (without
124        the .zip extension), only that zip file will be downloaded.
125        If set, and only a limited subset of available data is needed,
126        this can speed up download times significantly. Note: overrides
127        `get_zip`, `get_excel_if_no_zip`, and `get_excel`.
128
129    cache_only : bool = False
130        If set to True, this function will only access
131        data that has been previously cached. Normally, the function
132        checks the date of the cache data against the date of the data
133        on the ABS website, before deciding whether the ABS has fresher
134        data that needs to be downloaded to the cache.
135
136    zip_file: str | Path = ""
137        If set to a specific zip file name (with or without the .zip
138        extension), this function will only extract data from that zip file
139        on the local file system. This may be useful for debugging purposes.
140
141    Returns
142    -------
143    tuple[dict[str, DataFrame], DataFrame]
144        The function returns a tuple of two items. The first item is a
145        python dictionary of pandas DataFrames (which is the primary data
146        associated with the ABS catalogue item). The second item is a
147        DataFrame of ABS metadata for the ABS collection.
148
149        Note:
150        You can retrieve non-timeseries data using the grab_abs_url()
151        function. That takes the URL for the ABS landing page for the ABS
152        collection you are interested in. The read_abs_cat function is for
153        ABS catalogue identifiers which are timeseries data, for which the
154        metadata can be extracted.
155
156    Example
157    -------
158
159    ```python
160    import readabs as ra
161    from pandas import DataFrame
162    cat_num = "6202.0"  # The ABS labour force survey
163    data: tuple[dict[str, DataFrame], DataFrame] = ra.read_abs_cat(cat=cat_num)
164    abs_dict, meta = data
165    ```
166
167    """
168    # --- get the time series data ---
169    zip_file = kwargs.get("zip_file")
170    raw_abs_dict = (
171        grab_abs_zip(zip_file, **kwargs) if zip_file else grab_abs_url(cat=cat, url=url, **kwargs)
172    )
173    response = _get_time_series_data(cat, raw_abs_dict, **kwargs)
174
175    if not response:
176        response = {}, DataFrame()
177
178    return response  # dictionary of DataFrames, and a DataFrame of metadata
179
180
181# - private -
182def _get_time_series_data(
183    cat: str,
184    abs_dict: dict[str, DataFrame],
185    **kwargs: Any,  # keep_non_ts, verbose, ignore_errors
186) -> tuple[dict[str, DataFrame], DataFrame]:
187    """Extract the time series data for a specific ABS catalogue identifier."""
188    # --- set up ---
189    cat = "<catalogue number missing>" if not cat.strip() else cat.strip()
190    new_dict: dict[str, DataFrame] = {}
191    meta_data = DataFrame()
192
193    # --- group the sheets and iterate over these groups
194    long_groups = _group_sheets(abs_dict)
195    for table, sheets in long_groups.items():
196        args = {
197            "cat": cat,
198            "from_dict": abs_dict,
199            "table": table,
200            "long_sheets": sheets,
201        }
202        new_dict, meta_data = _capture(new_dict, meta_data, args, **kwargs)
203    return new_dict, meta_data
204
205
206def _copy_raw_sheets(
207    from_dict: dict[str, DataFrame],
208    long_sheets: list[str],
209    to_dict: dict[str, DataFrame],
210    *,
211    keep_non_ts: bool,
212) -> dict[str, DataFrame]:
213    """Copy the raw sheets across to the final dictionary.
214
215    Used if the data is not in a timeseries format, and keep_non_ts
216    flag is set to True. Returns an updated final dictionary.
217    """
218    if not keep_non_ts:
219        return to_dict
220
221    for sheet in long_sheets:
222        if sheet in from_dict:
223            to_dict[sheet] = from_dict[sheet]
224        else:
225            # should not happen
226            raise ValueError(f"Glitch: Sheet {sheet} not found in the data.")
227    return to_dict
228
229
230def _capture(
231    to_dict: dict[str, DataFrame],
232    meta_data: DataFrame,
233    args: dict[str, Any],
234    **kwargs: Any,  # keep_non_ts, ignore_errors
235) -> tuple[dict[str, DataFrame], DataFrame]:
236    """Capture the time series data and meta data from an Excel file.
237
238    For a specific Excel file, capture *both* the time series data
239    from the ABS data files as well as the meta data. These data are
240    added to the input 'to_dict' and 'meta_data' respectively, and
241    the combined results are returned as a tuple.
242    """
243    # --- step 0: set up ---
244    keep_non_ts: bool = kwargs.get("keep_non_ts", False)
245    ignore_errors: bool = kwargs.get("ignore_errors", False)
246
247    # --- step 1: capture the meta data ---
248    short_names = [x.split(HYPHEN, 1)[1] for x in args["long_sheets"]]
249    if "Index" not in short_names:
250        print(f"Table {args['table']} has no 'Index' sheet.")
251        to_dict = _copy_raw_sheets(args["from_dict"], args["long_sheets"], to_dict, keep_non_ts=keep_non_ts)
252        return to_dict, meta_data
253    index = short_names.index("Index")
254
255    index_sheet = args["long_sheets"][index]
256    this_meta = _capture_meta(args["cat"], args["from_dict"], index_sheet)
257    if this_meta.empty:
258        to_dict = _copy_raw_sheets(args["from_dict"], args["long_sheets"], to_dict, keep_non_ts=keep_non_ts)
259        return to_dict, meta_data
260
261    meta_data = pd.concat([meta_data, this_meta], axis=0)
262
263    # --- step 2: capture the actual time series data ---
264    data = _capture_data(meta_data, args["from_dict"], args["long_sheets"], **kwargs)
265    if len(data):
266        to_dict[args["table"]] = data
267    else:
268        # a glitch: we have the metadata but not the actual data
269        error = f"Unexpected: {args['table']} has no actual data."
270        if not ignore_errors:
271            raise ValueError(error)
272        print(error)
273        to_dict = _copy_raw_sheets(args["from_dict"], args["long_sheets"], to_dict, keep_non_ts=keep_non_ts)
274
275    return to_dict, meta_data
276
277
278def _capture_data(
279    abs_meta: DataFrame,
280    from_dict: dict[str, DataFrame],
281    long_sheets: list[str],
282    **kwargs: Any,  # verbose
283) -> DataFrame:
284    """Take a list of ABS data sheets and stitch them into a DataFrame.
285
286    Find the DataFrames for those sheets in the from_dict, and stitch them
287    into a single DataFrame with an appropriate PeriodIndex.
288    """
289    # --- step 0: set up ---
290    verbose: bool = kwargs.get("verbose", False)
291    merged_data = DataFrame()
292    header_row: int = 8
293
294    # --- step 1: capture the time series data ---
295    # identify the data sheets in the list of all sheets from Excel file
296    data_sheets = [x for x in long_sheets if x.split(HYPHEN, 1)[1].startswith("Data")]
297
298    for sheet_name in data_sheets:
299        if verbose:
300            print(f"About to cature data from {sheet_name=}")
301
302        # --- capture just the data, nothing else
303        sheet_data = from_dict[sheet_name].copy()
304
305        # get the columns
306        header = sheet_data.iloc[header_row]
307        sheet_data.columns = pd.Index(header)
308        sheet_data = sheet_data[(header_row + 1) :]
309
310        # get the row indexes
311        sheet_data = _index_to_period(sheet_data, sheet_name, abs_meta, verbose=verbose)
312
313        # --- merge data into a single dataframe
314        if len(merged_data) == 0:
315            merged_data = sheet_data
316        else:
317            merged_data = merged_data.merge(
318                right=sheet_data,
319                how="outer",
320                left_index=True,
321                right_index=True,
322                suffixes=("", ""),
323            )
324
325    # --- step 2 - final tidy-ups
326    # remove NA rows
327    merged_data = merged_data.dropna(how="all")
328    # check for NA columns - rarely happens
329    # Note: these empty columns are not removed,
330    # but it is useful to know they are there
331    if merged_data.isna().all().any() and verbose:
332        na_cols = merged_data.columns[merged_data.isna().all()]
333        print(f"Caution: These columns are all NA: {list(na_cols)}")
334
335    # check for duplicate columns - should not happen
336    # Note: these duplicate columns are removed
337    duplicates = merged_data.columns.duplicated()
338    if duplicates.any():
339        if verbose:
340            dup_table = abs_meta[metacol.table].iloc[0]
341            print(f"Note: duplicates removed from {dup_table}: " + f"{merged_data.columns[duplicates]}")
342        merged_data = merged_data.loc[:, ~duplicates].copy()
343
344    # make the data all floats.
345    return merged_data.astype(float).sort_index()
346
347
348def _index_to_period(sheet_data: DataFrame, sheet_name: str, abs_meta: DataFrame, *, verbose: bool) -> DataFrame:
349    """Convert the index of a DataFrame to a PeriodIndex."""
350    index_column = sheet_data[sheet_data.columns[0]].astype(str)
351    sheet_data = sheet_data.drop(sheet_data.columns[0], axis=1)
352    long_row_names = index_column.str.len() > MAX_DATETIME_CHARS  # 19 chars in datetime str
353    if verbose and long_row_names.any():
354        print(f"You may need to check index column for {sheet_name}")
355    index_column = index_column.loc[~long_row_names]
356    sheet_data = sheet_data.loc[~long_row_names]
357
358    proposed_index = pd.to_datetime(index_column)
359
360    # get the correct period index
361    short_name = sheet_name.split(HYPHEN, 1)[0]
362    series_id = sheet_data.columns[0]
363    freq_value = abs_meta[abs_meta[metacol.table] == short_name].loc[series_id, metacol.freq]
364    freq = str(freq_value).upper().strip()[0]
365    freq = "Y" if freq == "A" else freq  # pandas prefers yearly
366    freq = "Q" if freq == "B" else freq  # treat Biannual as quarterly
367    if freq not in ("Y", "Q", "M", "D"):
368        print(f"Check the frequency of the data in sheet: {sheet_name}")
369
370    # create an appropriate period index
371    if freq:
372        if freq in ("Q", "Y"):
373            month = str(calendar.month_abbr[proposed_index.dt.month.max()]).upper()
374            freq = f"{freq}-{month}"
375        sheet_data.index = pd.PeriodIndex(proposed_index, freq=freq)
376    else:
377        raise ValueError(f"With sheet {sheet_name} could not determime PeriodIndex")
378
379    return sheet_data
380
381
382def _capture_meta(
383    cat: str,
384    from_dict: dict[str, DataFrame],
385    index_sheet: str,
386) -> DataFrame:
387    """Capture the metadata from the Index sheet of an ABS excel file.
388
389    Returns a DataFrame specific to the current excel file.
390    Returning an empty DataFrame, means that the meta data could not
391    be identified. Meta data for each ABS data item is organised by row.
392    """
393    # --- step 0: set up ---
394    frame = from_dict[index_sheet]
395
396    # --- step 1: check if the metadata is present in the right place ---
397    # Unfortunately, the header for some of the 3401.0
398    #                spreadsheets starts on row 10
399    starting_rows = 8, 9, 10
400    required = metacol.did, metacol.id, metacol.stype, metacol.unit
401    required_set = set(required)
402
403    header_row = None
404    header_columns = None
405    for row in starting_rows:
406        columns = frame.iloc[row]
407        if required_set.issubset(set(columns)):
408            header_row = row
409            header_columns = columns
410            break
411
412    if header_row is None or header_columns is None:
413        print(f"Table has no metadata in sheet {index_sheet}.")
414        return DataFrame()
415
416    # --- step 2: capture the metadata ---
417    file_meta = frame.iloc[header_row + 1 :].copy()
418    file_meta.columns = pd.Index(header_columns)
419
420    # make damn sure there are no rogue white spaces
421    for col in required:
422        file_meta[col] = file_meta[col].str.strip()
423
424    # remove empty columns and rows
425    file_meta = file_meta.dropna(how="all", axis=1).dropna(how="all", axis=0)
426
427    # populate the metadata
428    file_meta[metacol.table] = index_sheet.split(HYPHEN, 1)[0]
429    tab_desc_value = frame.iloc[TABLE_DESC_ROW, TABLE_DESC_COL]
430    tab_desc = str(tab_desc_value).split(".", 1)[-1].strip()
431    file_meta[metacol.tdesc] = tab_desc
432    file_meta[metacol.cat] = cat
433
434    # drop last row - should just be copyright statement
435    file_meta = file_meta.iloc[:-1]
436
437    # set the index to the series_id
438    file_meta.index = pd.Index(file_meta[metacol.id])
439
440    return file_meta
441
442
443def _group_sheets(
444    abs_dict: dict[str, DataFrame],
445) -> dict[str, list[str]]:
446    """Group the sheets from an Excel file."""
447    keys = list(abs_dict.keys())
448    long_pairs = [(x.split(HYPHEN, 1)[0], x) for x in keys]
449
450    def group(p_list: list[tuple[str, str]]) -> dict[str, list[str]]:
451        groups: dict[str, list[str]] = {}
452        for x, y in p_list:
453            if x not in groups:
454                groups[x] = []
455            groups[x].append(y)
456        return groups
457
458    return group(long_pairs)
459
460
461# --- initial testing ---
462if __name__ == "__main__":
463
464    def simple_test() -> None:
465        """Test the read_abs_cat function."""
466        # ABS Catalogue ID 8731.0 has a mix of time
467        # series and non-time series data. Also,
468        # it has unusually structured Excel files. So, a good test.
469
470        print("Starting test.")
471
472        d, _m = read_abs_cat("8731.0", keep_non_ts=False, verbose=False)
473        print(f"--- {len(d)=} ---")
474        print(f"--- {d.keys()=} ---")
475        for table in d:
476            freq_str = getattr(d[table].index, "freqstr", "Unknown")
477            print(f"{table=} {d[table].shape=} {freq_str=}")
478
479        print ("=" * 20)
480
481        # Optional: exercise the local zip_file path. Requires a developer to
482        # have a pre-downloaded ABS zip at this location; skipped if absent.
483        local_zip = Path(".test-data/Qrtly-CPI-Time-series-spreadsheets-all.zip")
484        if local_zip.exists():
485            d, _m = read_abs_cat("", zip_file=str(local_zip), verbose=False)
486            print(f"--- {len(d)=} ---")
487            print(f"--- {d.keys()=} ---")
488            for table in d:
489                freq_str = getattr(d[table].index, "freqstr", "Unknown")
490                print(f"{table=} {d[table].shape=} {freq_str=}")
491        else:
492            print(f"Skipping local zip_file test: {local_zip} not present.")
493
494        print("Test complete.")
495
496    simple_test()
MAX_DATETIME_CHARS = 20
TABLE_DESC_ROW = 4
TABLE_DESC_COL = 1
@cache
def read_abs_cat( cat: str, url: str = '', **kwargs: Unpack[readabs.ReadArgs]) -> tuple[dict[str, pandas.DataFrame], pandas.DataFrame]:
 28@cache  # minimise slowness for any repeat business
 29def read_abs_cat(
 30    cat: str,
 31    url: str = "",
 32    **kwargs: Unpack[ReadArgs],
 33) -> tuple[dict[str, DataFrame], DataFrame]:
 34    """For a specific catalogue identifier, return the complete ABS Catalogue information as DataFrames.
 35
 36    This function returns the complete ABS Catalogue information as a
 37    python dictionary of pandas DataFrames, as well as the associated metadata
 38    in a separate DataFrame. The function automates the collection of zip and
 39    excel files from the ABS website. If necessary, these files are downloaded,
 40    and saved into a cache directory. The files are then parsed to extract time
 41    series data, and the associated metadata.
 42
 43    By default, the cache directory is `./.readabs_cache/`. You can change the
 44    default directory name by setting the shell environment variable
 45    `READABS_CACHE_DIR` with the name of the preferred directory.
 46
 47    Parameters
 48    ----------
 49    cat : str
 50        The ABS Catalogue Number for the data to be downloaded and made
 51        available by this function. This argument must be specified in the
 52        function call.
 53
 54    **kwargs : Unpack[ReadArgs]
 55        The following parameters may be passed as optional keyword arguments.
 56
 57    url : str = ""
 58        The URL of an ABS landing page. Use this for discontinued series
 59        that are no longer in the ABS Time Series Directory. If provided,
 60        data will be retrieved from this URL instead of looking up the
 61        catalogue number. Example:
 62        `read_abs_cat(cat="8501.0", url="https://www.abs.gov.au/.../jun-2025")`
 63
 64    keep_non_ts : bool = False
 65        A flag for whether to keep the non-time-series tables
 66        that might form part of an ABS catalogue item. Normally, the
 67        non-time-series information is ignored, and not made available to
 68        the user.
 69
 70    history : str = ""
 71        Provide a month-year string to extract historical ABS data.
 72        For example, you can set history="dec-2023" to the get the ABS data
 73        for a catalogue identifier that was originally published in respect
 74        of Q4 of 2023. Note: not all ABS data sources are structured so that
 75        this technique works in every case; but most are.
 76
 77    verbose : bool = False
 78        Setting this to true may help diagnose why something
 79        might be going wrong with the data retrieval process.
 80
 81    ignore_errors : bool = False
 82        Normally, this function will cease downloading when
 83        an error in encountered. However, sometimes the ABS website has
 84        malformed links, and changing this setting is necessitated. (Note:
 85        if you drop a message to the ABS, they will usually fix broken
 86        links with a business day).
 87
 88    get_zip : bool = True
 89        Download the excel files in .zip files.
 90
 91    get_excel_if_no_zip : bool = True
 92        Only try to download .xlsx files if there are no zip
 93        files available to be downloaded. Only downloading individual excel
 94        files when there are no zip files to download can speed up the
 95        download process.
 96
 97    get_excel : bool = False
 98        The default value means that excel files are not
 99        automatically download. Note: at least one of `get_zip`,
100        `get_excel_if_no_zip`, or `get_excel` must be true. For most ABS
101        catalogue items, it is sufficient to just download the one zip
102        file. But note, some catalogue items do not have a zip file.
103        Others have quite a number of zip files.
104
105    single_excel_only : str = ""
106        If this argument is set to a table name (without the
107        .xlsx extension), only that excel file will be downloaded. If
108        set, and only a limited subset of available data is needed,
109        this can speed up download times significantly. Note: overrides
110        `get_zip`, `get_excel_if_no_zip`, `get_excel` and `single_zip_only`.
111
112    selected_excel : tuple[str, ...] = ()
113        If set to a tuple of table names (without the .xlsx extension),
114        only those excel files will be downloaded. Useful when several
115        specific tables are needed and downloading the full zip would
116        be wasteful. Example:
117        `selected_excel=("62020001", "62020017", "62020X28")`.
118        Must be a tuple (not a list) because `read_abs_cat` uses an
119        internal cache that requires hashable arguments. Note: overrides
120        `get_zip`, `get_excel_if_no_zip`, `get_excel` and `single_zip_only`
121        when at least one matching file is found.
122
123    single_zip_only : str = ""
124        If this argument is set to a zip file name (without
125        the .zip extension), only that zip file will be downloaded.
126        If set, and only a limited subset of available data is needed,
127        this can speed up download times significantly. Note: overrides
128        `get_zip`, `get_excel_if_no_zip`, and `get_excel`.
129
130    cache_only : bool = False
131        If set to True, this function will only access
132        data that has been previously cached. Normally, the function
133        checks the date of the cache data against the date of the data
134        on the ABS website, before deciding whether the ABS has fresher
135        data that needs to be downloaded to the cache.
136
137    zip_file: str | Path = ""
138        If set to a specific zip file name (with or without the .zip
139        extension), this function will only extract data from that zip file
140        on the local file system. This may be useful for debugging purposes.
141
142    Returns
143    -------
144    tuple[dict[str, DataFrame], DataFrame]
145        The function returns a tuple of two items. The first item is a
146        python dictionary of pandas DataFrames (which is the primary data
147        associated with the ABS catalogue item). The second item is a
148        DataFrame of ABS metadata for the ABS collection.
149
150        Note:
151        You can retrieve non-timeseries data using the grab_abs_url()
152        function. That takes the URL for the ABS landing page for the ABS
153        collection you are interested in. The read_abs_cat function is for
154        ABS catalogue identifiers which are timeseries data, for which the
155        metadata can be extracted.
156
157    Example
158    -------
159
160    ```python
161    import readabs as ra
162    from pandas import DataFrame
163    cat_num = "6202.0"  # The ABS labour force survey
164    data: tuple[dict[str, DataFrame], DataFrame] = ra.read_abs_cat(cat=cat_num)
165    abs_dict, meta = data
166    ```
167
168    """
169    # --- get the time series data ---
170    zip_file = kwargs.get("zip_file")
171    raw_abs_dict = (
172        grab_abs_zip(zip_file, **kwargs) if zip_file else grab_abs_url(cat=cat, url=url, **kwargs)
173    )
174    response = _get_time_series_data(cat, raw_abs_dict, **kwargs)
175
176    if not response:
177        response = {}, DataFrame()
178
179    return response  # dictionary of DataFrames, and a DataFrame of metadata

For a specific catalogue identifier, return the complete ABS Catalogue information as DataFrames.

This function returns the complete ABS Catalogue information as a python dictionary of pandas DataFrames, as well as the associated metadata in a separate DataFrame. The function automates the collection of zip and excel files from the ABS website. If necessary, these files are downloaded, and saved into a cache directory. The files are then parsed to extract time series data, and the associated metadata.

By default, the cache directory is ./.readabs_cache/. You can change the default directory name by setting the shell environment variable READABS_CACHE_DIR with the name of the preferred directory.

Parameters

cat : str The ABS Catalogue Number for the data to be downloaded and made available by this function. This argument must be specified in the function call.

**kwargs : Unpack[ReadArgs] The following parameters may be passed as optional keyword arguments.

url : str = "" The URL of an ABS landing page. Use this for discontinued series that are no longer in the ABS Time Series Directory. If provided, data will be retrieved from this URL instead of looking up the catalogue number. Example: read_abs_cat(cat="8501.0", url="https://www.abs.gov.au/.../jun-2025")

keep_non_ts : bool = False A flag for whether to keep the non-time-series tables that might form part of an ABS catalogue item. Normally, the non-time-series information is ignored, and not made available to the user.

history : str = "" Provide a month-year string to extract historical ABS data. For example, you can set history="dec-2023" to the get the ABS data for a catalogue identifier that was originally published in respect of Q4 of 2023. Note: not all ABS data sources are structured so that this technique works in every case; but most are.

verbose : bool = False Setting this to true may help diagnose why something might be going wrong with the data retrieval process.

ignore_errors : bool = False Normally, this function will cease downloading when an error in encountered. However, sometimes the ABS website has malformed links, and changing this setting is necessitated. (Note: if you drop a message to the ABS, they will usually fix broken links with a business day).

get_zip : bool = True Download the excel files in .zip files.

get_excel_if_no_zip : bool = True Only try to download .xlsx files if there are no zip files available to be downloaded. Only downloading individual excel files when there are no zip files to download can speed up the download process.

get_excel : bool = False The default value means that excel files are not automatically download. Note: at least one of get_zip, get_excel_if_no_zip, or get_excel must be true. For most ABS catalogue items, it is sufficient to just download the one zip file. But note, some catalogue items do not have a zip file. Others have quite a number of zip files.

single_excel_only : str = "" If this argument is set to a table name (without the .xlsx extension), only that excel file will be downloaded. If set, and only a limited subset of available data is needed, this can speed up download times significantly. Note: overrides get_zip, get_excel_if_no_zip, get_excel and single_zip_only.

selected_excel : tuple[str, ...] = () If set to a tuple of table names (without the .xlsx extension), only those excel files will be downloaded. Useful when several specific tables are needed and downloading the full zip would be wasteful. Example: selected_excel=("62020001", "62020017", "62020X28"). Must be a tuple (not a list) because read_abs_cat uses an internal cache that requires hashable arguments. Note: overrides get_zip, get_excel_if_no_zip, get_excel and single_zip_only when at least one matching file is found.

single_zip_only : str = "" If this argument is set to a zip file name (without the .zip extension), only that zip file will be downloaded. If set, and only a limited subset of available data is needed, this can speed up download times significantly. Note: overrides get_zip, get_excel_if_no_zip, and get_excel.

cache_only : bool = False If set to True, this function will only access data that has been previously cached. Normally, the function checks the date of the cache data against the date of the data on the ABS website, before deciding whether the ABS has fresher data that needs to be downloaded to the cache.

zip_file: str | Path = "" If set to a specific zip file name (with or without the .zip extension), this function will only extract data from that zip file on the local file system. This may be useful for debugging purposes.

Returns

tuple[dict[str, DataFrame], DataFrame] The function returns a tuple of two items. The first item is a python dictionary of pandas DataFrames (which is the primary data associated with the ABS catalogue item). The second item is a DataFrame of ABS metadata for the ABS collection.

Note:
You can retrieve non-timeseries data using the grab_abs_url()
function. That takes the URL for the ABS landing page for the ABS
collection you are interested in. The read_abs_cat function is for
ABS catalogue identifiers which are timeseries data, for which the
metadata can be extracted.

Example

import readabs as ra
from pandas import DataFrame
cat_num = "6202.0"  # The ABS labour force survey
data: tuple[dict[str, DataFrame], DataFrame] = ra.read_abs_cat(cat=cat_num)
abs_dict, meta = data