Source code for bolster.cli

"""Console script for bolster."""

import os
import sys
from datetime import date

import click
import pandas as pd
from rich.console import Console
from rich.panel import Panel
from rich.text import Text

from . import __version__
from .data_sources import dva, education_suspensions, gender_pay_gap
from .data_sources.cineworld import get_cinema_listings
from .data_sources.companies_house import get_companies_house_records_that_might_be_in_farset, query_basic_company_data
from .data_sources.daera_waste import get_latest_waste_statistics, validate_waste_data
from .data_sources.eoni import get_results as get_ni_election_results
from .data_sources.metoffice import get_uk_precipitation
from .data_sources.ni_house_price_index import build as get_ni_house_prices
from .data_sources.ni_water import get_postcode_to_water_supply_zone, get_water_quality_by_zone
from .data_sources.nisra import ashe as nisra_ashe
from .data_sources.nisra import baby_names as nisra_baby_names
from .data_sources.nisra import births as nisra_births
from .data_sources.nisra import cancer_waiting_times as nisra_cancer
from .data_sources.nisra import composite_index as nisra_composite
from .data_sources.nisra import construction_output as nisra_construction
from .data_sources.nisra import deaths as nisra_deaths
from .data_sources.nisra import disease_prevalence as nisra_disease_prevalence
from .data_sources.nisra import emergency_care_waiting_times as nisra_emergency
from .data_sources.nisra import index_of_production as nisra_iop
from .data_sources.nisra import index_of_services as nisra_ios
from .data_sources.nisra import labour_market as nisra_labour_market
from .data_sources.nisra import marriages as nisra_marriages
from .data_sources.nisra import migration as nisra_migration
from .data_sources.nisra import planning_statistics as nisra_planning
from .data_sources.nisra import population as nisra_population
from .data_sources.nisra import population_projections as nisra_projections
from .data_sources.nisra import public_confidence as nisra_public_confidence
from .data_sources.nisra import registrar_general as nisra_registrar_general
from .data_sources.nisra import wellbeing as nisra_wellbeing
from .data_sources.nisra import work_quality as nisra_work_quality
from .data_sources.nisra.tourism import occupancy as nisra_occupancy
from .data_sources.nisra.tourism import visitor_statistics as nisra_visitors
from .data_sources.wikipedia import get_ni_executive_basic_table
from .utils.rss import filter_entries, get_nisra_statistics_feed, parse_rss_feed


@click.group()
@click.version_option(version=__version__, prog_name="bolster")
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
[docs] def cli(verbose, args=None): """Bolster - A comprehensive Python utility library for Northern Ireland and UK data sources. DATA SOURCES ============ Weather & Environment: * get-precipitation UK precipitation maps from Met Office * water-quality NI water quality by postcode/zone Government & Politics: * ni-executive NI Executive historical data * ni-elections NI Assembly election results (2016-2022) * nisra NISRA statistics (deaths, births, population, economic indicators) * psni PSNI statistics (road traffic collisions) * daera DAERA statistics (municipal waste) Business & Property: * companies-house UK Companies House data queries * ni-house-prices NI house price index data Transport: * dva DVA monthly test statistics (vehicle, driver, theory) Employment & Pay: * gender-pay-gap UK Gender Pay Gap reporting data (2017–present) Entertainment & Lifestyle: * cinema-listings Cineworld movie showtimes Utilities: * rss RSS/Atom feed reader with NISRA integration * list-sources Show all available data sources * --version Show version information * --help Show this help message QUICK EXAMPLES ============== bolster water-quality BT1 5GS # Water quality for postcode bolster nisra deaths --latest # Latest NISRA deaths statistics bolster dva --latest --summary # DVA test statistics summary bolster rss nisra-statistics # Browse NISRA publications bolster ni-executive # NI Executive history bolster companies-house farset # Companies at Farset Labs bolster list-sources # Show all data sources bolster --version # Show version information Note: Use 'bolster <command> --help' for detailed command options """ if verbose: click.echo("Verbose mode enabled") pass
@cli.command() @click.option( "--bounding-box", default=None, help="Geographic bounding box as 'min_lon,min_lat,max_lon,max_lat' (e.g., '-10.0,49.0,2.0,61.0' for UK)", ) @click.option( "--order-name", default=os.getenv("MAP_IMAGES_ORDER_NAME"), help="Met Office API order name for precipitation data (or set MAP_IMAGES_ORDER_NAME env var)", ) @click.option( "--output", default="precipitation.png", help="Output filename for the precipitation map image (default: precipitation.png)", )
[docs] def get_precipitation(bounding_box, order_name, output): """Download UK precipitation data from the Met Office and save as an image. This command retrieves precipitation map data from the UK Met Office API and saves it as a PNG image. Requires a Met Office API key and valid order name. Environment Variables: MET_OFFICE_API_KEY: Your Met Office API key (required) MAP_IMAGES_ORDER_NAME: Default order name for precipitation data (optional) Examples: # Download precipitation map for entire UK bolster get-precipitation --order-name "your-order-name" # Download for specific region (Northern Ireland) bolster get-precipitation --bounding-box "-8.5,54.0,-5.0,55.5" --order-name "your-order-name" # Save to custom filename bolster get-precipitation --output "ni_rain.png" --order-name "your-order-name" """ # Check for required Met Office API key if os.getenv("MET_OFFICE_API_KEY") is None: click.echo("❌ Error: MET_OFFICE_API_KEY environment variable is required") click.echo("πŸ’‘ Get your API key from: https://www.metoffice.gov.uk/services/data/datapoint") click.echo(" Then set it with: export MET_OFFICE_API_KEY=your_key_here") return # Validate bounding box format if provided if bounding_box is not None: try: coords = bounding_box.split(",") if len(coords) != 4: raise ValueError("Must have exactly 4 coordinates") min_lon, min_lat, max_lon, max_lat = map(float, coords) bounding_box = (min_lon, min_lat, max_lon, max_lat) click.echo(f"πŸ“ Using bounding box: {bounding_box}") except ValueError as e: click.echo(f"❌ Error: Invalid bounding box format: {e}") click.echo("πŸ’‘ Expected format: 'min_lon,min_lat,max_lon,max_lat'") click.echo(" Example: '-8.5,54.0,-5.0,55.5' for Northern Ireland") return # Check for order name if order_name is None: order_name = os.getenv("MAP_IMAGES_ORDER_NAME") if order_name is None: click.echo("❌ Error: Order name required but not provided") click.echo("πŸ’‘ Provide it with --order-name or set MAP_IMAGES_ORDER_NAME environment variable") click.echo(" Contact Met Office for your specific order name") return # TODO: API integration testing - requires valid Met Office credentials img = get_uk_precipitation(order_name=order_name, bounding_box=bounding_box) # pragma: no cover img.save(output) # pragma: no cover click.echo(f"Precipitation image saved as '{output}'")
@cli.command() @click.argument("postcode", required=False) @click.option("--zone-code", help="Water supply zone code (alternative to postcode lookup)") @click.option( "--format", "output_format", type=click.Choice(["json", "csv", "table"], case_sensitive=False), default="table", help="Output format (default: table)", )
[docs] def water_quality(postcode, zone_code, output_format): """Get water quality information for a Northern Ireland postcode or zone. Provides water quality data including hardness classification, chemical parameters, and compliance information from Northern Ireland Water. Examples: bolster water-quality BT1 5GS # Lookup by postcode bolster water-quality --zone-code BALM # Lookup by zone code bolster water-quality BT7 --format json # JSON output """ # Prompt for postcode if neither postcode nor zone code provided if not postcode and not zone_code: postcode = click.prompt("πŸ“ Enter a Northern Ireland postcode") try: if postcode and not zone_code: # Validate and normalize postcode format postcode = postcode.upper().strip() if not postcode: click.echo("❌ Error: Empty postcode provided") return # Look up zone code from postcode click.echo("πŸ” Looking up water supply zone...") zone_mapping = get_postcode_to_water_supply_zone() postcode_key = postcode.replace(" ", "") zone_code = zone_mapping.get(postcode_key, "UNKNOWN") if zone_code == "UNKNOWN": click.echo(f"❌ Error: Could not find water supply zone for postcode: {postcode}") click.echo("πŸ’‘ Please check the postcode format (e.g., 'BT1 5GS') or try a different postcode") click.echo(" Only Northern Ireland postcodes are supported") return click.echo(f"βœ… Postcode {postcode} maps to water supply zone: {zone_code}") # Get water quality data click.echo("πŸ’§ Retrieving water quality data...") quality_data = get_water_quality_by_zone(zone_code, strict=False) if quality_data.empty: click.echo(f"❌ Error: No water quality data available for zone: {zone_code}") click.echo("πŸ’‘ This may be a temporary issue. Please try again later or contact NI Water") return click.echo("βœ… Water quality data retrieved successfully") # Output in requested format if output_format == "json": click.echo(quality_data.to_json(indent=2)) elif output_format == "csv": click.echo(quality_data.to_csv()) else: # table format click.echo(f"\nπŸ’§ Water Quality Data for Zone: {zone_code}") click.echo("=" * 50) for param, value in quality_data.items(): click.echo(f" {param}: {value}") click.echo("\nπŸ’‘ Use --format json or csv for machine-readable output") except ConnectionError: click.echo("❌ Error: Could not connect to NI Water services") click.echo("πŸ’‘ Please check your internet connection and try again") except TimeoutError: click.echo("❌ Error: Request timed out") click.echo("πŸ’‘ NI Water services may be temporarily unavailable. Please try again later") except Exception as e: click.echo(f"❌ Error retrieving water quality data: {e}") click.echo("πŸ’‘ If this error persists, please report it as a bug")
@cli.command() @click.option( "--format", "output_format", type=click.Choice(["json", "csv", "table"], case_sensitive=False), default="table", help="Output format (default: table)", ) @click.option("--save", help="Save data to file (specify filename)")
[docs] def ni_executive(output_format, save): """Get Northern Ireland Executive composition and dissolution data. Retrieves historical data about NI Executive periods including establishment dates, dissolution dates, duration, and interregnum periods. Examples: bolster ni-executive # Display as table bolster ni-executive --format json # JSON output bolster ni-executive --save executive.csv # Save to CSV file """ try: executive_data = get_ni_executive_basic_table() if save: if save.endswith(".json"): executive_data.to_json(save, indent=2, date_format="iso") elif save.endswith(".csv"): executive_data.to_csv(save) else: # Default to CSV if no extension specified executive_data.to_csv(save) click.echo(f"Executive data saved to: {save}") return # Output in requested format if output_format == "json": click.echo(executive_data.to_json(indent=2, date_format="iso")) elif output_format == "csv": click.echo(executive_data.to_csv()) else: # table format click.echo("\nNorthern Ireland Executive History") click.echo("=" * 60) click.echo(executive_data.to_string()) except Exception as e: click.echo(f"Error retrieving NI Executive data: {e}")
@cli.command() @click.option("--site-code", type=int, default=117, help="Cineworld site code (default: 117 for Belfast)") @click.option("--date", "screening_date", help="Screening date in YYYY-MM-DD format (default: today)") @click.option( "--format", "output_format", type=click.Choice(["json", "table"], case_sensitive=False), default="table", help="Output format (default: table)", )
[docs] def cinema_listings(site_code, screening_date, output_format): """Get current movie listings from Cineworld cinema. Retrieves movie showtimes and information for a specific Cineworld location. Default location is Belfast (site code 117). Examples: bolster cinema-listings # Belfast, today bolster cinema-listings --site-code 105 # Different location bolster cinema-listings --date 2024-03-20 # Specific date """ try: # Validate and parse screening date if screening_date: try: screening_date = pd.to_datetime(screening_date).date() click.echo(f"🎬 Getting listings for {screening_date}") except ValueError: click.echo("❌ Error: Invalid date format") click.echo("πŸ’‘ Please use YYYY-MM-DD format (e.g., '2024-03-20')") return else: screening_date = date.today() click.echo(f"🎬 Getting today's listings ({screening_date})") # Validate site code if not isinstance(site_code, int) or site_code <= 0: click.echo("❌ Error: Invalid site code") click.echo("πŸ’‘ Site code must be a positive integer (default: 117 for Belfast)") return click.echo(f"🎭 Connecting to Cineworld site {site_code}...") listings = get_cinema_listings(site_code=site_code, screening_date=screening_date) if not listings: click.echo(f"❌ No movie listings found for site {site_code} on {screening_date}") click.echo("πŸ’‘ Possible causes:") click.echo(" - Invalid site code (try default 117 for Belfast)") click.echo(" - No screenings scheduled for this date") click.echo(" - Cineworld API temporarily unavailable") return click.echo(f"βœ… Found {len(listings)} movie listings") # Output in requested format if output_format == "json": import json click.echo(json.dumps(listings, indent=2, default=str)) else: # table format click.echo(f"\n🎬 Cineworld Listings - Site {site_code} - {screening_date}") click.echo("=" * 60) for i, movie in enumerate(listings, 1): click.echo(f"[{i}] {movie.get('title', 'Unknown Title')}") if "showtimes" in movie and movie["showtimes"]: showtimes = ", ".join(movie["showtimes"]) click.echo(f" πŸ• Showtimes: {showtimes}") else: click.echo(" πŸ• No showtimes available") if "genre" in movie: click.echo(f" 🎭 Genre: {movie['genre']}") click.echo("-" * 40) click.echo("\nπŸ’‘ Use --format json for machine-readable output") except ConnectionError: click.echo("❌ Error: Could not connect to Cineworld services") click.echo("πŸ’‘ Please check your internet connection and try again") except TimeoutError: click.echo("❌ Error: Request timed out") click.echo("πŸ’‘ Cineworld services may be temporarily unavailable. Please try again later") except Exception as e: click.echo(f"❌ Error retrieving cinema listings: {e}") click.echo("πŸ’‘ If this error persists, please report it as a bug")
@cli.command() @click.option( "--format", "output_format", type=click.Choice(["json", "csv"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--save", help="Save data to file (specify filename)")
[docs] def ni_house_prices(output_format, save): """Get Northern Ireland house price index data. Downloads and processes the latest NI house price statistics from official sources, including price trends by property type, region, and time period. Examples: bolster ni-house-prices # Display as CSV bolster ni-house-prices --format json # JSON output bolster ni-house-prices --save prices.csv # Save to file """ try: click.echo("Downloading NI house price data... (this may take a moment)") house_data = get_ni_house_prices() if not house_data: click.echo("No house price data available") return if save: if output_format == "json" or save.endswith(".json"): import json with open(save, "w") as f: json.dump({k: v.to_dict() for k, v in house_data.items()}, f, indent=2, default=str) else: # Save all tables as separate CSV files for table_name, df in house_data.items(): filename = f"{save.rsplit('.', 1)[0]}_{table_name}.csv" df.to_csv(filename) click.echo(f"Saved {table_name} to: {filename}") return # Output summary information click.echo(f"\nNI House Price Data - {len(house_data)} tables available:") click.echo("=" * 60) for table_name, df in house_data.items(): click.echo(f"{table_name}: {len(df)} rows, {len(df.columns)} columns") if not df.empty: click.echo(f" Sample data: {list(df.columns[:3])}") click.echo("\nUse --save option to export full data to files") except Exception as e: click.echo(f"Error retrieving house price data: {e}")
@cli.command(name="dva") @click.option("--latest", is_flag=True, default=True, help="Get the most recent DVA data available") @click.option( "--test-type", type=click.Choice(["vehicle", "driver", "theory", "all"], case_sensitive=False), default="all", help="Type of test statistics to retrieve (default: all)", ) @click.option("--year", type=int, help="Filter data by year") @click.option( "--format", "output_format", type=click.Choice(["json", "csv"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)") @click.option("--summary", is_flag=True, help="Show summary dashboard only")
[docs] def dva_cmd(latest, test_type, year, output_format, force_refresh, save, summary): """DVA (Driver & Vehicle Agency) Monthly Test Statistics. Retrieves monthly test statistics from the NI Driver & Vehicle Agency including: - Vehicle tests (MOT-style tests) - Driver tests (practical driving tests) - Theory tests Data available from April 2014 to present. Examples: bolster dva --latest # All test types bolster dva --latest --test-type vehicle # Just vehicle tests bolster dva --latest --year 2024 # Filter by year bolster dva --latest --summary # Quick summary dashboard bolster dva --latest --format json --save tests.json Source: https://www.infrastructure-ni.gov.uk/publications/type/statistics """ console = Console() try: if summary: # Show summary dashboard console.print("\n[bold]DVA Test Statistics Summary[/bold]") console.print("━" * 50) all_data = dva.get_latest_all_tests(force_refresh=force_refresh) # Get latest month info vehicle_df = all_data["vehicle"] driver_df = all_data["driver"] theory_df = all_data["theory"] latest_row = vehicle_df.iloc[-1] latest_month = f"{latest_row['month']} {latest_row['year']}" console.print(f"\n[cyan]Latest Data: {latest_month}[/cyan]\n") # Calculate stats for each test type console.print(f"{'Test Type':<10} {'This Month':>12} {'MoM':>8} {'QoQ':>8} {'YoY':>8}") console.print("─" * 50) for label, df in [("Vehicle", vehicle_df), ("Driver", driver_df), ("Theory", theory_df)]: current = df.iloc[-1]["tests_conducted"] # Month-on-month change if len(df) >= 2: prev_month = df.iloc[-2]["tests_conducted"] mom_change = ((current - prev_month) / prev_month) * 100 mom_str = f"{mom_change:+.1f}%" else: mom_str = "N/A" # Quarter-on-quarter change (3 months ago) if len(df) >= 4: prev_quarter = df.iloc[-4]["tests_conducted"] qoq_change = ((current - prev_quarter) / prev_quarter) * 100 qoq_str = f"{qoq_change:+.1f}%" else: qoq_str = "N/A" # YoY change (same month last year) last_year = df[(df["year"] == latest_row["year"] - 1) & (df["month"] == latest_row["month"])] if not last_year.empty: yoy_change = ( (current - last_year.iloc[0]["tests_conducted"]) / last_year.iloc[0]["tests_conducted"] ) * 100 yoy_str = f"{yoy_change:+.1f}%" else: yoy_str = "N/A" console.print(f"{label:<10} {current:>12,} {mom_str:>8} {qoq_str:>8} {yoy_str:>8}") # YTD total current_year = latest_row["year"] ytd_total = sum(df[df["year"] == current_year]["tests_conducted"].sum() for df in all_data.values()) console.print(f"\n[dim]{current_year} YTD Total: {ytd_total:,} tests[/dim]") return # Regular data retrieval console.print(f"[cyan]Fetching DVA {test_type} test statistics...[/cyan]") if test_type == "all": data = dva.get_latest_all_tests(force_refresh=force_refresh) if year: data = {k: dva.get_tests_by_year(v, year) for k, v in data.items()} else: if test_type == "vehicle": data = dva.get_latest_vehicle_tests(force_refresh=force_refresh) elif test_type == "driver": data = dva.get_latest_driver_tests(force_refresh=force_refresh) else: # theory data = dva.get_latest_theory_tests(force_refresh=force_refresh) if year: data = dva.get_tests_by_year(data, year) # Check for empty data if test_type == "all": if all(df.empty for df in data.values()): console.print("[yellow]⚠️ No data found for the specified filters[/yellow]") return else: if data.empty: console.print("[yellow]⚠️ No data found for the specified filters[/yellow]") return # Display success if test_type == "all": total_records = sum(len(df) for df in data.values()) console.print(f"[green]βœ… Retrieved {total_records} records[/green]") for name, df in data.items(): console.print(f" β€’ {name}: {len(df)} months") else: console.print(f"[green]βœ… Retrieved {len(data)} months of {test_type} test data[/green]") # Save to file if save: try: if test_type == "all": for name, df in data.items(): filename = ( f"{save.rsplit('.', 1)[0]}_{name}.{save.rsplit('.', 1)[-1] if '.' in save else 'csv'}" ) if output_format == "json" or filename.endswith(".json"): df.to_json(filename, orient="records", date_format="iso", indent=2) else: df.to_csv(filename, index=False) console.print(f"[green]πŸ’Ύ Saved {name} to: {filename}[/green]") else: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]πŸ’Ύ Data saved to: {save}[/green]") return except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return # Output to console if test_type == "all": for name, df in data.items(): console.print(f"\n[bold]{name.upper()} TESTS:[/bold]") if output_format == "json": click.echo(df.to_json(orient="records", date_format="iso", indent=2)) else: console.print(df.to_csv(index=False), end="") else: if output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]πŸ’‘ Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") raise click.Abort() from e
@cli.command(name="gender-pay-gap") @click.option( "--year", type=int, default=None, help="Reporting year (e.g. 2024). Defaults to most recent available.", ) @click.option( "--all-years", "all_years", is_flag=True, help="Fetch and combine all available years (2017–present).", ) @click.option( "--postcode-prefix", "postcode_prefix", default=None, help="Filter to employers whose postcode starts with this prefix (e.g. 'BT' for NI, 'EH' for Edinburgh). Returns all UK employers if omitted.", ) @click.option( "--format", "output_format", type=click.Choice(["json", "csv"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--save", help="Save data to file (specify filename)") @click.option("--summary", is_flag=True, help="Show summary statistics only")
[docs] def gender_pay_gap_cmd(year, all_years, postcode_prefix, output_format, save, summary): # pragma: no cover """UK Gender Pay Gap Reporting Data. All UK employers with 250+ employees are required to report their gender pay gap annually. Data is available from 2017 to present. Filter by postcode prefix to focus on a specific region β€” e.g. 'BT' for Northern Ireland, 'EH' for Edinburgh, 'M' for Manchester. Examples: bolster gender-pay-gap --postcode-prefix BT # NI employers, latest year bolster gender-pay-gap --year 2024 # All UK employers, 2024 bolster gender-pay-gap --all-years --postcode-prefix BT # NI trend data bolster gender-pay-gap --postcode-prefix BT --summary # NI summary stats bolster gender-pay-gap --year 2024 --format json --save gpg_2024.json Source: https://gender-pay-gap.service.gov.uk/viewing/download """ console = Console() try: available_years = gender_pay_gap.get_available_years() if all_years: console.print( f"[cyan]Fetching GPG data for all years ({min(available_years)}–{max(available_years)})...[/cyan]" ) df = gender_pay_gap.get_all_years(postcode_prefix=postcode_prefix) else: target_year = year or max(available_years) scope = f"postcode prefix '{postcode_prefix}'" if postcode_prefix else "all UK employers" console.print(f"[cyan]Fetching GPG data for {target_year} ({scope})...[/cyan]") df = gender_pay_gap.get_data(year=target_year, postcode_prefix=postcode_prefix) if df.empty: console.print("[yellow]⚠️ No data found for the specified filters[/yellow]") return if summary: console.print("\n[bold]Gender Pay Gap Summary[/bold]") console.print("━" * 56) years_in_data = sorted(df["reporting_year"].unique()) scope_label = f"Postcode prefix '{postcode_prefix}'" if postcode_prefix else "All UK employers" console.print(f"[cyan]Scope: {scope_label}[/cyan]") console.print(f"[cyan]Years: {', '.join(str(y) for y in years_in_data)}[/cyan]\n") # Per-year summary console.print(f"{'Year':>6} {'Employers':>10} {'Mean gap %':>11} {'Median gap %':>13}") console.print("─" * 46) for yr in years_in_data: yr_df = df[df["reporting_year"] == yr] mean_gap = yr_df["diff_mean_hourly_percent"].median() med_gap = yr_df["diff_median_hourly_percent"].median() console.print(f"{yr:>6} {len(yr_df):>10,} {mean_gap:>11.1f} {med_gap:>13.1f}") # Size breakdown for latest year latest_yr = max(years_in_data) latest = df[df["reporting_year"] == latest_yr] console.print(f"\n[bold]Employer sizes ({latest_yr}):[/bold]") size_counts = latest["employer_size"].value_counts() for size, count in size_counts.items(): console.print(f" {size:<22} {count:>5,}") return console.print(f"[green]βœ… Retrieved {len(df):,} employer records[/green]") if save: try: if output_format == "json" or save.endswith(".json"): df.to_json(save, orient="records", date_format="iso", indent=2) else: df.to_csv(save, index=False) console.print(f"[green]πŸ’Ύ Data saved to: {save}[/green]") except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return if output_format == "json": click.echo(df.to_json(orient="records", date_format="iso", indent=2)) else: console.print(df.to_csv(index=False), end="") except gender_pay_gap.GenderPayGapDataNotFoundError as e: console.print(f"[bold red]❌ Year not available:[/bold red] {e}") available = gender_pay_gap.get_available_years() console.print(f"[yellow]Available years: {min(available)}–{max(available)}[/yellow]") raise click.Abort() from e except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]πŸ’‘ Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try --year with a specific year (e.g. --year 2023)") raise click.Abort() from e
@cli.command() @click.argument("query", required=False) @click.option( "--format", "output_format", type=click.Choice(["json", "csv", "table"], case_sensitive=False), default="table", help="Output format (default: table)", ) @click.option("--save", help="Save data to file (specify filename)")
[docs] def companies_house(query, output_format, save): """Query UK Companies House data for company information. Search for companies by name or get information about companies that might be associated with Farset Labs (Belfast hackerspace). Examples: bolster companies-house "farset" # Search for companies with 'farset' bolster companies-house farset # Search without quotes bolster companies-house --format json # JSON output bolster companies-house --save companies.csv # Save results """ try: if not query: # If no query provided, get Farset Labs related companies click.echo("🏒 No query provided, retrieving Farset Labs related companies...") companies_data = get_companies_house_records_that_might_be_in_farset() query_description = "Farset Labs related companies" else: # Validate and clean query query = query.strip() if len(query) < 2: click.echo("❌ Error: Query too short") click.echo("πŸ’‘ Please provide at least 2 characters for company search") return # Query for specific company click.echo(f"πŸ” Searching Companies House for: '{query}'...") companies_data = query_basic_company_data(query) query_description = f"Companies matching '{query}'" if companies_data is None or companies_data.empty: search_term = query or "Farset Labs" click.echo(f"❌ No companies found for query: {search_term}") click.echo("πŸ’‘ Suggestions:") click.echo(" - Try different search terms") click.echo(" - Check spelling and try partial company names") click.echo(" - Use 'bolster companies-house' without arguments for Farset Labs companies") return click.echo(f"βœ… Found {len(companies_data)} companies") if save: if output_format == "json" or save.endswith(".json"): companies_data.to_json(save, indent=2, date_format="iso") elif output_format == "csv" or save.endswith(".csv"): companies_data.to_csv(save, index=False) else: # Default to CSV if no extension specified companies_data.to_csv(save, index=False) click.echo(f"Companies data saved to: {save}") return # Output in requested format if output_format == "json": click.echo(companies_data.to_json(indent=2, date_format="iso")) elif output_format == "csv": click.echo(companies_data.to_csv(index=False)) else: # table format click.echo(f"\n{query_description}") click.echo("=" * 60) if len(companies_data) > 10: click.echo(f"Showing first 10 of {len(companies_data)} companies:") click.echo(companies_data.head(10).to_string(index=False)) click.echo(f"\nUse --save option to export all {len(companies_data)} results") else: click.echo(companies_data.to_string(index=False)) except ConnectionError: click.echo("❌ Error: Could not connect to Companies House API") click.echo("πŸ’‘ Please check your internet connection and try again") except TimeoutError: click.echo("❌ Error: Request timed out") click.echo("πŸ’‘ Companies House API may be temporarily unavailable. Please try again later") except Exception as e: error_msg = str(e).lower() if "api key" in error_msg or "authentication" in error_msg: click.echo("❌ Error: Companies House API authentication failed") click.echo("πŸ’‘ Please check your API credentials or try again later") elif "rate limit" in error_msg: click.echo("❌ Error: API rate limit exceeded") click.echo("πŸ’‘ Please wait a few minutes before making another request") else: click.echo(f"❌ Error querying Companies House data: {e}") click.echo("πŸ’‘ If this error persists, please report it as a bug")
@cli.command() @click.option( "--election-year", type=click.Choice(["2016", "2017", "2022", "all"], case_sensitive=False), default="all", help="Filter by election year (default: all)", ) @click.option( "--format", "output_format", type=click.Choice(["json", "csv", "table"], case_sensitive=False), default="table", help="Output format (default: table)", ) @click.option("--save", help="Save data to file (specify filename)")
[docs] def ni_elections(election_year, output_format, save): """Get Northern Ireland Assembly election results (2016-2022). Retrieve detailed election results including candidates, parties, constituencies, and vote counts for NI Assembly elections. Examples: bolster ni-elections # All election results bolster ni-elections --election-year 2022 # 2022 results only bolster ni-elections --format json # JSON output bolster ni-elections --save elections.csv # Save to file """ try: click.echo("πŸ—³οΈ Retrieving NI Assembly election results...") # Get election results (the function returns data for all available years) election_data = get_ni_election_results() if not election_data: click.echo("❌ No election data available") click.echo("πŸ’‘ This may be a temporary issue. Please try again later") return # Filter by year if specified (and not 'all') if election_year != "all": # This would need to be implemented based on the structure of election_data # For now, we'll just note that filtering is requested click.echo(f"πŸ“Š Filtering results for year: {election_year}") click.echo("βœ… Election data retrieved successfully") # Handle file saving if save: try: if output_format == "json" or save.endswith(".json"): if isinstance(election_data, dict): import json with open(save, "w") as f: json.dump(election_data, f, indent=2, default=str) elif hasattr(election_data, "to_json"): election_data.to_json(save, indent=2, date_format="iso") else: click.echo("❌ Error: Cannot convert election data to JSON format") click.echo("πŸ’‘ Try using CSV format instead") return else: if hasattr(election_data, "to_csv"): election_data.to_csv(save, index=False) else: # If it's a dict of DataFrames, save each separately if isinstance(election_data, dict): for key, df in election_data.items(): if hasattr(df, "to_csv"): filename = f"{save.rsplit('.', 1)[0]}_{key}.csv" df.to_csv(filename, index=False) click.echo(f"πŸ’Ύ Saved {key} to: {filename}") return click.echo(f"πŸ’Ύ Election data saved to: {save}") return except PermissionError: click.echo(f"❌ Error: Permission denied writing to {save}") click.echo("πŸ’‘ Check file permissions or choose a different location") return except Exception as e: click.echo(f"❌ Error saving file: {e}") click.echo("πŸ’‘ Try a different filename or location") return # Output in requested format if output_format == "json": import json if isinstance(election_data, dict): click.echo(json.dumps(election_data, indent=2, default=str)) elif hasattr(election_data, "to_json"): click.echo(election_data.to_json(indent=2, date_format="iso")) else: click.echo("Warning: Cannot display election data in JSON format") elif output_format == "csv": if hasattr(election_data, "to_csv"): click.echo(election_data.to_csv(index=False)) else: click.echo("Warning: Cannot display election data in CSV format") else: # table format click.echo("\nNI Assembly Election Results") if election_year != "all": click.echo(f"Year: {election_year}") click.echo("=" * 60) if hasattr(election_data, "to_string"): # If it's a DataFrame if len(election_data) > 20: click.echo(f"Showing first 20 of {len(election_data)} records:") click.echo(election_data.head(20).to_string(index=False)) click.echo(f"\nUse --save option to export all {len(election_data)} results") else: click.echo(election_data.to_string(index=False)) elif isinstance(election_data, dict): # If it's a dict of DataFrames or other data for key, value in election_data.items(): click.echo(f"\n{key}:") click.echo("-" * 40) if hasattr(value, "to_string"): if len(value) > 10: click.echo(f"First 10 of {len(value)} records:") click.echo(value.head(10).to_string(index=False)) else: click.echo(value.to_string(index=False)) else: click.echo(str(value)) else: click.echo(str(election_data)) except Exception as e: click.echo(f"Error retrieving election data: {e}")
@cli.group()
[docs] def daera(): """DAERA (Department of Agriculture, Environment and Rural Affairs) statistics. Commands for accessing Northern Ireland environmental and agricultural statistics published by DAERA. """ pass
@daera.command(name="waste") @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--council", help="Filter by council area name (partial match)") @click.option("--financial-year", help="Filter by financial year (e.g. 2024/25)") @click.option("--summary", is_flag=True, help="Show summary statistics only") @click.option("--save", help="Save data to file (specify filename)") @click.option( "--format", "output_format", type=click.Choice(["table", "csv", "json"]), default="table", help="Output format", )
[docs] def daera_waste_cmd(force_refresh, council, financial_year, summary, save, output_format): r"""DAERA NI LAC Municipal Waste statistics. Downloads the latest quarterly waste management time-series from DAERA, covering all NI council areas from 2006/07 onwards. Examples:  bolster daera waste # All waste statistics bolster daera waste --council Belfast # Filter by council bolster daera waste --financial-year 2024/25 # Latest year bolster daera waste --summary # Summary only bolster daera waste --save waste.csv # Save to CSV """ console = Console() try: with console.status("[bold green]Downloading DAERA waste statistics..."): df = get_latest_waste_statistics(force_refresh=force_refresh) validate_waste_data(df) if council: df = df[df["council_area"].str.contains(council, case=False, na=False)] if df.empty: console.print(f"[yellow]No data found for council matching '{council}'[/yellow]") return if financial_year: df = df[df["financial_year"] == financial_year] if df.empty: console.print(f"[yellow]No data found for financial year '{financial_year}'[/yellow]") return if save: if save.endswith(".json"): df.to_json(save, orient="records", indent=2) else: df.to_csv(save, index=False) console.print(f"[green]Saved {len(df)} rows to {save}[/green]") return if summary: console.print( Panel( f"[bold cyan]DAERA NI LAC Municipal Waste Statistics[/bold cyan]\n" f"[green]Rows:[/green] {len(df):,}\n" f"[green]Financial years:[/green] {df['financial_year'].nunique()} " f"({df['financial_year'].min()} \u2013 {df['financial_year'].max()})\n" f"[green]Council areas:[/green] {df['council_area'].nunique()}\n" f"[green]Data status:[/green] {', '.join(sorted(df['data_status'].dropna().unique()))}", title="Summary", border_style="cyan", ) ) ni_latest = df[ (df["council_area"] == "Northern Ireland") & (df["financial_year"] == df["financial_year"].max()) ] if not ni_latest.empty: arisings = ni_latest["lac_waste_arisings_tonnes"].sum() recycling_rate = ni_latest["lac_dry_recycling_composting_rate_pct"].mean() console.print(f"\n[bold]Latest year NI totals ({df['financial_year'].max()}):[/bold]") console.print(f" LAC waste arisings: {arisings:,.0f} tonnes") if pd.notna(recycling_rate): console.print(f" Dry recycling & composting rate: {recycling_rate:.1f}%") return if output_format == "json": click.echo(df.to_json(orient="records", indent=2)) elif output_format == "csv": click.echo(df.to_csv(index=False)) else: display_cols = [ "financial_year", "quarter_code", "council_area", "data_status", "lac_waste_arisings_tonnes", "lac_dry_recycling_composting_rate_pct", "lac_landfill_rate_pct", "hh_waste_arisings_tonnes", ] available_cols = [c for c in display_cols if c in df.columns] out = df[available_cols].copy() if len(out) > 50: console.print(f"[dim]Showing first 50 of {len(out):,} rows[/dim]") out = out.head(50) click.echo(out.to_string(index=False)) except Exception as e: console.print(f"[red]Error: {e}[/red]") raise SystemExit(1) from e
@cli.group()
[docs] def rss(): """RSS/Atom feed reading commands. Tools for reading and parsing RSS/Atom feeds with beautiful terminal formatting. Includes generic feed reader and specialized commands for NISRA statistics. """ pass
@rss.command(name="read") @click.argument("feed_url") @click.option("--limit", "-l", default=20, help="Maximum number of entries to display") @click.option("--title-filter", "-t", help="Filter entries by title (case-insensitive)") @click.option("--after-date", "-a", help="Show entries published after this date (YYYY-MM-DD)") @click.option("--before-date", "-b", help="Show entries published before this date (YYYY-MM-DD)") @click.option("--format", "-f", type=click.Choice(["rich", "json", "csv"]), default="rich", help="Output format")
[docs] def rss_read(feed_url, limit, title_filter, after_date, before_date, format): """Read and display RSS/Atom feeds with beautiful formatting. Fetches and parses RSS or Atom feeds from any URL, displaying entries with rich formatting in the terminal. Supports filtering by title and date range. Args: feed_url: URL of the RSS or Atom feed to read limit: Maximum number of entries to display title_filter: Filter entries by title keyword after_date: Show only entries after this date before_date: Show only entries before this date format: Output format (rich, json, csv) Examples: # Display NISRA statistics feed bolster rss-feed "https://www.gov.uk/search/research-and-statistics.atom" # Filter by title keyword bolster rss-feed URL --title-filter "health" # Limit to recent entries bolster rss-feed URL --limit 10 --after-date 2024-01-01 Note: Output formats: - rich: Beautiful terminal output with colors and formatting (default) - json: Machine-readable JSON output - csv: Comma-separated values for spreadsheet import """ console = Console() try: with console.status(f"[bold green]Fetching feed from {feed_url}..."): feed = parse_rss_feed(feed_url) # Apply filters entries = feed.entries if title_filter or after_date or before_date: entries = filter_entries( entries, title_contains=title_filter, after_date=after_date, before_date=before_date, ) # Limit entries if limit and limit > 0: entries = entries[:limit] if format == "rich": # Display feed header console.print( Panel( f"[bold cyan]{feed.title}[/bold cyan]\n" f"[dim]{feed.description or 'No description'}[/dim]\n" f"[yellow]Showing {len(entries)} of {len(feed.entries)} entries[/yellow]", title="πŸ“° Feed Information", border_style="cyan", ) ) # Display entries for idx, entry in enumerate(entries, 1): # Create entry panel date_str = entry.published.strftime("%Y-%m-%d %H:%M") if entry.published else "No date" # Build content content_lines = [f"[bold]{entry.title}[/bold]"] content_lines.append(f"[dim]{date_str}[/dim]") if entry.author: content_lines.append(f"[green]Author:[/green] {entry.author}") if entry.categories: categories_str = ", ".join(entry.categories[:5]) content_lines.append(f"[blue]Categories:[/blue] {categories_str}") if entry.summary: # Truncate long summaries summary = entry.summary[:300] + "..." if len(entry.summary) > 300 else entry.summary content_lines.append(f"\n{summary}") content_lines.append(f"\n[cyan]πŸ”— {entry.link}[/cyan]") console.print( Panel( "\n".join(content_lines), title=f"Entry {idx}/{len(entries)}", border_style="green" if idx % 2 == 0 else "yellow", ) ) elif format == "json": import json output = { "feed": { "title": feed.title, "link": feed.link, "description": feed.description, "entry_count": len(entries), }, "entries": [entry.to_dict() for entry in entries], } click.echo(json.dumps(output, indent=2, default=str)) elif format == "csv": df = pd.DataFrame( [ { "title": entry.title, "link": entry.link, "published": entry.published.isoformat() if entry.published else None, "author": entry.author, "summary": entry.summary[:100] if entry.summary else None, } for entry in entries ] ) click.echo(df.to_csv(index=False)) except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") raise click.Abort() from e
@rss.command(name="nisra-statistics") @click.option("--limit", "-l", default=20, help="Maximum number of entries to display") @click.option("--title-filter", "-t", help="Filter entries by title (case-insensitive)") @click.option("--after-date", "-a", help="Show entries published after this date (YYYY-MM-DD)") @click.option( "--order", "-o", type=click.Choice(["recent", "oldest"]), default="recent", help="Sort order by release date" ) @click.option("--format", "-f", type=click.Choice(["rich", "json", "csv"]), default="rich", help="Output format")
[docs] def rss_nisra_statistics(limit, title_filter, after_date, order, format): """Browse NISRA (Northern Ireland Statistics and Research Agency) publications. Fetches and displays recent research and statistics publications from NISRA via the GOV.UK website. Includes reports on demographics, health, economy, labour market, crime, and lifestyle surveys. Args: limit: Maximum number of entries to display title_filter: Filter entries by title keyword after_date: Show only entries after this date order: Sort order for entries (newest, oldest) format: Output format (rich, json, csv) Examples: # Show recent NISRA publications bolster nisra-statistics # Find health-related statistics bolster nisra-statistics --title-filter "health" # Get oldest publications first bolster nisra-statistics --order oldest --limit 10 # Export to CSV bolster nisra-statistics --format csv > nisra.csv Note: Publication types include: - Labour Market Statistics - Population & Demographics - Health & Social Care - Crime & Justice Statistics - Economic Statistics - Lifestyle & Wellbeing Surveys Output formats: - rich: Beautiful terminal output with colors (default) - json: Machine-readable JSON format - csv: Spreadsheet-compatible CSV format """ console = Console() try: with console.status("[bold green]Fetching NISRA statistics feed from GOV.UK..."): feed = get_nisra_statistics_feed(order=order) # Apply filters entries = feed.entries if title_filter or after_date: entries = filter_entries( entries, title_contains=title_filter, after_date=after_date, ) # Limit entries if limit and limit > 0: entries = entries[:limit] if format == "rich": # Display header console.print( Panel( f"[bold cyan]NISRA Research & Statistics[/bold cyan]\n" f"[dim]Northern Ireland Statistics and Research Agency[/dim]\n" f"[yellow]Showing {len(entries)} publications[/yellow]", title="πŸ“Š NISRA Publications", border_style="cyan", ) ) # Group entries by month if we have dates from collections import defaultdict by_month = defaultdict(list) for entry in entries: if entry.published: month_key = entry.published.strftime("%Y-%m") by_month[month_key].append(entry) else: by_month["unknown"].append(entry) # Display by month for month_key in sorted(by_month.keys(), reverse=(order == "recent")): month_entries = by_month[month_key] if month_key != "unknown": console.print(f"\n[bold magenta]πŸ“… {month_key}[/bold magenta]") for entry in month_entries: date_str = entry.published.strftime("%Y-%m-%d") if entry.published else "No date" # Create compact entry display text = Text() text.append(" β€’ ", style="dim") text.append(entry.title, style="bold") text.append(f" [{date_str}]", style="dim cyan") console.print(text) if entry.summary: summary = entry.summary[:150] + "..." if len(entry.summary) > 150 else entry.summary console.print(f" [dim]{summary}[/dim]") console.print(f" [cyan]πŸ”— {entry.link}[/cyan]") console.print() elif format == "json": import json output = { "feed": { "title": "NISRA Research & Statistics", "organization": "Northern Ireland Statistics and Research Agency", "source": "GOV.UK", "entry_count": len(entries), }, "entries": [entry.to_dict() for entry in entries], } click.echo(json.dumps(output, indent=2, default=str)) elif format == "csv": df = pd.DataFrame( [ { "title": entry.title, "link": entry.link, "published": entry.published.isoformat() if entry.published else None, "summary": entry.summary[:200] if entry.summary else None, } for entry in entries ] ) click.echo(df.to_csv(index=False)) except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") raise click.Abort() from e
@cli.group()
[docs] def nisra(): """NISRA (Northern Ireland Statistics and Research Agency) data sources. Access official statistics and research publications from NISRA including: - Weekly death registrations with demographic breakdowns - Labour market statistics (employment, economic inactivity) - Economic indicators (Index of Production, Index of Services, Construction Output, Composite Index) - Population estimates, births, marriages, migration - Tourism (accommodation occupancy, visitor statistics) - Cancer and emergency care waiting times All data is sourced directly from NISRA publications and cached locally for performance. Use --force-refresh to bypass cache and download fresh data. """ pass
@nisra.command(name="feed") @click.option("--limit", "-n", default=20, help="Number of entries to show (default: 20)") @click.option("--filter", "-f", "title_filter", help="Filter entries by title (case-insensitive)") @click.option("--days", "-d", type=int, help="Show entries from last N days") @click.option("--check-coverage", is_flag=True, help="Show which datasets have modules implemented")
[docs] def nisra_feed(limit: int, title_filter: str, days: int, check_coverage: bool): """Show recent NISRA publications from RSS feed. Useful for discovering new datasets and checking for updates. Examples: bolster nisra feed # Recent 20 publications bolster nisra feed -n 50 # More entries bolster nisra feed -f tourism # Filter by title bolster nisra feed --days 7 # Last week only bolster nisra feed --check-coverage # Show implementation status """ from datetime import datetime, timedelta from rich.console import Console from rich.table import Table from bolster.utils.rss import filter_entries, get_nisra_statistics_feed console = Console() # Known implemented modules (keywords that map to our modules) implemented_keywords = { "death": "deaths", "birth": "births", "marriage": "marriages", "civil partnership": "civil-partnerships", "labour market": "labour-market", "labour force": "labour-market", "population": "population", "migration": "migration", "occupancy": "occupancy", "tourism": "visitors", "visitor": "visitors", "index of services": "index-of-services", "index of production": "index-of-production", "construction output": "construction-output", "ashe": "ashe", "hours and earnings": "ashe", "composite economic index": "composite-index", "nicei": "composite-index", "wellbeing": "wellbeing", "cancer waiting": "cancer-waiting-times", "planning": "planning-statistics", "emergency care": "emergency-care", "elective": "elective-waiting-times", "outpatient": "elective-waiting-times", "quarterly employment survey": "quarterly-employment-survey", "claimant count": "claimant-count", "claimant": "claimant-count", "stillbirth": "stillbirths", "registrar general": "registrar-general", "baby names": "baby-names", "work quality": "work-quality", } with console.status("Fetching NISRA RSS feed..."): feed = get_nisra_statistics_feed(limit=limit) entries = feed.entries # Apply date filter if days: cutoff = datetime.now() - timedelta(days=days) entries = [e for e in entries if e.published and e.published >= cutoff] # Apply title filter if title_filter: entries = filter_entries(entries, title_contains=title_filter) # Limit entries entries = entries[:limit] if not entries: console.print("[yellow]No entries found matching criteria[/yellow]") return # Build table table = Table(title=f"NISRA Publications ({len(entries)} shown)") table.add_column("Date", style="cyan", width=10) table.add_column("Title", style="white") if check_coverage: table.add_column("Module", style="green", width=20) for entry in entries: date_str = entry.published.strftime("%Y-%m-%d") if entry.published else "N/A" title = entry.title[:75] + "..." if len(entry.title) > 75 else entry.title if check_coverage: # Check if we have a module for this module = None title_lower = entry.title.lower() for keyword, mod_name in implemented_keywords.items(): if keyword in title_lower: module = mod_name break module_str = f"[green]βœ“ {module}[/green]" if module else "[dim]-[/dim]" table.add_row(date_str, title, module_str) else: table.add_row(date_str, title) console.print(table) if check_coverage: # Summary covered = sum(1 for e in entries if any(kw in e.title.lower() for kw in implemented_keywords)) console.print(f"\n[green]Covered: {covered}[/green] | [yellow]Not covered: {len(entries) - covered}[/yellow]")
@nisra.command(name="deaths") @click.option( "--dimension", type=click.Choice(["totals", "demographics", "geography", "place", "all"], case_sensitive=False), default="all", help="Which dimension to retrieve (default: all)", ) @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)")
[docs] def nisra_deaths_cmd(dimension, output_format, force_refresh, save): """NISRA Weekly Deaths Statistics. Retrieves weekly death registrations in Northern Ireland with breakdowns by: - Totals (COVID-19 deaths, flu/pneumonia deaths, excess deaths) - Demographics (age, sex) - Geography (Local Government Districts) - Place of death (hospital, home, care home, etc.) Examples: --------- Get COVID-19 and flu/pneumonia deaths:: bolster nisra deaths --dimension totals Get latest demographics breakdown as CSV:: bolster nisra deaths --dimension demographics Get all dimensions as JSON:: bolster nisra deaths --dimension all --format json Save totals data to analyze COVID trends:: bolster nisra deaths --dimension totals --save deaths_totals.csv Force refresh cached data:: bolster nisra deaths --force-refresh Notes: ------ - Based on registration date, not death occurrence date - Most deaths registered within 5 days in Northern Ireland - Weekly files are provisional and subject to revision - Dimensions are NOT cross-tabulated in source data - COVID-19/flu deaths are NOT broken down by age/sex in source - Excess deaths calculated using multiple methodologies Dimensions ---------- totals Weekly totals with COVID-19, flu/pneumonia, excess deaths demographics Age and sex breakdown (Total/Male/Female Γ— age ranges) geography Local Government Districts (11 LGDs) place Place of death (Hospital, Home, Care Home, Hospice, Other) all All dimensions (returns separate tables) Source ------ https://www.nisra.gov.uk/statistics/death-statistics/weekly-death-registrations-northern-ireland """ console = Console() try: with console.status("[bold green]Downloading latest NISRA deaths data..."): data = nisra_deaths.get_latest_deaths(dimension=dimension, force_refresh=force_refresh) # Handle the result based on whether it's a single DataFrame or dict of DataFrames if dimension == "all": console.print("[green]βœ… Retrieved all dimensions successfully[/green]") total_records = sum(len(df) for df in data.values()) console.print(f"[cyan]πŸ“Š Total records: {total_records}[/cyan]") for dim_name, df in data.items(): console.print(f" β€’ {dim_name}: {len(df)} records") if not df.empty: week_range = f"{df['week_ending'].min().date()} to {df['week_ending'].max().date()}" console.print(f" [dim]Weeks: {week_range}[/dim]") else: console.print(f"[green]βœ… Retrieved {dimension} dimension successfully[/green]") console.print(f"[cyan]πŸ“Š Total records: {len(data)}[/cyan]") if not data.empty: week_range = f"{data['week_ending'].min().date()} to {data['week_ending'].max().date()}" console.print(f"[dim]Weeks: {week_range}[/dim]") # Handle file saving if save: try: if dimension == "all": # Save each dimension to a separate file for dim_name, df in data.items(): filename = ( f"{save.rsplit('.', 1)[0]}_{dim_name}.{save.rsplit('.', 1)[-1] if '.' in save else 'csv'}" ) if output_format == "json" or filename.endswith(".json"): df.to_json(filename, orient="records", date_format="iso", indent=2) else: df.to_csv(filename, index=False) console.print(f"[green]πŸ’Ύ Saved {dim_name} to: {filename}[/green]") else: # Save single dimension if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]πŸ’Ύ Data saved to: {save}[/green]") return except PermissionError: console.print(f"[red]❌ Error: Permission denied writing to {save}[/red]") console.print("[yellow]πŸ’‘ Check file permissions or choose a different location[/yellow]") return except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return # Output to console in requested format if output_format == "json": import json if dimension == "all": # Convert DataFrames to JSON-serializable format output = {dim_name: df.to_dict(orient="records") for dim_name, df in data.items()} click.echo(json.dumps(output, indent=2, default=str)) else: click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: # csv format if dimension == "all": console.print("\n[yellow]πŸ’‘ Tip: For 'all' dimensions, use --save to export to files[/yellow]") console.print("[yellow] Displaying demographics dimension only:[/yellow]\n") click.echo(data["demographics"].to_csv(index=False)) else: click.echo(data.to_csv(index=False)) except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]πŸ’‘ Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") console.print(" β€’ Visit NISRA website to verify data availability") raise click.Abort() from e
@nisra.command(name="labour-market") @click.option( "--dimension", type=click.Choice(["employment", "economic_inactivity", "lgd", "all"], case_sensitive=False), default="all", help="Which dimension to retrieve (default: all)", ) @click.option("--table", "table_deprecated", hidden=True, default=None) @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)")
[docs] def nisra_labour_market_cmd(dimension, table_deprecated, output_format, force_refresh, save): """NISRA Labour Force Survey Statistics. Retrieves Labour Force Survey (LFS) data for Northern Ireland including: - Employment by age band and sex (quarterly) - Economic inactivity rates and numbers with historical time series (quarterly) - Employment by Local Government District (annual) The LFS is a sample survey of households providing labour force statistics using internationally agreed concepts and definitions. Examples: --------- Get latest employment data by age and sex:: bolster nisra labour-market --dimension employment Get economic inactivity time series (2012-2025):: bolster nisra labour-market --dimension economic_inactivity Get employment by Local Government District (annual):: bolster nisra labour-market --dimension lgd Get all dimensions as JSON:: bolster nisra labour-market --dimension all --format json Save employment data to analyze age distribution:: bolster nisra labour-market --dimension employment --save employment.csv Force refresh cached data:: bolster nisra labour-market --force-refresh Notes: ------ - Survey data with sampling variability (see NISRA notes on confidence intervals) - Quarterly publications covering 3-month rolling periods - Some estimates based on small samples (indicated by shading in source) - Estimates <3 suppressed for disclosure control - Not seasonally adjusted - Working age: 16-64 for both males and females Tables ------ employment Employment by age band and sex (Table 2.15) β€’ Percentage distribution across age groups β€’ Total employment numbers by sex β€’ Quarterly snapshot data economic_inactivity Economic inactivity by sex (Table 2.21) β€’ Numbers economically inactive by sex β€’ Economic inactivity rates (percentages) β€’ Historical time series (2012-2025 for same quarter) β€’ Allows year-over-year comparisons lgd Employment by Local Government District (Table 1.16a) β€’ Employment statistics for all 11 NI LGDs β€’ Population 16+, employment rates, economic activity β€’ Annual data only (published separately) all All quarterly tables (excludes LGD annual data) Definitions ----------- Employed Did β‰₯1 hour paid work in reference week, or has job temporarily away from Unemployed Not employed, actively seeking work, available to start within 2 weeks Economically Inactive Not employed and not seeking work (students, retired, caring for family, long-term sick/disabled, discouraged workers, etc.) Source ------ https://www.nisra.gov.uk/statistics/labour-market-and-social-welfare """ console = Console() if table_deprecated is not None: click.echo("Warning: --table is deprecated, use --dimension instead", err=True) dimension = table_deprecated try: with console.status("[bold green]Downloading latest NISRA labour market data..."): if dimension == "all": data = { "employment": nisra_labour_market.get_latest_employment(force_refresh=force_refresh), "economic_inactivity": nisra_labour_market.get_latest_economic_inactivity( force_refresh=force_refresh ), } elif dimension == "employment": data = nisra_labour_market.get_latest_employment(force_refresh=force_refresh) elif dimension == "economic_inactivity": data = nisra_labour_market.get_latest_economic_inactivity(force_refresh=force_refresh) elif dimension == "lgd": data = nisra_labour_market.get_latest_employment_by_lgd(force_refresh=force_refresh) # Handle the result based on whether it's a single DataFrame or dict of DataFrames if dimension == "all": console.print("[green]βœ… Retrieved all dimensions successfully[/green]") total_records = sum(len(df) for df in data.values()) console.print(f"[cyan]πŸ“Š Total records: {total_records}[/cyan]") for dim_name, df in data.items(): console.print(f" β€’ {dim_name}: {len(df)} records") if not df.empty and "quarter_period" in df.columns: periods = df["quarter_period"].unique() console.print(f" [dim]Period: {periods[0]}[/dim]") elif not df.empty and "time_period" in df.columns: periods = df["time_period"].unique() console.print( f" [dim]Time series: {len(periods)} periods ({periods[0]} to {periods[-1]})[/dim]" ) else: console.print(f"[green]βœ… Retrieved {dimension} dimension successfully[/green]") console.print(f"[cyan]πŸ“Š Total records: {len(data)}[/cyan]") if not data.empty: if "quarter_period" in data.columns: periods = data["quarter_period"].unique() console.print(f"[dim]Period: {periods[0]}[/dim]") elif "time_period" in data.columns: periods = data["time_period"].unique() console.print(f"[dim]Time series: {len(periods)} periods ({periods[0]} to {periods[-1]})[/dim]") # Handle file saving if save: try: if dimension == "all": # Save each dimension to a separate file for dim_name, df in data.items(): filename = ( f"{save.rsplit('.', 1)[0]}_{dim_name}.{save.rsplit('.', 1)[-1] if '.' in save else 'csv'}" ) if output_format == "json" or filename.endswith(".json"): df.to_json(filename, orient="records", date_format="iso", indent=2) else: df.to_csv(filename, index=False) console.print(f"[green]πŸ’Ύ Saved {dim_name} to: {filename}[/green]") else: # Save single dimension if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]πŸ’Ύ Data saved to: {save}[/green]") return except PermissionError: console.print(f"[red]❌ Error: Permission denied writing to {save}[/red]") console.print("[yellow]πŸ’‘ Check file permissions or choose a different location[/yellow]") return except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return # Output to console in requested format if output_format == "json": import json if dimension == "all": # Convert DataFrames to JSON-serializable format output = {dim_name: df.to_dict(orient="records") for dim_name, df in data.items()} click.echo(json.dumps(output, indent=2, default=str)) else: click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: # csv format if dimension == "all": console.print("\n[yellow]πŸ’‘ Tip: For 'all' dimensions, use --save to export to files[/yellow]") console.print("[yellow] Displaying employment dimension only:[/yellow]\n") click.echo(data["employment"].to_csv(index=False)) else: click.echo(data.to_csv(index=False)) except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]πŸ’‘ Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") console.print(" β€’ Visit NISRA website to verify data availability") raise click.Abort() from e
@nisra.command(name="births") @click.option( "--event-type", type=click.Choice(["registration", "occurrence", "both"], case_sensitive=False), default="both", help="Event type: registration (when registered), occurrence (when born), or both (default: both)", ) @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)")
[docs] def nisra_births_cmd(event_type, output_format, force_refresh, save): """NISRA Monthly Birth Registrations Statistics. Retrieves monthly birth registration data for Northern Ireland including: - Births by month of registration (when officially registered) - Births by month of occurrence (when actually born) - Breakdown by sex (Persons, Male, Female) Birth registration data are based on mother's residence at time of birth. Most births are registered within 42 days in Northern Ireland. Examples: --------- Get latest births by registration date:: bolster nisra births --event-type registration Get latest births by occurrence (actual birth date):: bolster nisra births --event-type occurrence Get both registration and occurrence data:: bolster nisra births --event-type both Save registration data to file:: bolster nisra births --event-type registration --save births_reg.csv Get data as JSON:: bolster nisra births --event-type both --format json Force refresh cached data:: bolster nisra births --force-refresh Notes: ------ - Monthly time series from 2006 to present - Final data for years up to and including 2024 - Provisional and subject to change for current year - Registration data lags occurrence data by ~1-2 months - COVID-19 Note: April-May 2020 registration data disrupted by lockdown (registration offices closed), but occurrence data remains normal Event Types ----------- registration Births by month they were officially registered β€’ Reflects administrative processing dates β€’ Can be affected by office closures (e.g., COVID-19) β€’ Latest data may be 1-2 months more recent than occurrence occurrence Births by month they actually occurred β€’ Reflects actual birth dates β€’ More stable measure of birth patterns β€’ Limited to births already registered both Returns both registration and occurrence data β€’ Useful for comparing registration patterns vs birth patterns β€’ Helps identify registration delays/backlogs Returns: -------- DataFrame - month: First day of month (datetime) - sex: Persons (total), Male, or Female - births: Number of births Source ------ https://www.nisra.gov.uk/statistics/births-deaths-and-marriages/births """ console = Console() try: with console.status("[bold green]Downloading latest NISRA births data..."): data = nisra_births.get_latest_births(event_type=event_type, force_refresh=force_refresh) # Handle the result based on whether it's a single DataFrame or dict of DataFrames if event_type == "both": console.print("[green]βœ… Retrieved both event types successfully[/green]") total_records = sum(len(df) for df in data.values()) console.print(f"[cyan]πŸ“Š Total records: {total_records}[/cyan]") for event_name, df in data.items(): console.print(f" β€’ {event_name}: {len(df)} records") if not df.empty: latest_month = df["month"].max() earliest_month = df["month"].min() console.print( f" [dim]Period: {earliest_month.strftime('%b %Y')} to {latest_month.strftime('%b %Y')}[/dim]" ) else: console.print(f"[green]βœ… Retrieved {event_type} data successfully[/green]") console.print(f"[cyan]πŸ“Š Total records: {len(data)}[/cyan]") if not data.empty: latest_month = data["month"].max() earliest_month = data["month"].min() console.print( f"[dim]Period: {earliest_month.strftime('%b %Y')} to {latest_month.strftime('%b %Y')}[/dim]" ) # Handle file saving if save: try: if event_type == "both": # Save each event type to a separate file for event_name, df in data.items(): filename = ( f"{save.rsplit('.', 1)[0]}_{event_name}.{save.rsplit('.', 1)[-1] if '.' in save else 'csv'}" ) if output_format == "json" or filename.endswith(".json"): df.to_json(filename, orient="records", date_format="iso", indent=2) else: df.to_csv(filename, index=False) console.print(f"[green]πŸ’Ύ Saved {event_name} to: {filename}[/green]") else: # Save single event type if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]πŸ’Ύ Data saved to: {save}[/green]") return except PermissionError: console.print(f"[red]❌ Error: Permission denied writing to {save}[/red]") console.print("[yellow]πŸ’‘ Check file permissions or choose a different location[/yellow]") return except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return # Output to console in requested format if output_format == "json": import json if event_type == "both": # Convert DataFrames to JSON-serializable format output = {event_name: df.to_dict(orient="records") for event_name, df in data.items()} click.echo(json.dumps(output, indent=2, default=str)) else: click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: # CSV output if event_type == "both": for event_name, df in data.items(): console.print(f"\n[bold]{event_name.upper()}:[/bold]") console.print(df.to_csv(index=False), end="") else: console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]πŸ’‘ Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") console.print(" β€’ Visit NISRA website to verify data availability") raise click.Abort() from e
@nisra.command(name="population") @click.option( "--area", type=click.Choice( ["all", "Northern Ireland", "Parliamentary Constituencies (2024)", "Health and Social Care Trusts"], case_sensitive=False, ), default="Northern Ireland", help="Geographic area (default: Northern Ireland)", ) @click.option( "--year", type=int, help="Specific year to retrieve (leave blank for all years)", ) @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)")
[docs] def nisra_population_cmd(area, year, output_format, force_refresh, save): """NISRA Mid-Year Population Estimates. Retrieves annual mid-year population estimates for Northern Ireland with breakdowns by: - Geography (NI overall, Parliamentary Constituencies, Health and Social Care Trusts) - Sex (All persons, Males, Females) - Age (5-year age bands: 00-04, 05-09, ..., 90+) - Year (1971-present for NI overall, 2021-present for sub-geographies) Mid-year estimates are referenced to June 30th of each year. Examples: --------- Get latest NI overall population:: bolster nisra population Get all geographic areas:: bolster nisra population --area all Get specific year:: bolster nisra population --year 2024 Get Parliamentary Constituencies:: bolster nisra population --area "Parliamentary Constituencies (2024)" Save to file:: bolster nisra population --save population.csv Get as JSON:: bolster nisra population --format json Notes: ------ - Published annually ~6 months after reference date - Reference date: June 30th of each year - Historical data for NI overall from 1971 - Sub-geography data from 2021 onwards - Age bands: 5-year groups (00-04, 05-09, ..., 85-89, 90+) - Also includes custom age bands and broad age groups Geographic Areas ---------------- Northern Ireland NI overall (1971-present) Parliamentary Constituencies (2024) 2024 parliamentary constituencies (2021-present) Health and Social Care Trusts Health & Social Care Trusts (2021-present) all All geographic breakdowns Returns: -------- DataFrame - area, area_code, area_name: Geographic identifiers - year: Reference year (mid-year estimate as of June 30th) - sex: All persons, Males, or Females - age_5: 5-year age band - age_band, age_broad: Alternative age groupings - population: Mid-year estimate Source ------ https://www.nisra.gov.uk/statistics/people-and-communities/population """ console = Console() try: with console.status("[bold green]Downloading latest NISRA population estimates..."): data = nisra_population.get_latest_population(area=area, force_refresh=force_refresh) # Filter by year if specified if year: data = data[data["year"] == year] if data.empty: console.print(f"[red]❌ No data found for year {year}[/red]") return console.print("[green]βœ… Retrieved population estimates successfully[/green]") console.print(f"[cyan]πŸ“Š Total records: {len(data)}[/cyan]") if not data.empty: years = sorted(data["year"].unique()) console.print(f"[dim]Years: {years[0]} to {years[-1]} ({len(years)} years)[/dim]") # Show total population for latest year if NI overall if area == "Northern Ireland": latest_year = data["year"].max() total_pop = data[(data["year"] == latest_year) & (data["sex"] == "All persons")]["population"].sum() console.print(f"[dim]{latest_year} NI population: {total_pop:,}[/dim]") # Handle file saving if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]πŸ’Ύ Data saved to: {save}[/green]") return except PermissionError: console.print(f"[red]❌ Error: Permission denied writing to {save}[/red]") console.print("[yellow]πŸ’‘ Check file permissions or choose a different location[/yellow]") return except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return # Output to console in requested format if output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: # CSV output console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]πŸ’‘ Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") console.print(" β€’ Visit NISRA website to verify data availability") raise click.Abort() from e
@nisra.command(name="population-projections") @click.option("--lgd", is_flag=True, default=False, help="Show LGD sub-area projections instead of NI-level") @click.option("--lgd-name", default=None, help="Filter LGD projections to a specific LGD name or code") @click.option("--start-year", type=int, default=None, help="Filter projections from this year (inclusive)") @click.option("--end-year", type=int, default=None, help="Filter projections to this year (inclusive)") @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, default=False, help="Bypass cache and download fresh data") @click.option("--save", type=click.Path(), default=None, help="Save output to file")
[docs] def nisra_population_projections_cmd(lgd, lgd_name, start_year, end_year, output_format, force_refresh, save): r"""NISRA Population Projections for Northern Ireland. By default returns NI-level principal projections (2024-based, to 2074). Use --lgd to get LGD sub-area projections (2022-based, 11 districts, to 2047). \b Examples: bolster nisra population-projections bolster nisra population-projections --start-year 2025 --end-year 2035 bolster nisra population-projections --lgd bolster nisra population-projections --lgd --lgd-name Belfast bolster nisra population-projections --lgd --lgd-name N09000003 """ from rich.console import Console console = Console() try: if lgd: data = nisra_projections.get_lgd_projections( lgd=lgd_name, start_year=start_year, end_year=end_year, force_refresh=force_refresh, ) console.print( f"[bold]NI LGD Population Projections (2022-based)[/bold] β€” " f"{data['lgd_name'].nunique()} LGDs, " f"{data['year'].min()}–{data['year'].max()}" ) else: data = nisra_projections.get_latest_projections( start_year=start_year, end_year=end_year, force_refresh=force_refresh, ) base = data["base_year"].iloc[0] if not data.empty else "?" console.print( f"[bold]NI Population Projections ({base}-based)[/bold] β€” {data['year'].min()}–{data['year'].max()}" ) if save: try: if output_format == "json": data.to_json(save, orient="records", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]βœ“ Saved to {save}[/green]") return except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return if output_format == "json": click.echo(data.to_json(orient="records", indent=2)) else: console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") raise click.Abort() from e
@nisra.command(name="marriages") @click.option("--year", type=int, help="Filter data for specific year") @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)")
[docs] def nisra_marriages_cmd(year, output_format, force_refresh, save): """NISRA Monthly Marriage Registrations Statistics. Retrieves monthly marriage registration data for Northern Ireland. Marriage registrations represent when the marriage was registered, not when the ceremony occurred. The data is published monthly with provisional figures for the current year and final figures for previous years. Examples: --------- Get latest marriages data:: bolster nisra marriages Filter for a specific year:: bolster nisra marriages --year 2024 Save to file:: bolster nisra marriages --save marriages.csv Get data as JSON:: bolster nisra marriages --format json Force refresh cached data:: bolster nisra marriages --force-refresh Notes: ------ - Monthly time series from 2006 to present - Final data for years up to and including 2024 - Provisional and subject to change for current year - COVID-19 Note: 2020 shows dramatic impact on marriages (April: 14, May: 4 marriages during strict lockdown) Seasonal Patterns ----------------- Summer months (June-September) are peak wedding season: β€’ August typically has the highest number of marriages β€’ June-September account for ~40% of annual marriages β€’ January-February typically have the lowest numbers Returns: -------- DataFrame - date: First day of month (datetime) - year: Year of registration - month: Month name - marriages: Number of marriage registrations Source ------ https://www.nisra.gov.uk/statistics/births-deaths-and-marriages/marriages """ console = Console() try: with console.status("[bold green]Downloading latest NISRA marriages data..."): data = nisra_marriages.get_latest_marriages(force_refresh=force_refresh) # Filter by year if specified if year: data = nisra_marriages.get_marriages_by_year(data, year) if data.empty: console.print(f"[yellow]⚠️ No data found for year {year}[/yellow]") return console.print("[green]βœ… Retrieved marriages data successfully[/green]") console.print(f"[cyan]πŸ“Š Total records: {len(data)}[/cyan]") if not data.empty: earliest_date = data["date"].min() latest_date = data["date"].max() console.print(f"[dim]Period: {earliest_date.strftime('%b %Y')} to {latest_date.strftime('%b %Y')}[/dim]") # Show summary statistics total_marriages = data["marriages"].sum() avg_per_month = data["marriages"].mean() console.print("\n[bold]Summary:[/bold]") if year: console.print(f" Total marriages in {year}: {total_marriages:,.0f}") console.print(f" Average per month: {avg_per_month:,.0f}") else: years_available = data["year"].nunique() console.print(f" Years available: {years_available}") console.print(f" Total marriages (all years): {total_marriages:,.0f}") console.print(f" Average per month: {avg_per_month:,.0f}") # Handle file saving if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]πŸ’Ύ Data saved to: {save}[/green]") return except PermissionError: console.print(f"[red]❌ Error: Permission denied writing to {save}[/red]") console.print("[yellow]πŸ’‘ Check file permissions or choose a different location[/yellow]") return except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return # Output to console in requested format if output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: # CSV output console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]πŸ’‘ Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") console.print(" β€’ Visit NISRA website to verify data availability") raise click.Abort() from e
@nisra.command(name="civil-partnerships") @click.option("--latest", is_flag=True, help="Get the most recent civil partnerships data") @click.option("--year", type=int, help="Filter data for specific year") @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)") @click.option("--summary", is_flag=True, help="Show summary statistics only")
[docs] def nisra_civil_partnerships_cmd(latest, year, output_format, force_refresh, save, summary): """NISRA Monthly Civil Partnership Registrations Statistics. Retrieves monthly civil partnership registration data for Northern Ireland. Civil partnerships became legal in Northern Ireland in December 2005. The data is published monthly with provisional figures for the current year and final figures for previous years. Examples: Get latest civil partnerships data:: bolster nisra civil-partnerships --latest Filter for a specific year:: bolster nisra civil-partnerships --latest --year 2024 Show annual summary:: bolster nisra civil-partnerships --latest --summary Save to file:: bolster nisra civil-partnerships --latest --save civil_partnerships.csv Data Notes: - Monthly time series from 2006 to present - Typically 80-120 civil partnerships per year - Numbers generally lower than marriages (5-10 per month average) - COVID-19 Note: 2020-2021 shows reduced registrations Returns: DataFrame with columns: - date: First day of month (datetime) - year: Year of registration - month: Month name - civil_partnerships: Number of civil partnership registrations Note: Source: https://www.nisra.gov.uk/statistics/births-deaths-and-marriages/civil-partnerships """ console = Console() if not latest and not summary: console.print("[yellow]Use --latest to retrieve data or --summary for statistics[/yellow]") return try: with console.status("[bold green]Downloading latest NISRA civil partnerships data..."): data = nisra_marriages.get_latest_civil_partnerships(force_refresh=force_refresh) # Filter by year if specified if year: data = nisra_marriages.get_civil_partnerships_by_year(data, year) if data.empty: console.print(f"[yellow]No data found for year {year}[/yellow]") return # Summary mode if summary: console.print("\n[bold cyan]Civil Partnerships Summary[/bold cyan]") console.print("=" * 45) yearly_summary = nisra_marriages.get_civil_partnerships_summary_by_year(data) console.print(f"\n{'Year':<8} {'Total':>8} {'Avg/Month':>10} {'Months':>8}") console.print("-" * 38) for _, row in yearly_summary.tail(10).iterrows(): console.print( f"{int(row['year']):<8} {int(row['total_civil_partnerships']):>8} " f"{row['avg_per_month']:>10.1f} {int(row['months_reported']):>8}" ) return console.print("[green]Retrieved civil partnerships data successfully[/green]") console.print(f"[cyan]Total records: {len(data)}[/cyan]") if not data.empty: earliest_date = data["date"].min() latest_date = data["date"].max() console.print(f"[dim]Period: {earliest_date.strftime('%b %Y')} to {latest_date.strftime('%b %Y')}[/dim]") total = data["civil_partnerships"].sum() avg_per_month = data["civil_partnerships"].mean() console.print("\n[bold]Summary:[/bold]") if year: console.print(f" Total civil partnerships in {year}: {total}") else: years_available = data["year"].nunique() console.print(f" Years available: {years_available}") console.print(f" Total civil partnerships: {total}") console.print(f" Average per month: {avg_per_month:.1f}") # Handle file saving if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]Data saved to: {save}[/green]") return except Exception as e: console.print(f"[red]Error saving file: {e}[/red]") return # Output to console if output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") console.print(" β€’ Visit NISRA website to verify data availability") raise click.Abort() from e
@nisra.command(name="occupancy") @click.option("--latest", is_flag=True, help="Get the most recent occupancy data") @click.option("--year", type=int, help="Filter data for specific year") @click.option( "--accommodation", type=click.Choice(["hotel", "ssa", "combined"], case_sensitive=False), default="hotel", help="Accommodation type: hotel, ssa (B&Bs/guest houses), or combined", ) @click.option( "--data-type", type=click.Choice(["rates", "sold"], case_sensitive=False), default="rates", help="Data type: rates (occupancy rates) or sold (rooms/beds sold)", ) @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)") @click.option("--summary", is_flag=True, help="Show summary statistics only") @click.option("--compare", is_flag=True, help="Compare hotel vs SSA occupancy by year")
[docs] def nisra_occupancy_cmd(latest, year, accommodation, data_type, output_format, force_refresh, save, summary, compare): """NISRA Monthly Accommodation Occupancy Statistics. Retrieves monthly occupancy data for Northern Ireland from the Tourism Statistics Branch. Supports both hotel and SSA (Small Service Accommodation - B&Bs, guest houses) data. Parameters ---------- accommodation : str Type of accommodation: ``hotel`` (Hotels, 2011-present, ~65% avg occupancy), ``ssa`` (Small Service Accommodation: B&Bs/guest houses, 2013-present, ~33% avg), or ``combined`` (both types with accommodation_type column). data_type : str Data to retrieve: ``rates`` (room and bed occupancy rates, 0-1 scale) or ``sold`` (number of rooms and beds sold monthly). .. rubric:: Examples Get latest hotel occupancy rates (default):: bolster nisra occupancy --latest Get SSA (B&B/guest house) occupancy:: bolster nisra occupancy --latest --accommodation ssa Get combined data with accommodation type:: bolster nisra occupancy --latest --accommodation combined Compare hotel vs SSA by year:: bolster nisra occupancy --compare Get rooms/beds sold data:: bolster nisra occupancy --latest --data-type sold Filter for a specific year:: bolster nisra occupancy --latest --year 2024 Show summary by year:: bolster nisra occupancy --latest --summary Save to file:: bolster nisra occupancy --latest --save occupancy.csv .. rubric:: Notes - Hotel data: 2011-present (~65% average room occupancy) - SSA data: 2013-present (~33% average room occupancy) - Room occupancy is typically higher than bed occupancy - COVID-19 Note: 2020-2021 shows dramatic impact on tourism (all accommodation closed March-July 2020, Oct-Dec 2020, Jan-May 2021) .. rubric:: Seasonal Patterns Summer months (June-September) are peak tourism season: - August typically has the highest occupancy (~80% hotel, ~55% SSA) - July-September are consistently strong - January-February typically have the lowest occupancy .. rubric:: Returns DataFrame (rates): - date: First day of month (datetime) - year: Year - month: Month name - room_occupancy: Room occupancy rate (0-1) - bed_occupancy: Bed occupancy rate (0-1) - accommodation_type: (combined only) 'hotel' or 'ssa' DataFrame (sold): - date: First day of month (datetime) - year: Year - month: Month name - rooms_sold: Number of rooms sold - beds_sold: Number of beds sold .. rubric:: Source https://www.nisra.gov.uk/statistics/tourism/occupancy-surveys """ console = Console() if not latest and not summary and not compare: console.print("[yellow]Use --latest to retrieve data, --summary for statistics, or --compare[/yellow]") return try: # Handle comparison mode if compare: with console.status("[bold green]Downloading hotel and SSA occupancy data..."): combined = nisra_occupancy.get_combined_occupancy(force_refresh=force_refresh) console.print("\n[bold cyan]Hotel vs SSA Occupancy Comparison[/bold cyan]") console.print("=" * 60) comparison = nisra_occupancy.compare_accommodation_types(combined) # Exclude COVID years from summary non_covid = comparison[~comparison["year"].isin([2020, 2021])] console.print(f"\n{'Year':<8} {'Hotel':>12} {'SSA':>12} {'Difference':>12}") console.print("-" * 48) for _, row in comparison.tail(10).iterrows(): hotel_pct = f"{row['hotel_room_occupancy']:.1%}" if row["hotel_room_occupancy"] else "N/A" ssa_pct = f"{row['ssa_room_occupancy']:.1%}" if row["ssa_room_occupancy"] else "N/A" diff = row["difference"] diff_str = f"{diff:+.1%}" if diff else "N/A" console.print(f"{int(row['year']):<8} {hotel_pct:>12} {ssa_pct:>12} {diff_str:>12}") # Average summary avg_hotel = non_covid["hotel_room_occupancy"].mean() avg_ssa = non_covid["ssa_room_occupancy"].mean() avg_diff = non_covid["difference"].mean() console.print("\n[bold]Average (excl. COVID years):[/bold]") console.print(f" Hotel: {avg_hotel:.1%}") console.print(f" SSA: {avg_ssa:.1%}") console.print(f" Difference: {avg_diff:+.1%}") return # Determine accommodation type and status message acc_label = { "hotel": "hotel", "ssa": "SSA (B&B/guest house)", "combined": "combined hotel and SSA", }[accommodation] with console.status(f"[bold green]Downloading latest NISRA {acc_label} occupancy data..."): if accommodation == "combined": if data_type == "sold": console.print("[yellow]Note: Combined mode only supports rates, not sold data[/yellow]") data = nisra_occupancy.get_combined_occupancy(force_refresh=force_refresh) elif accommodation == "ssa": if data_type == "sold": data = nisra_occupancy.get_latest_ssa_rooms_beds_sold(force_refresh=force_refresh) else: data = nisra_occupancy.get_latest_ssa_occupancy(force_refresh=force_refresh) else: # hotel (default) if data_type == "sold": data = nisra_occupancy.get_latest_rooms_beds_sold(force_refresh=force_refresh) else: data = nisra_occupancy.get_latest_hotel_occupancy(force_refresh=force_refresh) # Filter by year if specified if year: data = nisra_occupancy.get_occupancy_by_year(data, year) if data.empty: console.print(f"[yellow]No data found for year {year}[/yellow]") return # Summary mode if summary: console.print(f"\n[bold cyan]{acc_label.title()} Occupancy Summary[/bold cyan]") console.print("=" * 50) if data_type == "rates": yearly_summary = nisra_occupancy.get_occupancy_summary_by_year(data) console.print(f"\n{'Year':<8} {'Room Occ':>10} {'Bed Occ':>10} {'Months':>8}") console.print("-" * 40) for _, row in yearly_summary.tail(10).iterrows(): room_pct = f"{row['avg_room_occupancy']:.1%}" if row["avg_room_occupancy"] else "N/A" bed_pct = f"{row['avg_bed_occupancy']:.1%}" if row["avg_bed_occupancy"] else "N/A" console.print( f"{int(row['year']):<8} {room_pct:>10} {bed_pct:>10} {int(row['months_reported']):>8}" ) # Seasonal patterns console.print("\n[bold]Seasonal Patterns (All Years):[/bold]") seasonal = nisra_occupancy.get_seasonal_patterns(data) peak = seasonal.loc[seasonal["avg_room_occupancy"].idxmax()] low = seasonal.loc[seasonal["avg_room_occupancy"].idxmin()] console.print(f" Peak month: {peak['month']} ({peak['avg_room_occupancy']:.1%})") console.print(f" Low month: {low['month']} ({low['avg_room_occupancy']:.1%})") else: # Rooms/beds sold summary yearly = ( data.groupby("year") .agg( total_rooms=("rooms_sold", "sum"), total_beds=("beds_sold", "sum"), months=("rooms_sold", lambda x: x.notna().sum()), ) .reset_index() ) console.print(f"\n{'Year':<8} {'Rooms Sold':>14} {'Beds Sold':>14} {'Months':>8}") console.print("-" * 48) for _, row in yearly.tail(10).iterrows(): console.print( f"{int(row['year']):<8} {row['total_rooms']:>14,.0f} " f"{row['total_beds']:>14,.0f} {int(row['months']):>8}" ) return console.print(f"[green]Retrieved {acc_label} occupancy data successfully[/green]") console.print(f"[cyan]Total records: {len(data)}[/cyan]") if not data.empty: earliest_date = data["date"].min() latest_date = data["date"].max() console.print(f"[dim]Period: {earliest_date.strftime('%b %Y')} to {latest_date.strftime('%b %Y')}[/dim]") # Show quick stats if data_type == "rates": avg_room = data["room_occupancy"].mean() avg_bed = data["bed_occupancy"].mean() console.print("\n[bold]Average Occupancy:[/bold]") console.print(f" Room: {avg_room:.1%}") console.print(f" Bed: {avg_bed:.1%}") else: total_rooms = data["rooms_sold"].sum() total_beds = data["beds_sold"].sum() console.print("\n[bold]Totals:[/bold]") console.print(f" Rooms sold: {total_rooms:,.0f}") console.print(f" Beds sold: {total_beds:,.0f}") # Handle file saving if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]Data saved to: {save}[/green]") return except PermissionError: console.print(f"[red]Error: Permission denied writing to {save}[/red]") return except Exception as e: console.print(f"[red]Error saving file: {e}[/red]") return # Output to console in requested format if output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") console.print(" β€’ Visit NISRA website to verify data availability") raise click.Abort() from e
@nisra.command(name="visitors") @click.option("--latest", is_flag=True, help="Get the most recent visitor statistics") @click.option( "--market", type=click.Choice( ["all", "gb", "europe", "north-america", "overseas", "roi", "ni", "total"], case_sensitive=False, ), default="all", help="Filter by visitor origin market", ) @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)") @click.option("--summary", is_flag=True, help="Show market summary with derived metrics") @click.option("--compare", is_flag=True, help="Compare domestic vs external visitors")
[docs] def nisra_visitors_cmd(latest, market, output_format, force_refresh, save, summary, compare): """NISRA Quarterly Visitor Statistics (Trips, Nights, Expenditure). Retrieves quarterly visitor statistics for Northern Ireland from NISRA, showing overnight trips, nights spent, and visitor expenditure by market origin. Args: latest: Get the most recent visitor statistics data market: Filter by visitor market (gb, europe, north-america, overseas, roi, ni, total, all) output_format: Output format (csv or json) force_refresh: Force re-download even if cached save: Save data to file (specify filename) summary: Show summary statistics only compare: Compare domestic vs external visitors Markets: * gb - Great Britain (largest external market) * europe - Other Europe (excluding GB and ROI) * north-america - North America (USA and Canada) * overseas - Other overseas (Asia, Oceania, etc.) * roi - Republic of Ireland * ni - NI Residents (domestic tourism) * total - All markets combined * all - Show all markets (default) Examples: Get latest visitor statistics:: bolster nisra visitors --latest Show market summary with per-trip metrics:: bolster nisra visitors --latest --summary Compare domestic vs external visitors:: bolster nisra visitors --latest --compare Get Great Britain visitors only:: bolster nisra visitors --latest --market gb Save to file:: bolster nisra visitors --latest --save visitors.csv Data Insights: - GB is typically the largest external market (~30% of trips, ~38% of spend) - ROI visitors growing rapidly (up 32% trips, 68% spend YoY in 2025) - NI residents account for ~31% of trips but only ~17% of expenditure - External visitors spend 2-4x more per trip than NI residents - "Other Overseas" (long-haul) has highest spend per trip (~Β£540) Note: Source: https://www.nisra.gov.uk/publications/quarterly-tourism-statistics-publications """ from rich.console import Console from rich.table import Table console = Console() try: if not latest and not save: console.print("[yellow]Hint: Use --latest to fetch data, or --save to save to file[/yellow]") return with console.status("[bold green]Downloading NISRA visitor statistics..."): data = nisra_visitors.get_latest_visitor_statistics(force_refresh=force_refresh) # Market name mapping for filter market_map = { "gb": "Great Britain", "europe": "Other Europe", "north-america": "North America", "overseas": "Other Overseas", "roi": "Republic of Ireland", "ni": "NI Residents", "total": "Total", } if compare: # Show domestic vs external comparison comparison = nisra_visitors.get_domestic_vs_external(data) console.print("\n[bold cyan]Domestic vs External Visitors[/bold cyan]\n") table = Table(show_header=True, header_style="bold") table.add_column("Category") table.add_column("Trips", justify="right") table.add_column("% Trips", justify="right") table.add_column("Expenditure", justify="right") table.add_column("% Spend", justify="right") for _, row in comparison.iterrows(): table.add_row( row["category"], f"{row['trips']:,.0f}", f"{row['trips_pct']:.1f}%", f"Β£{row['expenditure']:.1f}M", f"{row['expenditure_pct']:.1f}%", ) console.print(table) # Additional insights ni_row = comparison[comparison["category"] == "Domestic (NI)"].iloc[0] ext_row = comparison[comparison["category"] == "External"].iloc[0] if ni_row["trips"] > 0 and ext_row["trips"] > 0: ni_spend_per = ni_row["expenditure"] * 1_000_000 / ni_row["trips"] ext_spend_per = ext_row["expenditure"] * 1_000_000 / ext_row["trips"] console.print( f"\n[dim]Spend per trip: Domestic Β£{ni_spend_per:.0f} vs External Β£{ext_spend_per:.0f}[/dim]" ) return if summary: # Show market summary with derived metrics market_summary = nisra_visitors.get_market_summary(data) console.print("\n[bold cyan]Visitor Statistics by Market[/bold cyan]\n") table = Table(show_header=True, header_style="bold") table.add_column("Market") table.add_column("Trips", justify="right") table.add_column("% Share", justify="right") table.add_column("Expenditure", justify="right") table.add_column("Β£/Trip", justify="right") table.add_column("Nights/Trip", justify="right") for _, row in market_summary.iterrows(): table.add_row( row["market"], f"{row['trips']:,.0f}", f"{row['trips_pct']:.1f}%", f"Β£{row['expenditure']:.1f}M", f"Β£{row['expenditure_per_trip']:.0f}", f"{row['nights_per_trip']:.1f}", ) console.print(table) # Total stats total = nisra_visitors.get_total_visitor_statistics(data) if total is not None: console.print("\n[bold]Totals:[/bold]") console.print(f" Trips: {total['trips']:,.0f}") console.print(f" Nights: {total['nights']:,.0f}") console.print(f" Expenditure: Β£{total['expenditure']:.1f}M") return # Filter by market if specified if market != "all": market_name = market_map.get(market.lower()) if market_name: data = data[data["market"] == market_name] console.print("[green]Retrieved visitor statistics successfully[/green]") console.print(f"[dim]Period: {data['period'].iloc[0]} to {data['year'].iloc[0]}[/dim]") # Show quick stats total = data[data["market"] == "Total"] if not total.empty: t = total.iloc[0] console.print("\n[bold]Total Visitors:[/bold]") console.print(f" Trips: {t['trips']:,.0f}") console.print(f" Nights: {t['nights']:,.0f}") console.print(f" Expenditure: Β£{t['expenditure']:.1f}M") # Handle file saving if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]Data saved to: {save}[/green]") return except PermissionError: console.print(f"[red]Error: Permission denied writing to {save}[/red]") return except Exception as e: console.print(f"[red]Error saving file: {e}[/red]") return # Output to console in requested format if output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") console.print(" β€’ Visit NISRA website to verify data availability") raise click.Abort() from e
@nisra.command(name="migration") @click.option("--year", type=int, help="Filter data for specific year") @click.option("--start-year", type=int, help="Start year for summary statistics") @click.option("--end-year", type=int, help="End year for summary statistics") @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)") @click.option("--summary", is_flag=True, help="Show summary statistics only")
[docs] def nisra_migration_cmd(year, start_year, end_year, output_format, force_refresh, save, summary): """NISRA Migration Estimates (Derived from Demographic Components). Calculates net migration using the demographic accounting equation: Net Migration = Population Change - Natural Change Net Migration = Ξ”Population - (Births - Deaths) This combines data from: - Mid-year population estimates - Monthly birth registrations (occurrence data) - Historical death registrations The demographic equation must hold: Pop(t+1) = Pop(t) + Births - Deaths + Migration Args: year: Filter data for specific year start_year: Start year for data range end_year: End year for data range output_format: Output format (csv or json) force_refresh: Force re-download even if cached save: Save data to file (specify filename) summary: Show summary statistics only Examples: Get all migration data:: bolster nisra migration Filter for specific year:: bolster nisra migration --year 2024 Show summary statistics for 2010-2024:: bolster nisra migration --start-year 2010 --summary Save to file:: bolster nisra migration --save migration.csv Get data as JSON:: bolster nisra migration --format json Force refresh all source data:: bolster nisra migration --force-refresh Data Notes: - Coverage: 2011-2024 (limited by historical deaths data) - Derived migration includes net effect of international and internal migration - Also captures measurement error and timing differences between sources - Demographic equation validated for all years (Ξ”Pop = Births - Deaths + Migration) Key Findings: - 2023: Highest net immigration (+7,225) - 2024: Strong immigration continues (+6,107) - 2013: Highest net emigration (-2,124) - Average 2011-2024: +2,082 per year - 9 years with net immigration, 5 with net emigration Returns: DataFrame with columns: - year: Year - population_start, population_end: Mid-year population estimates - births, deaths: Annual totals - natural_change: Births - Deaths - population_change: Year-over-year change - net_migration: Derived migration estimate - migration_rate: Per 1,000 population Note: Combines three NISRA data sources: - Population: https://www.nisra.gov.uk/statistics/people-and-communities/population - Births: https://www.nisra.gov.uk/statistics/births-deaths-and-marriages/births - Deaths: https://www.nisra.gov.uk/statistics/births-deaths-and-marriages/deaths """ console = Console() try: with console.status("[bold green]Calculating migration estimates from demographic components..."): data = nisra_migration.get_latest_migration(force_refresh=force_refresh) # Filter by year if specified if year: data = nisra_migration.get_migration_by_year(data, year) if data.empty: console.print(f"[yellow]⚠️ No data found for year {year}[/yellow]") return console.print("[green]βœ… Migration estimates calculated successfully[/green]") console.print(f"[cyan]πŸ“Š Total years: {len(data)}[/cyan]") if not data.empty: earliest_year = data["year"].min() latest_year = data["year"].max() console.print(f"[dim]Period: {earliest_year} to {latest_year}[/dim]") # Show validation console.print("\n[bold]Validation:[/bold]") try: nisra_migration.validate_demographic_equation(data) console.print(" βœ“ Demographic equation validated (Ξ”Pop = Births - Deaths + Migration)") except Exception as e: console.print(f" [red]βœ— Validation failed: {e}[/red]") # Show summary statistics if requested if summary: stats = nisra_migration.get_migration_summary_statistics(data, start_year=start_year, end_year=end_year) period = f"{start_year or data['year'].min()}-{end_year or data['year'].max()}" console.print(f"\n[bold]Summary Statistics ({period}):[/bold]") console.print(f" Total years: {stats['total_years']}") console.print(f" Average net migration: {stats['avg_net_migration']:+,.0f}") console.print(f" Average migration rate: {stats['avg_migration_rate']:+.2f} per 1,000") console.print(f" Years with net immigration: {stats['positive_years']}") console.print(f" Years with net emigration: {stats['negative_years']}") console.print(f"\n Peak immigration: {stats['max_immigration_year']} ({stats['max_immigration']:+,})") console.print(f" Peak emigration: {stats['max_emigration_year']} ({stats['max_emigration']:+,})") if not save: return # Don't output data if only showing summary # Handle file saving if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]πŸ’Ύ Data saved to: {save}[/green]") return except PermissionError: console.print(f"[red]❌ Error: Permission denied writing to {save}[/red]") console.print("[yellow]πŸ’‘ Check file permissions or choose a different location[/yellow]") return except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return # Output to console in requested format if output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: # CSV output console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]πŸ’‘ Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") console.print(" β€’ Ensure births, deaths, and population data are available") raise click.Abort() from e
@nisra.command(name="index-of-services") @click.option("--year", type=int, help="Filter data for specific year") @click.option("--quarter", help="Filter data for specific quarter (e.g., 'Q1', 'Q2')") @click.option("--start-year", type=int, help="Start year for summary statistics") @click.option("--end-year", type=int, help="End year for summary statistics") @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)") @click.option("--summary", is_flag=True, help="Show summary statistics only") @click.option("--growth", is_flag=True, help="Include year-on-year growth rates")
[docs] def nisra_index_of_services_cmd( year, quarter, start_year, end_year, output_format, force_refresh, save, summary, growth ): """NISRA Index of Services (IOS) - Quarterly Economic Indicator. Measures output in Northern Ireland's services sector, including business services, wholesale/retail trade, transport, and other services. Seasonally adjusted quarterly data from Q1 2005 to present. Args: year: Filter by specific year quarter: Filter by specific quarter (Q1, Q2, Q3, Q4) start_year: Start year for filtering end_year: End year for filtering output_format: Output format (csv or json) force_refresh: Force re-download even if cached save: Save data to file (specify filename) summary: Show summary statistics only growth: Include year-on-year growth rates Examples: Get all Index of Services data:: bolster nisra index-of-services Filter for specific year:: bolster nisra index-of-services --year 2024 Get specific quarter:: bolster nisra index-of-services --year 2025 --quarter Q3 Show summary statistics for 2020-2025:: bolster nisra index-of-services --start-year 2020 --summary Include year-on-year growth rates:: bolster nisra index-of-services --growth Save to file:: bolster nisra index-of-services --save ios.csv Data Notes: - Coverage: Q1 2005 - Q3 2025 (quarterly) - Seasonally adjusted values - Index values (100 = base period) - Includes NI and UK comparator data - Published ~3 months after quarter end Returns: DataFrame with columns: - date: First day of quarter - quarter: Quarter code (Q1, Q2, Q3, Q4) - year: Year - ni_index: Northern Ireland services index value - uk_index: UK services index value - ni_growth_rate: YoY % change (if --growth specified) - uk_growth_rate: YoY % change (if --growth specified) Note: Source: NISRA Economic & Labour Market Statistics Branch https://www.nisra.gov.uk/statistics/economic-output/index-services """ console = Console() try: with console.status("[bold green]Fetching Index of Services data..."): data = nisra_ios.get_latest_ios(force_refresh=force_refresh) # Add growth rates if requested if growth: data = nisra_ios.get_ios_growth(data) # Filter by year and/or quarter if specified if year: data = nisra_ios.get_ios_by_year(data, year) if quarter: quarter_num = int(quarter.lstrip("Qq")) data = nisra_ios.get_ios_by_quarter(data, quarter_num, year) if data.empty: console.print("[yellow]⚠️ No data found for the specified filters[/yellow]") return console.print("[green]βœ… Index of Services data fetched successfully[/green]") console.print(f"[cyan]πŸ“Š Total quarters: {len(data)}[/cyan]") if not data.empty: earliest = data.iloc[0]["quarter_label"] latest_q = data.iloc[-1]["quarter_label"] console.print(f"[dim]Period: {earliest} to {latest_q}[/dim]") # Show summary statistics if requested if summary: stats = nisra_ios.get_ios_summary_statistics(data, start_year=start_year, end_year=end_year) console.print(f"\n[bold]Summary Statistics ({stats['period']}):[/bold]") console.print(f" Total quarters: {stats['quarters_count']}") console.print("\n NI Services Index:") console.print(f" Mean: {stats['ni_mean']:.1f}") console.print(f" Range: {stats['ni_min']:.1f} - {stats['ni_max']:.1f}") console.print("\n UK Services Index:") console.print(f" Mean: {stats['uk_mean']:.1f}") console.print(f" Range: {stats['uk_min']:.1f} - {stats['uk_max']:.1f}") if not save: return # Handle file saving if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]πŸ’Ύ Data saved to: {save}[/green]") return except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return # Output to console if output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]πŸ’‘ Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") raise click.Abort() from e
@nisra.command(name="index-of-production") @click.option("--year", type=int, help="Filter data for specific year") @click.option("--quarter", help="Filter data for specific quarter (e.g., 'Q1', 'Q2')") @click.option("--start-year", type=int, help="Start year for summary statistics") @click.option("--end-year", type=int, help="End year for summary statistics") @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)") @click.option("--summary", is_flag=True, help="Show summary statistics only") @click.option("--growth", is_flag=True, help="Include year-on-year growth rates")
[docs] def nisra_index_of_production_cmd( year, quarter, start_year, end_year, output_format, force_refresh, save, summary, growth ): """NISRA Index of Production (IOP) - Quarterly Economic Indicator. Measures output in Northern Ireland's production industries, including manufacturing, mining, and utilities. Seasonally adjusted quarterly data from Q1 2005 to present. Args: year: Filter data for specific year quarter: Filter data for specific quarter (e.g., 'Q1', 'Q2') start_year: Start year for summary statistics end_year: End year for summary statistics output_format: Output format (csv or json) force_refresh: Force re-download even if cached save: Save data to file (specify filename) summary: Show summary statistics only growth: Include year-on-year growth rates Examples: Get all Index of Production data:: bolster nisra index-of-production Filter for specific year:: bolster nisra index-of-production --year 2024 Get specific quarter:: bolster nisra index-of-production --year 2025 --quarter Q3 Show summary statistics:: bolster nisra index-of-production --start-year 2020 --summary Include growth rates:: bolster nisra index-of-production --growth Save as JSON:: bolster nisra index-of-production --format json --save iop.json Data Notes: - Coverage: Q1 2005 - Q3 2025 (quarterly) - Seasonally adjusted values - Index values (100 = base period) - Includes NI and UK comparator data - Production industries have faced long-term challenges Returns: DataFrame with columns: - date: First day of quarter - quarter: Quarter code (Q1, Q2, Q3, Q4) - year: Year - ni_index: Northern Ireland production index value - uk_index: UK production index value - ni_growth_rate: YoY % change (if --growth specified) - uk_growth_rate: YoY % change (if --growth specified) Note: Source: NISRA Economic & Labour Market Statistics Branch https://www.nisra.gov.uk/statistics/economic-output/index-production """ console = Console() try: with console.status("[bold green]Fetching Index of Production data..."): data = nisra_iop.get_latest_iop(force_refresh=force_refresh) # Add growth rates if requested if growth: data = nisra_iop.get_iop_growth(data) # Filter by year and/or quarter if specified if year: data = nisra_iop.get_iop_by_year(data, year) if quarter: quarter_num = int(quarter.lstrip("Qq")) data = nisra_iop.get_iop_by_quarter(data, quarter_num, year) if data.empty: console.print("[yellow]⚠️ No data found for the specified filters[/yellow]") return console.print("[green]βœ… Index of Production data fetched successfully[/green]") console.print(f"[cyan]πŸ“Š Total quarters: {len(data)}[/cyan]") if not data.empty: earliest = data.iloc[0]["quarter_label"] latest_q = data.iloc[-1]["quarter_label"] console.print(f"[dim]Period: {earliest} to {latest_q}[/dim]") # Show summary statistics if requested if summary: stats = nisra_iop.get_iop_summary_statistics(data, start_year=start_year, end_year=end_year) console.print(f"\n[bold]Summary Statistics ({stats['period']}):[/bold]") console.print(f" Total quarters: {stats['quarters_count']}") console.print("\n NI Production Index:") console.print(f" Mean: {stats['ni_mean']:.1f}") console.print(f" Range: {stats['ni_min']:.1f} - {stats['ni_max']:.1f}") console.print("\n UK Production Index:") console.print(f" Mean: {stats['uk_mean']:.1f}") console.print(f" Range: {stats['uk_min']:.1f} - {stats['uk_max']:.1f}") if not save: return # Handle file saving if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]πŸ’Ύ Data saved to: {save}[/green]") return except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return # Output to console if output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]πŸ’‘ Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") raise click.Abort() from e
@nisra.command(name="construction-output") @click.option("--year", type=int, help="Filter data for specific year") @click.option("--quarter", help="Filter data for specific quarter (e.g., 'Q1', 'Q2')") @click.option("--start-year", type=int, help="Start year for summary statistics") @click.option("--end-year", type=int, help="End year for summary statistics") @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)") @click.option("--summary", is_flag=True, help="Show summary statistics only") @click.option("--growth", is_flag=True, help="Include year-on-year growth rates")
[docs] def nisra_construction_output_cmd( year, quarter, start_year, end_year, output_format, force_refresh, save, summary, growth ): r"""NISRA Construction Output Statistics - Quarterly Economic Indicator. Measures volume and value of construction output in Northern Ireland across all work, new work, and repair & maintenance. Chained volume measure data (base year 2022=100) from Q2 2000 to present. Args: year: Filter data for specific year quarter: Filter data for specific quarter (e.g., 'Q1', 'Q2') start_year: Start year for summary statistics end_year: End year for summary statistics output_format: Output format (csv or json) force_refresh: Force re-download even if cached save: Save data to file (specify filename) summary: Show summary statistics only growth: Include year-on-year growth rates Examples: Get all Construction Output data:: bolster nisra construction-output Filter for specific year:: bolster nisra construction-output --year 2024 Get specific quarter:: bolster nisra construction-output --year 2025 --quarter Q2 Show summary statistics for 2020-2025:: bolster nisra construction-output --start-year 2020 --summary Include year-on-year growth rates:: bolster nisra construction-output --growth Save to file:: bolster nisra construction-output --save construction.csv Data Notes: - Coverage: Q2 2000 - Q2 2025 (quarterly) - Base year: 2022 = 100 (chained volume measure) - All Work: Non-seasonally adjusted (NSA) - New Work: Non-seasonally adjusted (NSA) - Repair & Maintenance: Seasonally adjusted (SA) - Published ~3 months after quarter end Returns: DataFrame with columns: - date: First day of quarter - quarter: Quarter code (Q1, Q2, Q3, Q4) - year: Year - all_work_index: Total construction output index (NSA) - new_work_index: New construction work index (NSA) - repair_maintenance_index: Repair & maintenance index (SA) - \*_yoy_growth: YoY % change (if --growth specified) Note: Source: NISRA Economic & Labour Market Statistics Branch https://www.nisra.gov.uk/statistics/economic-output/construction-output-statistics """ console = Console() try: with console.status("[bold green]Fetching Construction Output data..."): data = nisra_construction.get_latest_construction_output(force_refresh=force_refresh) # Add growth rates if requested if growth: data = nisra_construction.calculate_growth_rates(data) # Filter by year and/or quarter if specified if year: data = nisra_construction.get_construction_by_year(data, year) if quarter: data = nisra_construction.get_construction_by_quarter(data, quarter, year) if data.empty: console.print("[yellow]⚠️ No data found for the specified filters[/yellow]") return console.print("[green]βœ… Construction Output data fetched successfully[/green]") console.print(f"[cyan]πŸ“Š Total quarters: {len(data)}[/cyan]") if not data.empty: earliest = f"{data.iloc[0]['quarter']} {data.iloc[0]['year']}" latest_q = f"{data.iloc[-1]['quarter']} {data.iloc[-1]['year']}" console.print(f"[dim]Period: {earliest} to {latest_q}[/dim]") # Show summary statistics if requested if summary: stats = nisra_construction.get_summary_statistics(data, start_year=start_year, end_year=end_year) console.print(f"\n[bold]Summary Statistics ({stats['period']}):[/bold]") console.print(f" Total quarters: {stats['quarters_count']}") console.print("\n All Work Index:") console.print(f" Mean: {stats['all_work_mean']:.1f}") console.print(f" Range: {stats['all_work_min']:.1f} - {stats['all_work_max']:.1f}") console.print("\n New Work Index:") console.print(f" Mean: {stats['new_work_mean']:.1f}") console.print(f" Range: {stats['new_work_min']:.1f} - {stats['new_work_max']:.1f}") console.print("\n Repair & Maintenance Index:") console.print(f" Mean: {stats['repair_maintenance_mean']:.1f}") console.print(f" Range: {stats['repair_maintenance_min']:.1f} - {stats['repair_maintenance_max']:.1f}") if not save: return # Handle file saving if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]πŸ’Ύ Data saved to: {save}[/green]") return except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return # Output to console if output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]πŸ’‘ Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") raise click.Abort() from e
@nisra.command(name="ashe") @click.option( "--metric", type=click.Choice(["weekly", "hourly", "annual"], case_sensitive=False), default="weekly", help="Type of earnings metric (default: weekly)", ) @click.option( "--dimension", type=click.Choice( [ "timeseries", "geography", "sector", "real-earnings", "real-earnings-change", "real-earnings-index", "occupation-change", "industry-change", "pay-distribution", "pay-distribution-by-classification", ], case_sensitive=False, ), help="Data dimension to retrieve", ) @click.option( "--basis", type=click.Choice(["workplace", "residence"], case_sensitive=False), default="workplace", help="Geographic basis (for geography dimension only)", ) @click.option("--year", type=int, help="Filter data for specific year") @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)") @click.option("--growth", is_flag=True, help="Include year-on-year growth rates (timeseries only)")
[docs] def nisra_ashe_cmd(metric, dimension, basis, year, output_format, force_refresh, save, growth): """NISRA Annual Survey of Hours and Earnings (ASHE) - Employee Earnings Statistics. Annual survey measuring employee earnings in Northern Ireland across multiple dimensions including employment type, sector, geography, occupation, and industry. Data from April of each year, published in October. Args: metric: Type of earnings metric (weekly, hourly, or annual) dimension: Data dimension (timeseries, geography, sector, real-earnings, real-earnings-change, real-earnings-index, occupation-change, industry-change, pay-distribution, pay-distribution-by-classification) basis: Geographic basis (workplace or residence, for geography dimension only) year: Filter data for specific year output_format: Output format (csv or json) force_refresh: Force re-download even if cached save: Save data to file (specify filename) growth: Include year-on-year growth rates (timeseries only) Examples: Get weekly earnings timeseries (1997-2025):: bolster nisra ashe Get hourly earnings timeseries:: bolster nisra ashe --metric hourly Get annual earnings timeseries:: bolster nisra ashe --metric annual Get geographic earnings by workplace:: bolster nisra ashe --dimension geography Get geographic earnings by residence:: bolster nisra ashe --dimension geography --basis residence Get public vs private sector comparison:: bolster nisra ashe --dimension sector Include year-on-year growth rates:: bolster nisra ashe --growth Filter for specific year:: bolster nisra ashe --year 2025 Save to file:: bolster nisra ashe --save earnings.csv --format csv Data Notes: - Coverage: April 1997 - 2025 (annual, timeseries) - Annual earnings: 1999 - 2025 - Sector breakdown: 2005 - 2025 - Reference period: April of each year - Published: October each year - Base: Employee jobs in Northern Ireland (not self-employed) Metrics: - weekly: Median gross weekly earnings (Β£) - hourly: Median hourly earnings excluding overtime (Β£) - annual: Median annual earnings (Β£) Dimensions: - timeseries: Historical trends by work pattern (Full-time/Part-time/All) - geography: Earnings by 11 Local Government Districts - sector: Public vs Private sector comparison (NI & UK) Returns: DataFrame with different columns depending on dimension: Timeseries: - year: Year - work_pattern: Full-time, Part-time, or All - median_*_earnings: Median earnings (Β£) - earnings_yoy_growth: YoY % change (if --growth specified) Geography: - year: Year - lgd: Local Government District name - basis: workplace or residence - median_weekly_earnings: Median weekly earnings (Β£) Sector: - year: Year - location: Northern Ireland or United Kingdom - sector: Public or Private - median_weekly_earnings: Median weekly earnings (Β£) Note: Source: NISRA Economic & Labour Market Statistics Branch https://www.nisra.gov.uk/statistics/work-pay-and-benefits/annual-survey-hours-and-earnings """ console = Console() try: # Determine what data to fetch if dimension == "geography": with console.status(f"[bold green]Fetching ASHE geographic earnings ({basis})..."): data = nisra_ashe.get_latest_ashe_geography(basis=basis, force_refresh=force_refresh) elif dimension == "sector": with console.status("[bold green]Fetching ASHE sector earnings..."): data = nisra_ashe.get_latest_ashe_sector(force_refresh=force_refresh) elif dimension == "real-earnings": with console.status("[bold green]Fetching ASHE real earnings (Figure 2)..."): data = nisra_ashe.get_real_earnings(force_refresh=force_refresh) elif dimension == "real-earnings-change": with console.status("[bold green]Fetching ASHE real earnings change by pattern (Figure 3)..."): data = nisra_ashe.get_real_earnings_change_by_pattern(force_refresh=force_refresh) elif dimension == "real-earnings-index": with console.status("[bold green]Fetching ASHE real earnings index by sector (Figure 6)..."): data = nisra_ashe.get_real_earnings_index_by_sector(force_refresh=force_refresh) elif dimension == "occupation-change": with console.status("[bold green]Fetching ASHE earnings change by occupation (Figure 7)..."): data = nisra_ashe.get_annual_change_by_occupation(force_refresh=force_refresh) elif dimension == "industry-change": with console.status("[bold green]Fetching ASHE earnings change by industry (Figure 8)..."): data = nisra_ashe.get_annual_change_by_industry(force_refresh=force_refresh) elif dimension == "pay-distribution": with console.status("[bold green]Fetching ASHE pay distribution timeseries (Figure 11)..."): data = nisra_ashe.get_pay_distribution_timeseries(force_refresh=force_refresh) elif dimension == "pay-distribution-by-classification": with console.status("[bold green]Fetching ASHE pay distribution by classification (Figure 12)..."): data = nisra_ashe.get_pay_distribution_by_classification(force_refresh=force_refresh) else: # Default to timeseries with console.status(f"[bold green]Fetching ASHE {metric} earnings..."): data = nisra_ashe.get_latest_ashe_timeseries(metric=metric, force_refresh=force_refresh) # Add growth rates if requested (only for timeseries) if growth: data = nisra_ashe.calculate_growth_rates(data) # Filter by year if specified if year: data = nisra_ashe.get_earnings_by_year(data, year) if data.empty: console.print("[yellow]⚠️ No data found for the specified filters[/yellow]") return console.print("[green]βœ… ASHE data fetched successfully[/green]") console.print(f"[cyan]πŸ“Š Total records: {len(data)}[/cyan]") if not data.empty and "year" in data.columns: years = data["year"].unique() console.print(f"[dim]Years: {min(years)} - {max(years)}[/dim]") # Handle file saving if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]πŸ’Ύ Data saved to: {save}[/green]") return except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return # Output to console if output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]πŸ’‘ Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") raise click.Abort() from e
@nisra.command(name="composite-index") @click.option( "--dimension", type=click.Choice(["indices", "contributions", "all"], case_sensitive=False), default="indices", help="Which dimension to retrieve (default: indices)", ) @click.option("--table", "table_deprecated", hidden=True, default=None) @click.option("--year", type=int, help="Filter data for specific year") @click.option("--quarter", type=int, help="Filter data for specific quarter (1-4)") @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)")
[docs] def nisra_composite_index_cmd(dimension, table_deprecated, year, quarter, output_format, force_refresh, save): """NISRA Northern Ireland Composite Economic Index (NICEI) - Experimental Economic Indicator. Quarterly measure of NI economic performance tracking five key sectors: Services, Production, Construction, Agriculture, and Public Sector. Base period 2022=100, quarterly data from Q1 2006 to present. Args: dimension: Which dimension to retrieve (indices, contributions, or all) table_deprecated: Deprecated alias for --dimension (use --dimension instead) year: Filter data for specific year quarter: Filter data for specific quarter (1-4) output_format: Output format (csv or json) force_refresh: Force re-download even if cached save: Save data to file (specify filename) Examples: Get latest NICEI indices:: bolster nisra composite-index Get sector contributions to quarterly change:: bolster nisra composite-index --dimension contributions Get all dimensions:: bolster nisra composite-index --dimension all Filter for specific year:: bolster nisra composite-index --year 2024 Get specific quarter:: bolster nisra composite-index --year 2025 --quarter 2 Save to file:: bolster nisra composite-index --save nicei.csv Data Notes: - Coverage: Q1 2006 - Q2 2025 (quarterly) - Base period: 2022 = 100 - Experimental statistic subject to revision - Published ~3 months after quarter end - Not seasonally adjusted Tables: indices: NICEI and component indices by quarter (Table 1) β€’ Overall NICEI, private/public sector breakdowns β€’ Sectoral indices: Services, Production, Construction, Agriculture β€’ Quarterly time series from Q1 2006 contributions: Sector contributions to quarterly change (Table 11) β€’ How much each sector contributed to NICEI quarterly change β€’ Percentage point contributions β€’ Identifies main drivers of economic growth/decline all: All available tables Returns: DataFrame with columns varying by table type: - date: Quarter start date - quarter: Quarter (Q1-Q4) - year: Year - Various index/contribution columns depending on table Note: Source: NISRA Economic & Labour Market Statistics Branch https://www.nisra.gov.uk/statistics/economic-output-statistics/ni-composite-economic-index """ console = Console() if table_deprecated is not None: click.echo("Warning: --table is deprecated, use --dimension instead", err=True) dimension = table_deprecated try: with console.status("[bold green]Fetching NICEI data..."): if dimension in ("indices", "all"): indices_data = nisra_composite.get_latest_nicei(force_refresh=force_refresh) if dimension in ("contributions", "all"): contrib_data = nisra_composite.get_latest_nicei_contributions(force_refresh=force_refresh) # Apply filters if dimension == "indices": data = indices_data if year: data = nisra_composite.get_nicei_by_year(data, year) if quarter: data = nisra_composite.get_nicei_by_quarter(data, year, quarter) elif dimension == "contributions": data = contrib_data if year: data = data[data["year"] == year] if quarter: data = data[data["quarter"] == quarter] else: # all # For 'all', we'll use a dict like labour_market does data = {"indices": indices_data, "contributions": contrib_data} if year: data["indices"] = nisra_composite.get_nicei_by_year(data["indices"], year) data["contributions"] = data["contributions"][data["contributions"]["year"] == year] if quarter: data["indices"] = nisra_composite.get_nicei_by_quarter(data["indices"], year, quarter) data["contributions"] = data["contributions"][data["contributions"]["quarter"] == quarter] # Check for empty results if dimension in ("indices", "contributions"): if data.empty: console.print("[yellow]⚠️ No data found for the specified filters[/yellow]") return else: if all(df.empty for df in data.values()): console.print("[yellow]⚠️ No data found for the specified filters[/yellow]") return # Display success message if dimension == "all": console.print("[green]βœ… Retrieved all dimensions successfully[/green]") total_records = sum(len(df) for df in data.values()) console.print(f"[cyan]πŸ“Š Total records: {total_records}[/cyan]") for dim_name, df in data.items(): console.print(f" β€’ {dim_name}: {len(df)} records") else: console.print(f"[green]βœ… Retrieved {dimension} dimension successfully[/green]") console.print(f"[cyan]πŸ“Š Total records: {len(data)}[/cyan]") if ( not (isinstance(data, dict) and all(df.empty for df in data.values())) and dimension != "all" and not data.empty ): years = data["year"].unique() console.print(f"[dim]Years: {min(years)} - {max(years)}[/dim]") # Handle file saving if save: try: if dimension == "all": # Save each dimension to a separate file for dim_name, df in data.items(): filename = ( f"{save.rsplit('.', 1)[0]}_{dim_name}.{save.rsplit('.', 1)[-1] if '.' in save else 'csv'}" ) if output_format == "json" or filename.endswith(".json"): df.to_json(filename, orient="records", date_format="iso", indent=2) else: df.to_csv(filename, index=False) console.print(f"[green]πŸ’Ύ Saved {dim_name} to: {filename}[/green]") else: # Save single dimension if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]πŸ’Ύ Data saved to: {save}[/green]") return except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return # Output to console if dimension == "all": for dim_name, df in data.items(): console.print(f"\n[bold]{dim_name.upper()}:[/bold]") if output_format == "json": click.echo(df.to_json(orient="records", date_format="iso", indent=2)) else: console.print(df.to_csv(index=False), end="") else: if output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]πŸ’‘ Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") raise click.Abort() from e
@nisra.command(name="wellbeing") @click.option( "--dimension", type=click.Choice(["personal", "loneliness", "self-efficacy", "summary"], case_sensitive=False), default="personal", help="Which dimension to retrieve (default: personal)", ) @click.option("--metric", "metric_deprecated", hidden=True, default=None) @click.option("--year", type=str, help="Filter data for specific year (format: 2024/25)") @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)")
[docs] def nisra_wellbeing_cmd(dimension, metric_deprecated, year, output_format, force_refresh, save): """NISRA Individual Wellbeing Statistics. Retrieves individual wellbeing statistics for Northern Ireland, measuring subjective wellbeing across the population aged 16 and over. Args: dimension: Which dimension to retrieve (personal, loneliness, self-efficacy, or summary) metric_deprecated: Deprecated alias for --dimension (use --dimension instead) year: Filter data for specific year (format: 2024/25) output_format: Output format (csv or json) force_refresh: Force re-download even if cached save: Save data to file (specify filename) Dimensions (personal, loneliness, self-efficacy, summary): personal - ONS4 measures (Life Satisfaction, Worthwhile, Happiness, Anxiety); scores 0-10 (higher is better, except Anxiety). loneliness - Proportion feeling lonely at least some of the time. self-efficacy - Mean self-efficacy scores (range 5-25). summary - Combined latest values for all measures. Examples: Get personal wellbeing (ONS4 measures):: bolster nisra wellbeing Get loneliness statistics:: bolster nisra wellbeing --dimension loneliness Get summary of all dimensions for latest year:: bolster nisra wellbeing --dimension summary Filter for a specific year:: bolster nisra wellbeing --year "2023/24" Save to file:: bolster nisra wellbeing --save wellbeing.csv Data Notes: - Personal wellbeing: Annual from 2014/15 to present - Loneliness: Annual from 2017/18 to present - Self-efficacy: Annual from 2014/15 to present - COVID-19 Note: 2020/21 shows increased anxiety and loneliness Returns: DataFrame with columns (for personal metric): - year: Financial year (e.g., "2024/25") - life_satisfaction: Mean score 0-10 (higher is better) - worthwhile: Mean score 0-10 (higher is better) - happiness: Mean score 0-10 (higher is better) - anxiety: Mean score 0-10 (lower is better) Note: Source: https://www.nisra.gov.uk/statistics/wellbeing/individual-wellbeing-northern-ireland """ console = Console() if metric_deprecated is not None: click.echo("Warning: --metric is deprecated, use --dimension instead", err=True) dimension = metric_deprecated try: with console.status("[bold green]Downloading latest NISRA wellbeing data..."): if dimension == "personal": data = nisra_wellbeing.get_latest_personal_wellbeing(force_refresh=force_refresh) elif dimension == "loneliness": data = nisra_wellbeing.get_latest_loneliness(force_refresh=force_refresh) elif dimension == "self-efficacy": data = nisra_wellbeing.get_latest_self_efficacy(force_refresh=force_refresh) elif dimension == "summary": data = nisra_wellbeing.get_wellbeing_summary(force_refresh=force_refresh) # Filter by year if specified if year and dimension == "personal": data = nisra_wellbeing.get_personal_wellbeing_by_year(data, year) if data.empty: console.print(f"[yellow]⚠️ No data found for year {year}[/yellow]") return elif year and dimension != "summary": data = data[data["year"] == year] if data.empty: console.print(f"[yellow]⚠️ No data found for year {year}[/yellow]") return console.print(f"[green]βœ… Retrieved {dimension} wellbeing data successfully[/green]") console.print(f"[cyan]πŸ“Š Total records: {len(data)}[/cyan]") if not data.empty: years = data["year"].unique() if len(years) > 1: console.print(f"[dim]Years: {years[0]} - {years[-1]}[/dim]") else: console.print(f"[dim]Year: {years[0]}[/dim]") # Show summary based on dimension type if dimension == "personal": latest_row = data.iloc[-1] console.print("\n[bold]Latest Values:[/bold]") console.print(f" Life Satisfaction: {latest_row['life_satisfaction']:.1f}/10") console.print(f" Worthwhile: {latest_row['worthwhile']:.1f}/10") console.print(f" Happiness: {latest_row['happiness']:.1f}/10") console.print(f" Anxiety: {latest_row['anxiety']:.1f}/10 (lower is better)") elif dimension == "loneliness": latest_row = data.iloc[-1] console.print("\n[bold]Latest Values:[/bold]") console.print(f" Lonely (some of time): {latest_row['lonely_some_of_time']:.1%}") elif dimension == "self-efficacy": latest_row = data.iloc[-1] console.print("\n[bold]Latest Values:[/bold]") console.print(f" Self-efficacy mean: {latest_row['self_efficacy_mean']:.1f}/25") elif dimension == "summary": row = data.iloc[0] console.print("\n[bold]Wellbeing Summary:[/bold]") console.print(f" Life Satisfaction: {row['life_satisfaction']:.1f}/10") console.print(f" Worthwhile: {row['worthwhile']:.1f}/10") console.print(f" Happiness: {row['happiness']:.1f}/10") console.print(f" Anxiety: {row['anxiety']:.1f}/10") console.print(f" Lonely (some of time): {row['lonely_some_of_time']:.1%}") console.print(f" Self-efficacy: {row['self_efficacy_mean']:.1f}/25") # Handle file saving if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]πŸ’Ύ Data saved to: {save}[/green]") return except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return # Output to console if output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: console.print("\n[bold]Data:[/bold]") console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]πŸ’‘ Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") raise click.Abort() from e
@nisra.command(name="cancer-waiting-times") @click.option( "--target", type=click.Choice(["14-day", "31-day", "62-day", "referrals"], case_sensitive=False), default="31-day", help="Waiting time target to retrieve (default: 31-day)", ) @click.option( "--dimension", type=click.Choice(["trust", "tumour"], case_sensitive=False), default="trust", help="Data dimension: by HSC Trust or by Tumour Site (default: trust)", ) @click.option("--year", type=int, help="Filter data for specific year") @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)") @click.option("--summary", is_flag=True, help="Show NI-wide summary instead of raw data")
[docs] def nisra_cancer_cmd(target, dimension, year, output_format, force_refresh, save, summary): """NISRA Cancer Waiting Times Statistics. Retrieves cancer waiting times performance data for Northern Ireland from the Department of Health, tracking progress against ministerial targets. Targets: 14-day (urgent breast referrals), 31-day (decision to treat), 62-day (urgent GP referral), referrals (monthly breast volumes). Dimensions: trust (HSC Trust breakdown) or tumour (tumour site breakdown). Examples:: bolster nisra cancer-waiting-times bolster nisra cancer-waiting-times --target 62-day --dimension tumour bolster nisra cancer-waiting-times --target 14-day bolster nisra cancer-waiting-times --year 2024 --summary bolster nisra cancer-waiting-times --save cancer.csv Key insights (as of 2025): 31-day target NI at ~90%; 62-day collapsed to ~32%; 14-day breast dropped from 77% (2020) to 17% (2025). Data from Q1 2008 to present. Source: https://www.health-ni.gov.uk/articles/cancer-waiting-times """ console = Console() try: with console.status("[bold green]Downloading latest NISRA cancer waiting times data..."): # Select appropriate data source based on target and dimension if target == "14-day": data = nisra_cancer.get_latest_14_day_breast(force_refresh=force_refresh) target_label = "14-day Breast" elif target == "referrals": data = nisra_cancer.get_latest_breast_referrals(force_refresh=force_refresh) target_label = "Breast Referrals" elif target == "31-day": if dimension == "tumour": data = nisra_cancer.get_latest_31_day_by_tumour(force_refresh=force_refresh) target_label = "31-day by Tumour Site" else: data = nisra_cancer.get_latest_31_day_by_trust(force_refresh=force_refresh) target_label = "31-day by HSC Trust" elif target == "62-day": if dimension == "tumour": data = nisra_cancer.get_latest_62_day_by_tumour(force_refresh=force_refresh) target_label = "62-day by Tumour Site" else: data = nisra_cancer.get_latest_62_day_by_trust(force_refresh=force_refresh) target_label = "62-day by HSC Trust" # Filter by year if specified if year: data = nisra_cancer.get_data_by_year(data, year) if data.empty: console.print(f"[yellow]⚠️ No data found for year {year}[/yellow]") return # Get NI-wide summary if requested (not for referrals) if summary and target != "referrals": data = nisra_cancer.get_ni_wide_performance(data) console.print(f"[green]βœ… Retrieved {target_label} data successfully[/green]") console.print(f"[cyan]πŸ“Š Total records: {len(data)}[/cyan]") if not data.empty: years = sorted(data["year"].unique()) if len(years) > 1: console.print(f"[dim]Years: {years[0]} - {years[-1]}[/dim]") else: console.print(f"[dim]Year: {years[0]}[/dim]") # Show summary statistics if target == "referrals": latest_year = data["year"].max() latest_data = data[data["year"] == latest_year] total_referrals = latest_data["total_referrals"].sum() urgent_referrals = latest_data["urgent_referrals"].sum() console.print(f"\n[bold]Latest Year ({latest_year}) Summary:[/bold]") console.print(f" Total Referrals: {total_referrals:,.0f}") console.print(f" Urgent Referrals: {urgent_referrals:,.0f}") console.print(f" Urgent Rate: {urgent_referrals / total_referrals:.1%}") else: # Performance summary valid_data = data.dropna(subset=["performance_rate"]) valid_data = valid_data[valid_data["total"] > 0] if not valid_data.empty: latest_year = valid_data["year"].max() latest_data = valid_data[valid_data["year"] == latest_year] total_patients = latest_data["total"].sum() within_target = latest_data["within_target"].sum() overall_rate = within_target / total_patients if total_patients > 0 else 0 console.print(f"\n[bold]Latest Year ({latest_year}) Summary:[/bold]") console.print(f" Total Patients: {total_patients:,.0f}") console.print(f" Within Target: {within_target:,.0f}") console.print(f" Performance Rate: {overall_rate:.1%}") # Show target status target_threshold = 0.95 if overall_rate >= target_threshold: console.print(" [green]βœ… Meeting 95% target[/green]") else: gap = (target_threshold - overall_rate) * 100 console.print(f" [red]❌ {gap:.1f}pp below 95% target[/red]") # Handle file saving if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]πŸ’Ύ Data saved to: {save}[/green]") return except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return # Output to console if output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: console.print("\n[bold]Data:[/bold]") console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]πŸ’‘ Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") raise click.Abort() from e
@nisra.command(name="emergency-care") @click.option( "--format", "output_format", type=click.Choice(["table", "csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--trust", help="Filter by HSC Trust name (e.g. Belfast)") @click.option( "--type", "attendance_type", type=click.Choice(["1", "2", "3"], case_sensitive=False), help="Filter by attendance type (1=major A&E, 2=single specialty, 3=MIU/UTC)", ) @click.option("--year", type=int, help="Filter data for a specific calendar year") @click.option("--save", help="Save output to file (specify filename)") @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached")
[docs] def nisra_emergency_care_cmd(output_format, trust, attendance_type, year, save, force_refresh): """NISRA Emergency Care Waiting Times Statistics. Monthly A&E performance data for Northern Ireland measuring the 4-hour target (95% of attendances seen, treated, admitted or discharged within 4 hours). Data by hospital department and HSC Trust, April 2008 to present. Attendance Types: Type 1 - Major A&E departments (full A&E) Type 2 - Single specialty emergency departments Type 3 - Minor injury units and urgent treatment centres HSC Trusts: Belfast, Northern, South Eastern, Southern, Western Examples: --------- Get all data as CSV:: bolster nisra emergency-care Get Type 1 performance for Belfast Trust:: bolster nisra emergency-care --type 1 --trust Belfast Get 2024 data as JSON:: bolster nisra emergency-care --year 2024 --format json Save to file:: bolster nisra emergency-care --save emergency.csv Key Insights (as of 2026) ------------------------- - 4-hour target (95%): NI performance has declined significantly since 2008 - Type 1 performance well below target; Type 3 minor injury units perform best - Belfast RVH and Altnagelvin consistently the busiest Type 1 sites Source ------ https://www.health-ni.gov.uk/articles/emergency-care-waiting-times """ console = Console() try: with console.status("[bold green]Downloading emergency care waiting times data..."): data = nisra_emergency.get_latest_data(force_refresh=force_refresh) if trust: trust_filter = trust.strip() filtered = data[data["trust"].str.contains(trust_filter, case=False, na=False)] if filtered.empty: console.print(f"[yellow]No data found for trust matching '{trust_filter}'[/yellow]") available = sorted(data["trust"].unique()) console.print(f"[dim]Available trusts: {', '.join(available)}[/dim]") return data = filtered if attendance_type: type_label = f"Type {attendance_type}" data = data[data["attendance_type"] == type_label] if data.empty: console.print(f"[yellow]No data found for {type_label}[/yellow]") return if year: data = data[data["year"] == year] if data.empty: console.print(f"[yellow]No data found for year {year}[/yellow]") return console.print(f"[green]Retrieved {len(data):,} records[/green]") if not data.empty: min_year = int(data["year"].min()) max_year = int(data["year"].max()) console.print(f"[dim]Years: {min_year} - {max_year}[/dim]") avg_pct = data["pct_within_4hrs"].mean() console.print(f"[dim]Average pct within 4hrs: {avg_pct:.1%}[/dim]") if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]Data saved to: {save}[/green]") return except Exception as e: console.print(f"[red]Error saving file: {e}[/red]") return if output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) elif output_format == "table": from rich.table import Table table = Table(title="Emergency Care Waiting Times") for col in data.columns: table.add_column(col) for _, row in data.head(50).iterrows(): table.add_row(*[str(v) for v in row]) console.print(table) if len(data) > 50: console.print(f"[dim]... and {len(data) - 50:,} more rows (use --save to get all)[/dim]") else: console.print("\n[bold]Data:[/bold]") console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") raise click.Abort() from e
@nisra.command(name="elective-waiting-times") @click.option( "--type", "waiting_type", type=click.Choice(["all", "inpatient_day_case", "outpatient"], case_sensitive=False), default="all", help="Series to retrieve: all (default), inpatient_day_case, or outpatient", ) @click.option("--trust", help="Filter by HSC Trust name (e.g. Belfast)") @click.option("--year", type=int, help="Filter data for a specific calendar year") @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save output to file (specify filename)")
[docs] def nisra_elective_waiting_times_cmd(waiting_type, trust, year, output_format, force_refresh, save): r"""NISRA Elective/Outpatient Waiting Times Statistics. \b Two quarterly series from the Department of Health NI: inpatient_day_case Patients waiting for inpatient/day case admission, by weeks-waited band, specialty, and HSC Trust. Data from Q1 2007-08 (June 2007) to present. outpatient Referrals waiting for an outpatient appointment, by weeks-waited band, specialty, and HSC Trust. Data from Q1 2008-09 (June 2008) to present. \b HSC Trusts: Belfast, Northern, South Eastern, Southern, Western \b Examples:: bolster nisra elective-waiting-times bolster nisra elective-waiting-times --type outpatient bolster nisra elective-waiting-times --trust Belfast --year 2024 bolster nisra elective-waiting-times --type inpatient_day_case --save inpatient.csv bolster nisra elective-waiting-times --format json \b Key insights (as of 2025): over 400,000 outpatient referrals waiting; median outpatient wait exceeding 43 weeks; inpatient waits exceeding 104 weeks visible across multiple specialties and trusts. \b Sources: https://www.health-ni.gov.uk/articles/inpatient-waiting-times https://www.health-ni.gov.uk/articles/outpatient-waiting-times """ from rich.console import Console from bolster.data_sources.nisra import elective_waiting_times as ewt console = Console() try: with console.status("[bold green]Downloading latest elective waiting times data..."): data = ewt.get_latest_elective_waiting_times(force_refresh=force_refresh) # Filter by waiting_type if waiting_type != "all": data = data[data["waiting_type"] == waiting_type] if data.empty: console.print(f"[yellow]No data found for waiting_type='{waiting_type}'[/yellow]") return # Filter by trust if trust: data = data[data["trust"].str.contains(trust, case=False, na=False)] if data.empty: console.print(f"[yellow]No data found for trust matching '{trust}'[/yellow]") return # Filter by year if year: data = data[data["year"] == year] if data.empty: console.print(f"[yellow]No data found for year {year}[/yellow]") return console.print(f"[green]Retrieved {len(data):,} records[/green]") if not data.empty: min_year = int(data["year"].min()) max_year = int(data["year"].max()) console.print(f"[dim]Years: {min_year} - {max_year}[/dim]") total_waiting = data["patients_waiting"].sum() console.print(f"[dim]Total patient-band records: {total_waiting:,.0f}[/dim]") if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]Data saved to: {save}[/green]") return except Exception as e: console.print(f"[red]Error saving file: {e}[/red]") return if output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: console.print("\n[bold]Data:[/bold]") console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") raise click.Abort() from e
@nisra.command(name="registrar-general") @click.option("--latest", is_flag=True, help="Get the most recent quarterly tables data") @click.option("--quarterly", is_flag=True, help="Show full quarterly time series") @click.option("--lgd", is_flag=True, help="Show LGD (Local Government District) breakdown") @click.option("--validate", is_flag=True, help="Run cross-validation against monthly data") @click.option( "--dimension", type=click.Choice(["births", "deaths", "all"], case_sensitive=False), default="all", help="Which dimension to retrieve (default: all)", ) @click.option("--table", "table_deprecated", hidden=True, default=None) @click.option("--year", type=int, help="Filter data for specific year") @click.option("--quarter", type=int, help="Filter data for specific quarter (1-4)") @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)")
[docs] def nisra_registrar_general_cmd( latest, quarterly, lgd, validate, dimension, table_deprecated, year, quarter, output_format, force_refresh, save ): """NISRA Registrar General Quarterly Tables. Quarterly vital statistics for Northern Ireland including births, deaths, marriages, civil partnerships, and LGD-level breakdowns. Data available from Q1 2009 to present. Args: latest: Get the most recent quarterly tables data quarterly: Show full quarterly time series lgd: Show LGD (Local Government District) breakdown validate: Run cross-validation against monthly data dimension: Which dimension to retrieve (births, deaths, or all) table_deprecated: Deprecated alias for --dimension (use --dimension instead) year: Filter data for specific year quarter: Filter data for specific quarter (1-4) output_format: Output format (csv or json) force_refresh: Force re-download even if cached save: Save data to file (specify filename) Tables Available: births: Quarterly births, stillbirths, birth rates deaths: Quarterly deaths, marriages, civil partnerships, death rates lgd: Current quarter breakdown by Local Government District Examples: Get latest quarterly data (all tables):: bolster nisra registrar-general --latest Get quarterly births time series:: bolster nisra registrar-general --quarterly --dimension births Get LGD breakdown for current quarter:: bolster nisra registrar-general --lgd Cross-validate quarterly vs monthly data:: bolster nisra registrar-general --validate Get specific year/quarter:: bolster nisra registrar-general --quarterly --year 2024 --quarter 1 Save to file:: bolster nisra registrar-general --quarterly --save quarterly.csv Cross-Validation: The --validate option compares quarterly totals against aggregated monthly data from the births and marriages modules. Differences within 2% are considered acceptable (timing of registrations). Data Notes: - Quarterly data from Q1 2009 to present - Updated approximately 6 weeks after each quarter ends - 11 Local Government Districts in NI - Birth/death rates per 1,000 population Returns: DataFrame with columns varying by table type: - quarter: Quarter (Q1, Q2, Q3, Q4) - year: Year - Various statistical columns depending on table Note: Source: https://www.nisra.gov.uk/statistics/births-deaths-and-marriages/registrar-general-quarterly-report """ console = Console() if table_deprecated is not None: click.echo("Warning: --table is deprecated, use --dimension instead", err=True) dimension = table_deprecated if not any([latest, quarterly, lgd, validate]): console.print("[yellow]⚠️ Please specify an option: --latest, --quarterly, --lgd, or --validate[/yellow]") console.print("[dim]Use --help for more information[/dim]") return try: with console.status("[bold green]Downloading Registrar General Quarterly Tables..."): data = nisra_registrar_general.get_quarterly_vital_statistics(force_refresh=force_refresh) # Handle validation mode if validate: console.print("[bold cyan]Cross-Validation Report[/bold cyan]") console.print("=" * 50) report = nisra_registrar_general.get_validation_report(force_refresh=force_refresh) if not report["summary"].empty: console.print("\n[bold]Summary:[/bold]") for _, row in report["summary"].iterrows(): validation_name = row["validation"].replace("_", " ").title() console.print(f" {validation_name}:") console.print(f" Quarters compared: {row['quarters_compared']}") console.print(f" Average difference: {row['avg_pct_diff']:.2f}%") console.print(f" Max difference: {row['max_pct_diff']:.2f}%") console.print(f" Within 2%: {row['within_2pct']}") if "births_validation" in report and not report["births_validation"].empty: console.print("\n[bold]Births Validation (recent quarters):[/bold]") recent = report["births_validation"].tail(8) console.print(recent.to_string(index=False)) if "marriages_validation" in report and not report["marriages_validation"].empty: console.print("\n[bold]Marriages Validation (recent quarters):[/bold]") recent = report["marriages_validation"].tail(8) console.print(recent.to_string(index=False)) return # Handle LGD mode if lgd: lgd_data = data["lgd"] if lgd_data.empty: console.print("[yellow]⚠️ LGD data not available[/yellow]") return console.print("[bold cyan]LGD-Level Statistics (Current Quarter)[/bold cyan]") console.print(f"[dim]{len(lgd_data)} Local Government Districts[/dim]\n") # Handle file saving if save: try: if output_format == "json" or save.endswith(".json"): lgd_data.to_json(save, orient="records", indent=2) else: lgd_data.to_csv(save, index=False) console.print(f"[green]πŸ’Ύ Data saved to: {save}[/green]") return except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return # Output data if output_format == "json": click.echo(lgd_data.to_json(orient="records", indent=2)) else: console.print(lgd_data.to_csv(index=False), end="") return # Handle quarterly time series or latest if dimension == "births" or dimension == "all": births_data = data["births"] if not births_data.empty: # Filter by year/quarter if specified if year: births_data = births_data[births_data["year"] == year] if quarter: births_data = births_data[births_data["quarter"] == quarter] if dimension == "births" or (dimension == "all" and not quarterly): output_data = births_data else: output_data = births_data if dimension == "births" else None if dimension == "deaths" or dimension == "all": deaths_data = data["deaths"] if not deaths_data.empty: if year: deaths_data = deaths_data[deaths_data["year"] == year] if quarter: deaths_data = deaths_data[deaths_data["quarter"] == quarter] if dimension == "deaths": output_data = deaths_data # Show summary for --latest if latest and not quarterly: console.print("[bold cyan]Registrar General Quarterly Tables - Latest Data[/bold cyan]") console.print("=" * 50) if not data["births"].empty: latest_birth = data["births"].iloc[-1] console.print(f"\n[bold]Latest Births (Q{latest_birth['quarter']} {latest_birth['year']}):[/bold]") console.print(f" Total births: {latest_birth['total_births']:,}") if "birth_rate" in latest_birth and pd.notna(latest_birth["birth_rate"]): console.print(f" Birth rate: {latest_birth['birth_rate']:.1f} per 1,000") if "stillbirths" in latest_birth and pd.notna(latest_birth["stillbirths"]): console.print(f" Stillbirths: {int(latest_birth['stillbirths'])}") if not data["deaths"].empty: latest_death = data["deaths"].iloc[-1] console.print( f"\n[bold]Latest Deaths/Marriages (Q{latest_death['quarter']} {latest_death['year']}):[/bold]" ) console.print(f" Deaths: {latest_death['deaths']:,}") if "death_rate" in latest_death and pd.notna(latest_death["death_rate"]): console.print(f" Death rate: {latest_death['death_rate']:.1f} per 1,000") if "marriages" in latest_death and pd.notna(latest_death["marriages"]): console.print(f" Marriages: {int(latest_death['marriages']):,}") if "civil_partnerships" in latest_death and pd.notna(latest_death["civil_partnerships"]): console.print(f" Civil partnerships: {int(latest_death['civil_partnerships'])}") # Show historical range if not data["births"].empty: min_year = data["births"]["year"].min() num_quarters = len(data["births"]) console.print(f"\n[dim]Historical data: {num_quarters} quarters from Q1 {min_year} to present[/dim]") return # Handle full data output for --quarterly if quarterly: if dimension == "all": console.print("[bold cyan]Quarterly Vital Statistics[/bold cyan]") console.print("\n[bold]Births Data:[/bold]") if not data["births"].empty: output = data["births"] if year: output = output[output["year"] == year] if quarter: output = output[output["quarter"] == quarter] console.print(f"[dim]{len(output)} records[/dim]") if save: births_file = save.replace(".", "_births.") output.to_csv(births_file, index=False) console.print(f"[green]πŸ’Ύ Births saved to: {births_file}[/green]") else: if output_format == "json": click.echo(output.to_json(orient="records", date_format="iso", indent=2)) else: console.print(output.to_csv(index=False), end="") console.print("\n[bold]Deaths/Marriages Data:[/bold]") if not data["deaths"].empty: output = data["deaths"] if year: output = output[output["year"] == year] if quarter: output = output[output["quarter"] == quarter] console.print(f"[dim]{len(output)} records[/dim]") if save: deaths_file = save.replace(".", "_deaths.") output.to_csv(deaths_file, index=False) console.print(f"[green]πŸ’Ύ Deaths saved to: {deaths_file}[/green]") else: if output_format == "json": click.echo(output.to_json(orient="records", date_format="iso", indent=2)) else: console.print(output.to_csv(index=False), end="") else: # Single dimension output output_data = data["births"] if dimension == "births" else data["deaths"] if year: output_data = output_data[output_data["year"] == year] if quarter: output_data = output_data[output_data["quarter"] == quarter] console.print(f"[bold cyan]Quarterly {dimension.title()} Data[/bold cyan]") console.print(f"[dim]{len(output_data)} records[/dim]\n") if save: try: if output_format == "json" or save.endswith(".json"): output_data.to_json(save, orient="records", date_format="iso", indent=2) else: output_data.to_csv(save, index=False) console.print(f"[green]πŸ’Ύ Data saved to: {save}[/green]") return except Exception as e: console.print(f"[red]❌ Error saving file: {e}[/red]") return if output_format == "json": click.echo(output_data.to_json(orient="records", date_format="iso", indent=2)) else: console.print(output_data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]πŸ’‘ Troubleshooting:[/yellow]") console.print(" β€’ Check your internet connection") console.print(" β€’ Try again with --force-refresh to bypass cache") console.print(" β€’ Visit NISRA website to verify data availability") raise click.Abort() from e
@cli.group()
[docs] def psni(): """PSNI (Police Service of Northern Ireland) data sources. Access official statistics from the Police Service of Northern Ireland including: - Road Traffic Collision statistics (injury collisions, casualties, vehicles) - Police recorded crime statistics (from OpenDataNI) All data is sourced from OpenDataNI under the Open Government Licence v3.0. Geographic breakdowns use 11 Policing Districts aligned with LGDs. """ pass
@psni.command(name="rtc") @click.option("--year", type=int, help="Specific year to retrieve (default: latest)") @click.option( "--data-type", type=click.Choice(["collisions", "casualties", "vehicles", "summary"], case_sensitive=False), default="summary", help="Type of data to retrieve (default: summary)", ) @click.option( "--by", type=click.Choice(["district", "road-user", "year"], case_sensitive=False), help="Group results by dimension", ) @click.option( "--format", "output_format", type=click.Choice(["table", "csv", "json"], case_sensitive=False), default="table", help="Output format (default: table)", ) @click.option("--save", help="Save data to file (specify filename)") @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached")
[docs] def psni_rtc_cmd(year, data_type, by, output_format, save, force_refresh): """PSNI Road Traffic Collision Statistics. Retrieves police-recorded injury road traffic collision data including: - Collision records with date, location, road conditions - Casualty records with severity (fatal/serious/slight), road user type - Vehicle records with type and driver details Args: year: Specific year to retrieve (default: latest) data_type: Type of data to retrieve (collisions, casualties, vehicles, or summary) by: Group results by dimension (district, road-user, or year) output_format: Output format (table, csv, or json) save: Save data to file (specify filename) force_refresh: Force re-download even if cached Examples: Get annual summary for all available years:: bolster psni rtc --data-type summary Get 2024 casualties by district:: bolster psni rtc --year 2024 --data-type casualties --by district Get casualties by road user type:: bolster psni rtc --data-type casualties --by road-user Export collision data to CSV:: bolster psni rtc --year 2024 --data-type collisions --format csv --save collisions.csv Data Notes: - Data covers injury collisions only (not damage-only) - Available from 2013 onwards via OpenDataNI - Severity: Fatal, Serious, Slight - Updated annually (~6 months after year end) Returns: DataFrame with various columns depending on data_type and grouping """ from rich.table import Table from bolster.data_sources.psni import road_traffic_collisions console = Console() try: console.print("\n[bold blue]πŸš— PSNI Road Traffic Collision Statistics[/bold blue]\n") if data_type == "summary" or by == "year": # Annual summary if year: console.print("[yellow]Note: --year ignored for summary view[/yellow]\n") df = road_traffic_collisions.get_annual_summary() title = "Annual RTC Summary" elif by == "district": df = road_traffic_collisions.get_casualties_by_district(year, force_refresh=force_refresh) title = f"Casualties by District ({year or 'latest'})" elif by == "road-user": df = road_traffic_collisions.get_casualties_by_road_user(year, force_refresh=force_refresh) title = f"Casualties by Road User Type ({year or 'latest'})" elif data_type == "collisions": df = road_traffic_collisions.get_collisions(year, force_refresh=force_refresh) title = f"Collision Records ({year or 'latest'})" elif data_type == "casualties": df = road_traffic_collisions.get_casualties(year, force_refresh=force_refresh) title = f"Casualty Records ({year or 'latest'})" elif data_type == "vehicles": df = road_traffic_collisions.get_vehicles(year, force_refresh=force_refresh) title = f"Vehicle Records ({year or 'latest'})" else: df = road_traffic_collisions.get_annual_summary() title = "Annual RTC Summary" console.print(f"[bold]{title}[/bold]\n") if output_format == "table": # Create rich table table = Table(show_header=True, header_style="bold cyan") for col in df.columns: table.add_column(str(col)) for _, row in df.head(50).iterrows(): table.add_row(*[str(v) for v in row.values]) console.print(table) if len(df) > 50: console.print(f"\n[yellow]Showing first 50 of {len(df)} rows[/yellow]") elif output_format == "csv": console.print(df.to_csv(index=False)) elif output_format == "json": console.print(df.to_json(orient="records", indent=2)) if save: if save.endswith(".json"): df.to_json(save, orient="records", indent=2) else: df.to_csv(save, index=False) console.print(f"\n[green]βœ… Saved to {save}[/green]") # Show summary stats if data_type == "summary" or by == "year": total_fatal = df["fatal"].sum() total_casualties = df["casualties"].sum() years_covered = f"{df['year'].min()}-{df['year'].max()}" console.print( f"\n[dim]Data covers {years_covered} | {total_casualties:,} total casualties | {total_fatal:,} fatalities[/dim]" ) except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") raise click.Abort() from e
@psni.command(name="ombudsman") @click.option( "--breakdown", type=click.Choice( ["totals", "by-district", "by-allegation-type", "by-outcome", "quarterly"], case_sensitive=False, ), default="totals", show_default=True, help="Which breakdown to retrieve.", ) @click.option( "--format", "output_format", type=click.Choice(["table", "csv", "json"], case_sensitive=False), default="table", show_default=True, help="Output format.", ) @click.option("--save", metavar="PATH", help="Save output to file.") @click.option("--force-refresh", is_flag=True, help="Bypass cache and re-download.")
[docs] def psni_ombudsman_cmd(breakdown, output_format, save, force_refresh): r"""Police Ombudsman Complaint Statistics. Retrieves official complaint statistics published by the Police Ombudsman for Northern Ireland, covering complaints, allegations, and case outcomes. Available breakdowns: \b totals - Total complaints 2000/01 to present by-district - Complaints by policing district, 2011/12+ by-allegation-type- Allegations by type and subtype, 2011/12+ by-outcome - Complaint closures by outcome, 2011/12+ quarterly - Quarterly complaints, latest 5 financial years Examples: \b bolster psni ombudsman bolster psni ombudsman --breakdown by-district bolster psni ombudsman --breakdown quarterly --format csv bolster psni ombudsman --breakdown by-allegation-type --save allegations.csv """ from rich.table import Table from bolster.data_sources.psni import police_ombudsman console = Console() # Map CLI choice (hyphens) to module parameter (underscores) _breakdown_map = { "totals": "totals", "by-district": "by_district", "by-allegation-type": "by_allegation_type", "by-outcome": "by_outcome", "quarterly": "quarterly", } module_breakdown = _breakdown_map[breakdown.lower()] try: console.print("\n[bold blue]Police Ombudsman Complaint Statistics[/bold blue]\n") df = police_ombudsman.get_latest_complaints(module_breakdown, force_refresh=force_refresh) title = f"Police Ombudsman β€” {breakdown}" console.print(f"[bold]{title}[/bold] ({len(df):,} rows)\n") if output_format == "table": table = Table(show_header=True, header_style="bold cyan") for col in df.columns: table.add_column(str(col)) for _, row in df.head(50).iterrows(): table.add_row(*[str(v) for v in row.values]) console.print(table) if len(df) > 50: console.print(f"\n[yellow]Showing first 50 of {len(df):,} rows[/yellow]") elif output_format == "csv": console.print(df.to_csv(index=False)) elif output_format == "json": console.print(df.to_json(orient="records", indent=2)) if save: if save.endswith(".json"): df.to_json(save, orient="records", indent=2) else: df.to_csv(save, index=False) console.print(f"\n[green]Saved to {save}[/green]") except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") raise click.Abort() from e
@psni.command(name="stop-search") @click.option("--year", help="Filter by financial year, e.g. '2023/24'") @click.option("--district", help="No district breakdown is available in this dataset (ignored with warning)") @click.option( "--format", "output_format", type=click.Choice(["table", "csv", "json"], case_sensitive=False), default="table", help="Output format (default: table)", ) @click.option("--save", help="Save data to file (specify filename)") @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached")
[docs] def psni_stop_search_cmd(year, district, output_format, save, force_refresh): """PSNI Stop and Search Statistics (2017/18 onwards). Retrieves individual stop and search records for Northern Ireland covering: - Financial year and quarter - Legislation used (Misuse of Drugs Act, PACE, Justice & Security Act, etc.) - PACE-specific search reasons (stolen articles, prohibited articles, blade/point, fireworks) - Subject demographics (age group, gender) Note: This dataset does not include a policing district breakdown. All records are at Northern Ireland level only. Examples: Show summary table of all records:: bolster psni stop-search Filter to a specific financial year:: bolster psni stop-search --year 2023/24 Export all records to CSV:: bolster psni stop-search --format csv --save stop_search.csv Export 2024/25 data as JSON:: bolster psni stop-search --year 2024/25 --format json Returns: DataFrame with individual stop and search records. """ from rich.table import Table from bolster.data_sources.psni import stop_and_search console = Console() try: if district: console.print( "[yellow]Note: This dataset has no district-level breakdown. " "The --district filter is not applicable and will be ignored.[/yellow]\n" ) console.print("\n[bold blue]Stop and Search Statistics[/bold blue]\n") df = stop_and_search.get_latest_stop_and_search(force_refresh=force_refresh) if year: df = df[df["financial_year"] == year] if df.empty: available = sorted(df["financial_year"].cat.categories.tolist()) console.print(f"[red]No data found for year '{year}'. Available years: {available}[/red]") return title = f"Stop and Search Records ({year or 'all years'})" console.print(f"[bold]{title}[/bold] β€” {len(df):,} records\n") if output_format == "table": table = Table(show_header=True, header_style="bold cyan") for col in df.columns: table.add_column(str(col)) display_df = df.head(50) for _, row in display_df.iterrows(): table.add_row(*[str(v) for v in row.values]) console.print(table) if len(df) > 50: console.print(f"\n[yellow]Showing first 50 of {len(df):,} rows[/yellow]") elif output_format == "csv": console.print(df.to_csv(index=False)) elif output_format == "json": console.print(df.to_json(orient="records", indent=2)) if save: if save.endswith(".json"): df.to_json(save, orient="records", indent=2) else: df.to_csv(save, index=False) console.print(f"\n[green]Saved to {save}[/green]") # Summary stats years_covered = sorted(df["financial_year"].unique().tolist()) console.print( f"\n[dim]Financial years: {years_covered[0]}–{years_covered[-1]} | " f"{len(df):,} records | " f"Top legislation: {df['legislation'].value_counts().index[0]}[/dim]" ) except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") raise click.Abort() from e
@psni.command(name="pace") @click.option( "--breakdown", type=click.Choice(["stop-search", "arrests"], case_sensitive=False), default="stop-search", help="Which table to retrieve: 'stop-search' (monthly counts) or 'arrests' (quarterly demographics). Default: stop-search.", ) @click.option( "--format", "output_format", type=click.Choice(["table", "csv", "json"], case_sensitive=False), default="table", help="Output format (default: table)", ) @click.option("--save", help="Save data to file (specify filename)") @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached")
[docs] def psni_pace_cmd(breakdown, output_format, save, force_refresh): """PSNI PACE (Police and Criminal Evidence Order) Statistics. Retrieves annual PACE statistics from PSNI Statistics Branch including: - Monthly stop and search counts by reason (stolen articles, offensive weapons, going equipped, fireworks) with associated arrests - Quarterly arrests under PACE by gender and legal-rights requests (solicitor, friend/relative) Args: breakdown: Which table to retrieve ('stop-search' or 'arrests') output_format: Output format (table, csv, or json) save: Save data to file (specify filename) force_refresh: Force re-download even if cached Examples: Get monthly stop & search breakdown:: bolster psni pace --breakdown stop-search Get quarterly arrest demographics as CSV:: bolster psni pace --breakdown arrests --format csv Save arrests data to file:: bolster psni pace --breakdown arrests --save pace_arrests.csv Data Notes: - Single financial year per workbook; PACE_URLS updated each May - Stop & search reasons: Stolen Articles, Offensive Weapon/Blade or Point, Going Equipped/Prohibited Articles, Fireworks - Arrests breakdown: Total, Male, Female, Unknown/Other, Requested friend/relative, Requested solicitor """ from rich.table import Table from bolster.data_sources.psni import pace console = Console() try: breakdown_key = breakdown.replace("-", "_") console.print("\n[bold blue]πŸ” PSNI PACE Statistics[/bold blue]\n") df = pace.get_latest_pace(breakdown=breakdown_key, force_refresh=force_refresh) fy = df["financial_year"].iloc[0] if len(df) > 0 else "unknown" title = f"PACE Stop & Search β€” {fy}" if breakdown == "stop-search" else f"PACE Arrests β€” {fy}" console.print(f"[bold]{title}[/bold]\n") if output_format == "table": table = Table(show_header=True, header_style="bold cyan") for col in df.columns: table.add_column(str(col)) for _, row in df.iterrows(): table.add_row(*[str(v) for v in row.values]) console.print(table) elif output_format == "csv": console.print(df.to_csv(index=False)) elif output_format == "json": console.print(df.to_json(orient="records", indent=2)) if save: if save.endswith(".json"): df.to_json(save, orient="records", indent=2) else: df.to_csv(save, index=False) console.print(f"\n[green]βœ… Saved to {save}[/green]") if breakdown == "stop-search": total_searches = df[df["metric"] == "Searches"]["count"].sum() total_arrests = df[df["metric"] == "Arrests"]["count"].sum() console.print( f"\n[dim]{fy} | {total_searches:,} total searches | {total_arrests:,} arrests following search[/dim]" ) else: annual = df[df["quarter"] == "Annual Total"] total_row = annual[annual["category"] == "Total"] if not total_row.empty: total = int(total_row["count"].iloc[0]) console.print(f"\n[dim]{fy} | {total:,} total PACE arrests[/dim]") except Exception as e: console.print(f"[bold red]❌ Error:[/bold red] {str(e)}", style="red") raise click.Abort() from e
@nisra.command(name="planning-statistics") @click.option( "--dimension", type=click.Choice(["ni", "council", "annual", "council-summary"], case_sensitive=False), default="ni", help=( "Which view to retrieve: 'ni' (quarterly NI-wide, default), " "'council' (per-council quarterly), 'annual' (NI financial-year totals), " "'council-summary' (per-council aggregate)." ), ) @click.option("--financial-year", help="Filter by financial year, e.g. '2024/25'") @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)") @click.option("--summary", is_flag=True, help="Show summary statistics instead of full data")
[docs] def nisra_planning_statistics_cmd(dimension, financial_year, output_format, force_refresh, save, summary): """NISRA Planning Activity Statistics (Department for Infrastructure). Quarterly Northern Ireland planning application statistics: applications received, decided, approved, withdrawn, and approval rate. NI-wide series goes back to Q1 2002/03; council-area breakdowns cover recent quarters across the 11 local councils. Examples: Quarterly NI-wide series (default):: bolster nisra planning-statistics Per-council quarterly data for the latest financial year:: bolster nisra planning-statistics --dimension council --financial-year 2024/25 Annual financial-year totals:: bolster nisra planning-statistics --dimension annual Council summary for 2024/25:: bolster nisra planning-statistics --dimension council-summary --financial-year 2024/25 Save to file:: bolster nisra planning-statistics --save planning.csv Source: https://www.infrastructure-ni.gov.uk/articles/planning-activity-statistics """ console = Console() try: dim = dimension.lower() with console.status("[bold green]Fetching NI planning statistics..."): if dim in ("ni", "annual"): data = nisra_planning.get_latest_data(force_refresh=force_refresh) else: data = nisra_planning.get_latest_council_data(force_refresh=force_refresh) if dim == "annual": data = nisra_planning.get_annual_totals(data) elif dim == "council-summary": data = nisra_planning.get_council_summary(data, financial_year=financial_year) elif financial_year is not None: data = data[data["financial_year"] == financial_year] if data.empty: console.print("[yellow]No data found for the specified filters[/yellow]") return console.print("[green]Planning statistics fetched successfully[/green]") console.print(f"[cyan]Rows: {len(data)}[/cyan]") if summary and dim in ("ni",): total_received = int(data["applications_received"].sum()) total_decided = int(data["applications_decided"].sum()) total_approved = int(data["applications_approved"].sum()) overall_rate = total_approved / total_decided if total_decided else 0.0 console.print(f" Applications received: {total_received:,}") console.print(f" Applications decided: {total_decided:,}") console.print(f" Applications approved: {total_approved:,}") console.print(f" Approval rate: {overall_rate:.1%}") if not save: return if save: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]Saved to: {save}[/green]") return if output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) else: console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]Troubleshooting:[/yellow]") console.print(" - Check your internet connection") console.print(" - Try again with --force-refresh to bypass cache") raise click.Abort() from e
@nisra.command(name="baby-names") @click.option("--year", type=int, default=None, help="Filter by registration year") @click.option( "--sex", type=click.Choice(["Boys", "Girls", "both"], case_sensitive=False), default="both", help="Filter by sex (default: both)", ) @click.option("--top", "top_n", type=int, default=None, help="Show only top N names by rank") @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)")
[docs] def nisra_baby_names_cmd(year, sex, top_n, output_format, force_refresh, save): r"""NISRA Baby Names for Northern Ireland (1997 to present). \b Full historical list of all first forenames given to babies registered in Northern Ireland, with annual rank and count for every name by sex. Examples: Top 10 boys' names in 2024:: bolster nisra baby-names --year 2024 --sex Boys --top 10 All names for 2023 saved to file:: bolster nisra baby-names --year 2023 --save baby_names_2023.csv Full dataset as JSON:: bolster nisra baby-names --format json Source: https://www.nisra.gov.uk/statistics/births/baby-names """ from rich.console import Console console = Console() try: with console.status("[bold green]Downloading NISRA baby names data..."): data = nisra_baby_names.get_baby_names(force_refresh=force_refresh) if year is not None: data = data[data["year"] == year] if data.empty: console.print(f"[yellow]No data found for year {year}[/yellow]") return if sex.lower() != "both": sex_title = sex.title() data = data[data["sex"] == sex_title] if data.empty: console.print(f"[yellow]No data found for sex '{sex_title}'[/yellow]") return if top_n is not None: data = data[data["rank"] <= top_n] console.print("[green]Baby names data retrieved successfully[/green]") console.print(f"[cyan]Rows: {len(data)}[/cyan]") if not data.empty: console.print( f"[dim]Years: {data['year'].min()}–{data['year'].max()} | " f"Sexes: {', '.join(sorted(data['sex'].unique()))}[/dim]" ) if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]Saved to: {save}[/green]") return except PermissionError: console.print(f"[red]Error: Permission denied writing to {save}[/red]") return except Exception as e: console.print(f"[red]Error saving file: {e}[/red]") return if output_format == "json": click.echo(data.to_json(orient="records", indent=2)) else: console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]Troubleshooting:[/yellow]") console.print(" - Check your internet connection") console.print(" - Try again with --force-refresh to bypass cache") raise click.Abort() from e
@nisra.command(name="work-quality") @click.option("--indicator", default=None, help="Filter by indicator name (e.g. 'job_satisfaction')") @click.option("--year", type=int, default=None, help="Filter by year") @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)")
[docs] def nisra_work_quality_cmd(indicator, year, output_format, force_refresh, save): r"""NISRA Work Quality Statistics for Northern Ireland. \b Seventeen indicators of job quality for employees aged 18 and over, drawn from the Labour Force Survey (LFS) and Annual Survey of Hours and Earnings (ASHE). Covers 2020 to present with breakdowns by sex, age group, deprivation quintile, and skill level. Examples: All work quality indicators:: bolster nisra work-quality Filter to job satisfaction indicator:: bolster nisra work-quality --indicator job_satisfaction Data for 2023 saved as JSON:: bolster nisra work-quality --year 2023 --format json --save wq2023.json Source: https://www.nisra.gov.uk/statistics/labour-market-and-social-welfare/work-quality """ from rich.console import Console console = Console() try: with console.status("[bold green]Downloading NISRA work quality data..."): data = nisra_work_quality.get_latest_work_quality(force_refresh=force_refresh) if indicator is not None: data = data[data["indicator"] == indicator] if data.empty: console.print(f"[yellow]No data found for indicator '{indicator}'[/yellow]") all_data = nisra_work_quality.get_latest_work_quality(force_refresh=False) available = all_data["indicator"].unique() console.print(f"[dim]Available indicators: {', '.join(sorted(available))}[/dim]") return if year is not None: data = data[data["year"] == year] if data.empty: console.print(f"[yellow]No data found for year {year}[/yellow]") return console.print("[green]Work quality data retrieved successfully[/green]") console.print(f"[cyan]Rows: {len(data)}[/cyan]") if not data.empty and "year" in data.columns: console.print(f"[dim]Years: {data['year'].min()}–{data['year'].max()}[/dim]") if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]Saved to: {save}[/green]") return except PermissionError: console.print(f"[red]Error: Permission denied writing to {save}[/red]") return except Exception as e: console.print(f"[red]Error saving file: {e}[/red]") return if output_format == "json": click.echo(data.to_json(orient="records", indent=2)) else: console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]Troubleshooting:[/yellow]") console.print(" - Check your internet connection") console.print(" - Try again with --force-refresh to bypass cache") raise click.Abort() from e
@nisra.command(name="public-confidence") @click.option( "--breakdown", type=click.Choice( [ "awareness", "trust-nisra", "trust-civil-service", "trust-ni-assembly", "trust-media", "trust-nisra-statistics", "all-trust", ], case_sensitive=False, ), default="awareness", help="Which breakdown to retrieve (default: awareness)", ) @click.option( "--format", "output_format", type=click.Choice(["table", "csv", "json"], case_sensitive=False), default="table", help="Output format (default: table)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify path)")
[docs] def nisra_public_confidence_cmd(breakdown, output_format, force_refresh, save): r"""NISRA Public Confidence in Official Statistics (PCOS). \b Annual survey measuring public awareness of and trust in NISRA and official statistics, drawn from the Northern Ireland Continuous Household Survey (CHS). Data covers 2009–present for awareness and 2014–present for trust measures. Breakdowns: awareness Awareness of NISRA (2009–present) trust-nisra Trust in NISRA as an institution (2014–present) trust-civil-service Trust in the Civil Service (2014–present) trust-ni-assembly Trust in the NI Assembly/elected bodies (2014–present) trust-media Trust in the Media (2014–present) trust-nisra-statistics Trust in statistics produced by NISRA (2014–present) all-trust All five trust topics combined (adds topic column) Examples: Awareness of NISRA over time:: bolster nisra public-confidence Trust in NISRA statistics:: bolster nisra public-confidence --breakdown trust-nisra-statistics All trust topics as JSON:: bolster nisra public-confidence --breakdown all-trust --format json Save awareness data to CSV:: bolster nisra public-confidence --save awareness.csv Source: https://www.nisra.gov.uk/statistics/people-and-communities/public-awareness-and-trust-confidence-official-statistics-pcos """ from rich.table import Table console = Console() # Normalise hyphenated CLI choice to underscore key expected by the module breakdown_key = breakdown.replace("-", "_") try: with console.status("[bold green]Downloading NISRA public confidence data..."): data = nisra_public_confidence.get_latest_public_confidence( breakdown=breakdown_key, force_refresh=force_refresh, ) console.print("[green]Public confidence data retrieved successfully[/green]") console.print(f"[cyan]Rows: {len(data)}[/cyan]") if not data.empty and "year" in data.columns: console.print(f"[dim]Years: {data['year'].min()}–{data['year'].max()}[/dim]") if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]Saved to: {save}[/green]") return except PermissionError: console.print(f"[red]Error: Permission denied writing to {save}[/red]") return except Exception as e: console.print(f"[red]Error saving file: {e}[/red]") return if output_format == "json": click.echo(data.to_json(orient="records", indent=2)) elif output_format == "csv": console.print(data.to_csv(index=False), end="") else: rich_table = Table(title=f"NISRA Public Confidence β€” {breakdown}") for col in data.columns: rich_table.add_column(col, style="cyan" if col == "year" else "white") for _, row in data.iterrows(): rich_table.add_row(*[str(v) for v in row]) console.print(rich_table) except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]Troubleshooting:[/yellow]") console.print(" - Check your internet connection") console.print(" - Try again with --force-refresh to bypass cache") raise click.Abort() from e
@nisra.command(name="disease-prevalence") @click.option("--register", default=None, help="Filter by register name (case-insensitive substring match)") @click.option( "--format", "output_format", type=click.Choice(["table", "csv", "json"], case_sensitive=False), default="table", help="Output format (default: table)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)") @click.option( "--level", type=click.Choice(["ni", "gp"], case_sensitive=False), default="ni", show_default=True, help="Data level: 'ni' for NI-level summary, 'gp' for GP-practice-level data", ) @click.option("--lcg", default=None, help="Filter by LCG name (only applies with --level gp)")
[docs] def nisra_disease_prevalence_cmd(register, output_format, force_refresh, save, level, lcg): r"""NI Raw Disease Prevalence Statistics (Department of Health). \b Annual disease register sizes and prevalence per 1,000 patients for Northern Ireland, covering 2004/05 to the most recently published year. Data originate from GP clinical disease registers (QOF). Examples: Full NI disease prevalence time series:: bolster nisra disease-prevalence Filter to hypertension register:: bolster nisra disease-prevalence --register hypertension GP-practice-level data for Belfast LCG:: bolster nisra disease-prevalence --level gp --lcg Belfast Export GP data as CSV:: bolster nisra disease-prevalence --level gp --format csv --save gp.csv Latest data as JSON:: bolster nisra disease-prevalence --format json Source: https://www.health-ni.gov.uk/articles/prevalence-statistics """ from rich.table import Table as RichTable console = Console() try: if level == "gp": with console.status("[bold green]Downloading GP-practice disease prevalence data..."): data = nisra_disease_prevalence.get_latest_gp_prevalence(force_refresh=force_refresh) if lcg is not None: mask = data["lcg"].str.contains(lcg, case=False, na=False) data = data[mask] if data.empty: console.print(f"[yellow]No data found for LCG matching '{lcg}'[/yellow]") all_lcgs = nisra_disease_prevalence.get_latest_gp_prevalence()["lcg"].dropna().unique() console.print(f"[dim]Available LCGs: {', '.join(sorted(all_lcgs))}[/dim]") return else: with console.status("[bold green]Downloading NI disease prevalence data..."): data = nisra_disease_prevalence.get_latest_disease_prevalence(force_refresh=force_refresh) if register is not None: mask = data["register"].str.contains(register, case=False, na=False) data = data[mask] if data.empty: console.print(f"[yellow]No data found for register matching '{register}'[/yellow]") if level == "gp": available = nisra_disease_prevalence.get_latest_gp_prevalence()["register"].unique() else: available = nisra_disease_prevalence.get_latest_disease_prevalence()["register"].unique() console.print(f"[dim]Available registers: {', '.join(sorted(available))}[/dim]") return level_label = "GP Practice" if level == "gp" else "NI Summary" console.print("[green]Disease prevalence data retrieved successfully[/green]") if level == "gp": console.print( f"[cyan]Rows: {len(data)} | Practices: {data['practice_code'].nunique()} | " f"Registers: {data['register'].nunique()} | " f"Years: {data['financial_year'].nunique()}[/cyan]" ) else: console.print( f"[cyan]Rows: {len(data)} | Registers: {data['register'].nunique()} | " f"Years: {data['financial_year'].nunique()}[/cyan]" ) if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]Saved to: {save}[/green]") return except PermissionError: console.print(f"[red]Error: Permission denied writing to {save}[/red]") return except Exception as e: console.print(f"[red]Error saving file: {e}[/red]") return if output_format == "json": click.echo(data.to_json(orient="records", indent=2)) elif output_format == "csv": console.print(data.to_csv(index=False), end="") else: # Rich table β€” show a summary view if level == "gp": rich_table = RichTable(title=f"NI Disease Prevalence ({level_label})") rich_table.add_column("Financial Year", style="cyan", width=12) rich_table.add_column("Practice", style="dim", width=8) rich_table.add_column("LCG", style="white", width=14) rich_table.add_column("Register", style="white") rich_table.add_column("Reg. Patients", justify="right", style="green") rich_table.add_column("Prev /1000", justify="right", style="yellow") for _, row in data.head(200).iterrows(): pat = f"{int(row['registered_patients']):,}" if pd.notna(row["registered_patients"]) else "-" prev = f"{row['prevalence_per_1000']:.1f}" if pd.notna(row["prevalence_per_1000"]) else "-" rich_table.add_row( str(row["financial_year"]), str(row["practice_code"]), str(row["lcg"]) if pd.notna(row["lcg"]) else "-", str(row["register"]), pat, prev, ) if len(data) > 200: console.print(f"[dim](showing first 200 of {len(data):,} rows β€” use --save to export all)[/dim]") else: rich_table = RichTable(title=f"NI Disease Prevalence ({level_label})") rich_table.add_column("Financial Year", style="cyan", width=14) rich_table.add_column("Register", style="white") rich_table.add_column("Registered Patients", justify="right", style="green") rich_table.add_column("Prevalence /1000", justify="right", style="yellow") for _, row in data.iterrows(): pat = f"{int(row['registered_patients']):,}" if pd.notna(row["registered_patients"]) else "-" prev = f"{row['prevalence_per_1000']:.1f}" if pd.notna(row["prevalence_per_1000"]) else "-" rich_table.add_row( str(row["financial_year"]), str(row["register"]), pat, prev, ) console.print(rich_table) except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]Troubleshooting:[/yellow]") console.print(" - Check your internet connection") console.print(" - Try again with --force-refresh to bypass cache") raise click.Abort() from e
@nisra.command(name="quarterly-employment-survey") @click.option( "--format", "output_format", type=click.Choice(["table", "csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option( "--adjusted/--unadjusted", default=True, help="Use seasonally adjusted series (default: adjusted)", ) @click.option("--year", type=int, default=None, help="Filter by year") @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)")
[docs] def nisra_qes_cmd(output_format, adjusted, year, force_refresh, save): r"""NISRA Quarterly Employment Survey (QES). \b Employee jobs in Northern Ireland by sector (manufacturing, construction, services, other), Q1 1998 to present. Seasonally adjusted by default. Examples: All QES data (seasonally adjusted):: bolster nisra quarterly-employment-survey Unadjusted series saved as JSON:: bolster nisra quarterly-employment-survey --unadjusted --format json --save qes.json Filter to 2024:: bolster nisra quarterly-employment-survey --year 2024 Source: https://www.nisra.gov.uk/statistics/labour-market-and-social-welfare/quarterly-employment-survey """ from bolster.data_sources.nisra import quarterly_employment_survey as qes_module console = Console() try: with console.status("[bold green]Downloading NISRA Quarterly Employment Survey data..."): data = qes_module.get_latest_qes(force_refresh=force_refresh, adjusted=adjusted) if year is not None: data = data[data["year"] == year] if data.empty: console.print(f"[yellow]No data found for year {year}[/yellow]") return console.print(f"[cyan]πŸ“Š {len(data):,} records[/cyan]") if save: if output_format == "json": data.to_json(save, orient="records", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]βœ… Saved to {save}[/green]") elif output_format == "json": click.echo(data.to_json(orient="records", indent=2)) else: click.echo(data.to_csv(index=False)) except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]Troubleshooting:[/yellow]") console.print(" - Check your internet connection") console.print(" - Try again with --force-refresh to bypass cache") raise click.Abort() from e
@nisra.command(name="claimant-count") @click.option( "--breakdown", type=click.Choice(["headline", "age", "lgd", "pca", "ttwa"], case_sensitive=False), default="headline", help="Data breakdown to retrieve (default: headline)", ) @click.option( "--format", "output_format", type=click.Choice(["table", "csv", "json"], case_sensitive=False), default="table", help="Output format (default: table)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)")
[docs] def nisra_claimant_count_cmd(breakdown, output_format, force_refresh, save): r"""NISRA Claimant Count statistics (UC + JSA). \b Monthly experimental statistics measuring the number of people claiming Universal Credit (UC) or Jobseeker's Allowance (JSA) principally for the reason of being unemployed. Data covers Northern Ireland with multiple geographic and demographic breakdowns. Breakdowns available: headline NI total by sex, seasonally adjusted + non-SA (from 1997) age NI total by age band: 16-24, 25-49, 50+ (from 2013) lgd 11 Local Government Districts (current month snapshot) pca 18 Parliamentary Constituency Areas (current month snapshot) ttwa 10 Travel-to-Work Areas (current month snapshot) Examples: Show latest headline claimant count in a table:: bolster nisra claimant-count Show age breakdown as CSV:: bolster nisra claimant-count --breakdown age --format csv Save LGD data to file:: bolster nisra claimant-count --breakdown lgd --save lgd.csv Export PCA data as JSON:: bolster nisra claimant-count --breakdown pca --format json --save pca.json Source: https://www.nisra.gov.uk/statistics/labour-market-and-social-welfare/claimant-count """ from rich.table import Table from bolster.data_sources.nisra import claimant_count as cc_module console = Console() try: with console.status(f"[bold green]Downloading NISRA claimant count ({breakdown})..."): data = cc_module.get_latest_claimant_count(breakdown=breakdown, force_refresh=force_refresh) console.print(f"[green]Claimant count ({breakdown}) retrieved successfully[/green]") console.print(f"[cyan]Rows: {len(data)}[/cyan]") if not data.empty and "date" in data.columns: min_date = data["date"].min() max_date = data["date"].max() if pd.notna(min_date) and pd.notna(max_date): console.print(f"[dim]Date range: {min_date.strftime('%b %Y')} – {max_date.strftime('%b %Y')}[/dim]") if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", date_format="iso", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]Saved to: {save}[/green]") return except PermissionError: console.print(f"[red]Error: Permission denied writing to {save}[/red]") return except Exception as e: console.print(f"[red]Error saving file: {e}[/red]") return if output_format == "table": table = Table(title=f"Claimant Count β€” {breakdown}", show_header=True, header_style="bold cyan") for col in data.columns: table.add_column(str(col)) for _, row in data.head(50).iterrows(): table.add_row(*[str(v) for v in row.values]) console.print(table) if len(data) > 50: console.print(f"\n[yellow]Showing first 50 of {len(data)} rows[/yellow]") elif output_format == "csv": click.echo(data.to_csv(index=False)) elif output_format == "json": click.echo(data.to_json(orient="records", date_format="iso", indent=2)) except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]Troubleshooting:[/yellow]") console.print(" - Check your internet connection") console.print(" - Try again with --force-refresh to bypass cache") console.print(" - Visit NISRA website to verify data availability") raise click.Abort() from e
@psni.command(name="crime") @click.option( "--format", "output_format", type=click.Choice(["table", "csv", "json"], case_sensitive=False), default="table", help="Output format (default: table)", ) @click.option("--save", help="Save data to file (specify filename)")
[docs] def psni_crime_cmd(output_format, save): r"""PSNI historical crime statistics (Apr 2001–Dec 2021). \b ⚠️ This data source is STALE. The PSNI no longer publishes the structured OpenDataNI crime statistics dataset used by this module. The data covers April 2001 to December 2021 only and will not be updated. For more recent crime data, consult the PSNI's published statistical reports at https://www.psni.police.uk/statistics/ \b Examples: bolster psni crime --format csv --save crime_history.csv bolster psni crime --format json """ from bolster.data_sources.psni import crime_statistics console = Console() try: with console.status("[bold yellow]Loading historical PSNI crime data (Apr 2001–Dec 2021)..."): df = crime_statistics.get_historical_crime_statistics() console.print("[yellow]⚠️ Data covers Apr 2001–Dec 2021 only (source no longer updated)[/yellow]") console.print( f"[cyan]πŸ“Š {len(df):,} records across {df['year_month'].nunique() if 'year_month' in df.columns else '?'} months[/cyan]" ) if save: if output_format == "json": df.to_json(save, orient="records", indent=2) else: df.to_csv(save, index=False) console.print(f"[green]βœ… Saved to {save}[/green]") elif output_format == "json": click.echo(df.to_json(orient="records", indent=2)) else: click.echo(df.to_csv(index=False)) except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") raise click.Abort() from e
@cli.group(name="education")
[docs] def education(): """Education statistics for Northern Ireland. Access official education data from the Department of Education Northern Ireland (DE NI), including pupil suspension and expulsion statistics. """ pass
@education.command(name="suspensions") @click.option("--year", default=None, help="Filter by academic year (e.g. '2023/24')") @click.option( "--format", "output_format", type=click.Choice(["csv", "json"], case_sensitive=False), default="csv", help="Output format (default: csv)", ) @click.option("--force-refresh", is_flag=True, help="Force re-download even if cached") @click.option("--save", help="Save data to file (specify filename)") @click.option("--summary", is_flag=True, help="Show summary statistics instead of full data")
[docs] def education_suspensions_cmd(year, output_format, force_refresh, save, summary): r"""NI Pupil Suspensions and Expulsions (Department of Education). \b Annual suspension statistics for pupils of compulsory school age in Northern Ireland, covering 2011/12 to present. Examples: Full suspension time series:: bolster education suspensions Filter to a single academic year:: bolster education suspensions --year 2023/24 Summary overview:: bolster education suspensions --summary Save as CSV:: bolster education suspensions --save suspensions.csv Source: https://www.education-ni.gov.uk/articles/pupil-suspensions-and-expulsions """ from rich.console import Console console = Console() try: with console.status("[bold green]Downloading NI pupil suspensions data..."): data = education_suspensions.get_latest_suspensions(force_refresh=force_refresh) if year is not None: data = data[data["academic_year"] == year] if data.empty: console.print(f"[yellow]No data found for academic year '{year}'[/yellow]") return console.print("[green]Pupil suspensions data retrieved successfully[/green]") console.print(f"[cyan]Rows: {len(data)}[/cyan]") if not data.empty: console.print(f"[dim]Coverage: {data['academic_year'].iloc[0]} to {data['academic_year'].iloc[-1]}[/dim]") if summary: latest = data.iloc[-1] console.print("\n[bold]Latest year:[/bold]") console.print(f" Academic year: {latest['academic_year']}") console.print(f" Pupils suspended: {latest['pupils_suspended']:,}") pct = latest["pct_pupils_suspended"] if pct is not None and not pd.isna(pct): console.print(f" Percentage suspended: {pct:.1%}") if not save: return if save: try: if output_format == "json" or save.endswith(".json"): data.to_json(save, orient="records", indent=2) else: data.to_csv(save, index=False) console.print(f"[green]Saved to: {save}[/green]") return except PermissionError: console.print(f"[red]Error: Permission denied writing to {save}[/red]") return except Exception as e: console.print(f"[red]Error saving file: {e}[/red]") return if output_format == "json": click.echo(data.to_json(orient="records", indent=2)) else: console.print(data.to_csv(index=False), end="") except Exception as e: console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red") console.print("\n[yellow]Troubleshooting:[/yellow]") console.print(" - Check your internet connection") console.print(" - Try again with --force-refresh to bypass cache") raise click.Abort() from e
@cli.command()
[docs] def list_sources(): """List all available data sources and their descriptions. Shows a comprehensive overview of all data sources available in the Bolster library, organized by category with brief descriptions of what data each source provides. """ click.echo("\nBolster - Available Data Sources") click.echo("=" * 50) click.echo("\nWEATHER & ENVIRONMENT") click.echo(" get-precipitation UK precipitation maps from Met Office API") click.echo(" Requires MET_OFFICE_API_KEY environment variable") click.echo("\nWATER & UTILITIES") click.echo(" water-quality NI water quality data by postcode or zone") click.echo(" Chemical parameters, hardness, compliance info") click.echo("\nGOVERNMENT & POLITICS") click.echo(" ni-executive NI Executive composition and dissolution history") click.echo(" Establishment dates, duration, interregnum periods") click.echo(" ni-elections NI Assembly election results (2016-2022)") click.echo(" Candidates, parties, constituencies, vote counts") click.echo(" nisra deaths NISRA weekly death registrations") click.echo(" Demographics (age/sex), geography (LGDs), place of death") click.echo("\nBUSINESS & PROPERTY") click.echo(" companies-house UK Companies House company data queries") click.echo(" Company search, Farset Labs related companies") click.echo(" ni-house-prices NI house price index data from official sources") click.echo(" Price trends by property type, region, time period") click.echo("\nTRANSPORT") click.echo(" dva DVA monthly test statistics (vehicle, driver, theory)") click.echo(" April 2014 - present, includes --summary dashboard") click.echo("\nENTERTAINMENT & LIFESTYLE") click.echo(" cinema-listings Cineworld movie listings and showtimes") click.echo(" Default: Belfast (site 117), supports other locations") click.echo("\nRSS & FEEDS") click.echo(" rss read Generic RSS/Atom feed reader with filtering") click.echo(" Beautiful terminal output, JSON/CSV export") click.echo(" rss nisra-statistics Browse NISRA publications feed") click.echo(" Research and statistics from NISRA via GOV.UK") click.echo("\nNISRA DATA MODULES (bolster nisra <command>)") click.echo(" nisra feed Browse NISRA RSS publications feed") click.echo(" nisra deaths Weekly death registrations (demographics, LGDs)") click.echo(" nisra births Monthly birth registrations") click.echo(" nisra marriages Monthly marriage registrations") click.echo(" nisra stillbirths Monthly stillbirth registrations") click.echo(" nisra population Annual mid-year population estimates") click.echo(" nisra population-projections Population projections to 2072 (NI + LGD)") click.echo(" nisra migration Migration estimates (derived + official LTI)") click.echo(" nisra labour-market Quarterly Labour Force Survey (employment)") click.echo(" nisra quarterly-employment-survey Employee jobs by sector (QES)") click.echo(" nisra ashe Annual earnings survey (10 dimensions)") click.echo(" nisra composite-index NI Composite Economic Index (NICEI)") click.echo(" nisra index-of-services Quarterly Index of Services") click.echo(" nisra index-of-production Quarterly Index of Production") click.echo(" nisra construction-output Quarterly construction output") click.echo(" nisra cancer-waiting-times Cancer treatment waiting times") click.echo(" nisra emergency-care Emergency care (A&E) 4-hour waiting times") click.echo(" nisra elective-waiting-times Elective/outpatient waiting times") click.echo(" nisra wellbeing Individual wellbeing statistics") click.echo(" nisra work-quality Work quality indicators (17 dimensions)") click.echo(" nisra planning-statistics NI planning applications by council") click.echo(" nisra registrar-general Registrar General quarterly vital statistics") click.echo(" nisra baby-names Baby name registrations (1997–present)") click.echo(" nisra occupancy Tourism hotel/SSA occupancy surveys") click.echo(" nisra visitors Tourism visitor statistics") click.echo("\nPSNI DATA MODULES (bolster psni <command>)") click.echo(" psni rtc Road traffic collisions, casualties, vehicles") click.echo(" psni crime Historical crime statistics (Apr 2001–Dec 2021, stale)") click.echo("\nOTHER DATA MODULES") click.echo(" dva DVA monthly test statistics (vehicle, driver, theory)") click.echo(" education suspensions NI school suspensions and expulsions (DE)") click.echo(" gender-pay-gap UK Gender Pay Gap Reporting service") click.echo("\nUSAGE EXAMPLES") click.echo(" bolster water-quality BT1 5GS # Water quality by postcode") click.echo(" bolster nisra deaths --latest # Latest NISRA deaths data") click.echo(" bolster dva --latest --summary # DVA test statistics summary") click.echo(" bolster rss nisra-statistics # Browse NISRA publications") click.echo(" bolster ni-executive --format json # Executive data as JSON") click.echo(" bolster companies-house farset # Search for Farset companies") click.echo(" bolster ni-elections --election-year 2022 # 2022 election results") click.echo(" bolster cinema-listings --date 2024-03-20 # Movie listings for date") click.echo(" bolster --help # General help") click.echo(" bolster <command> --help # Command-specific help") click.echo(f"\nBolster v{__version__} - Northern Ireland & UK Data Sources")
if __name__ == "__main__": sys.exit(cli()) # pragma: no cover