Source code for operators.s3_geojson_to_athena

"""Operator that converts a GeoJSON file in S3 into a parquet-backed Athena table with a WKT column."""

from config import Config
from airflow.models import BaseOperator
from airflow.utils.decorators import apply_defaults


[docs] class S3GeoJsonToAthenaOperator(BaseOperator): """ Converts a GeoJSON file stored in S3 to an Athena table, stored as parquet The output fill will contain a 'wkt' column in addition to the WKB geometry column The expected input file must be in the geopandas-parseable .geojson format Please follow standard naming and file structure conventions for both the storage of the geojson and the output table in Athena This operation is performed ON THE AIRFLOW SERVER. Adapt as needed to scale. :s3_geojson_path: the path to the GeoJSON in S3 :type s3_geojson_path: str :output_table_name: the name to use for the output table :type output_table_name: str :output_path: the path to the output table in S3 :type output_path: str :geometry_column: the name of the geometry column in the GeoJSON (default is "geometry") :type geometry_column: str :custom_columns: a dictionary of custom columns and their data types (default is None, string assigned for all columns) :type custom_columns: dict """ ui_color = "420a69" @apply_defaults def __init__( self: object, s3_geojson_path: str, output_table_name: str, output_path: str, geometry_column: str = "geometry", custom_columns: dict = None, *args: list, **kwargs: dict, ): super().__init__( *args, **kwargs, ) self.s3_geojson_path = s3_geojson_path self.output_table_name = output_table_name self.output_path = output_path self.custom_columns = custom_columns self.geometry_column = geometry_column
[docs] def execute( self: object, context: dict, ) -> None: try: import awswrangler as wr import geopandas as gpd # Call me a castor canadensis cause I be stacking logs self.log.info(f"Source filepath: {self.s3_geojson_path}") self.log.info(f"Output table name: {self.output_table_name}") self.log.info(f"Destination filepath: {self.output_path}") self.log.info(f"Geometry column: {self.geometry_column}") self.log.info(f"Custom columns: {self.custom_columns}") # Read the GeoJSON directly from S3 into local memory gdf = gpd.read_file(self.s3_geojson_path) # Adjust projection to 4326 gdf.to_crs(epsg=4326, inplace=True) # Cast geometry column to WKT, add to gdf as column gdf["wkt"] = gdf[f"{self.geometry_column}"].to_wkt() # Cast original column to WKB, replace geometry column with WKB gdf[f"{self.geometry_column}"] = gdf[f"{self.geometry_column}"].to_wkb() # Establish column names and types if self.custom_columns is None: col_types_dict = self.custom_columns else: # Assign all columns as string by default col_types_dict = {col: "string" for col in gdf.columns} # Remove geometry column from the dictionary to retain source data type col_types_dict.pop(f"{self.geometry_column}") # Write parquet file to S3, register table in Athena response = wr.s3.to_parquet( df=gdf, path=self.output_path, dataset=True, database=Config.DATA_LAKE_DB, table=self.output_table_name, mode="overwrite", dtype=col_types_dict, ) # Logging self.log.info(f"Saved to S3: {response['paths']}") self.log.info( f"Saved to Athena as: {Config.DATA_LAKE_DB}.{self.output_table_name}" ) except Exception as e: self.log.error(f"Error: {e}") raise e