{"id":1323,"date":"2026-10-22T07:02:06","date_gmt":"2026-10-22T07:02:06","guid":{"rendered":"https:\/\/helpwithmygisassignment.com\/blog\/?p=1323"},"modified":"2026-10-22T07:02:06","modified_gmt":"2026-10-22T07:02:06","slug":"automated-metadata-generation-python","status":"publish","type":"post","link":"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/","title":{"rendered":"Automated Metadata Generation and ISO Standards Compliance with Python"},"content":{"rendered":"<div>\n<div><\/div>\n<div>\n<div>\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_89 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Table of Contents<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-1'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#Automated_Metadata_Generation_and_ISO_Standards_Compliance_with_Python\" >Automated Metadata Generation and ISO Standards Compliance with Python<\/a><ul class='ez-toc-list-level-2' ><li class='ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#1_Introduction\" >1. Introduction<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#2_What_information_can_be_extracted_automatically\" >2. What information can be extracted automatically?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#3_Environment_setup\" >3. Environment setup<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#Install_the_Python_dependencies\" >Install the Python dependencies<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#4_Step_1_%E2%80%94_Read_vector_and_raster_datasets_programmatically\" >4. Step 1 \u2014 Read vector and raster datasets programmatically<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#Reading_a_vector_layer_with_Fiona\" >Reading a vector layer with Fiona<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#Reading_a_raster_with_Rasterio\" >Reading a raster with Rasterio<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#5_Step_2_%E2%80%94_Extract_the_bounding_box_CRS_schema_and_timestamps\" >5. Step 2 \u2014 Extract the bounding box, CRS, schema, and timestamps<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#51_Parse_CRS_information_and_calculate_a_geographic_bounding_box\" >5.1 Parse CRS information and calculate a geographic bounding box<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#52_Extract_attribute_names_and_data_types\" >5.2 Extract attribute names and data types<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#53_Extract_filesystem_timestamps\" >5.3 Extract filesystem timestamps<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#6_Step_3_%E2%80%94_Format_standardized_metadata_as_JSON_and_ISO_XML\" >6. Step 3 \u2014 Format standardized metadata as JSON and ISO XML<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-14\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#61_Define_a_JSON_metadata_profile\" >6.1 Define a JSON metadata profile<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-15\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#62_Create_an_ISO_Metadata_Control_File\" >6.2 Create an ISO Metadata Control File<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-16\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#63_Generate_ISO_19139_XML_using_pygeometa\" >6.3 Generate ISO 19139 XML using pygeometa<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-17\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#7_Step_4_%E2%80%94_Automate_metadata_generation_across_a_directory\" >7. Step 4 \u2014 Automate metadata generation across a directory<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-18\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#Configure_the_workflow\" >Configure the workflow<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-19\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#Full_batch_metadata_generator\" >Full batch metadata generator<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-20\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#Run_the_batch_processor\" >Run the batch processor<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-21\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#8_Validation_and_enterprise_best_practices\" >8. Validation and enterprise best practices<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-22\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#Validate_technical_metadata_before_publication\" >Validate technical metadata before publication<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-23\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#Validate_against_the_intended_ISO_implementation\" >Validate against the intended ISO implementation<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-24\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#Capture_lineage_directly_from_the_processing_pipeline\" >Capture lineage directly from the processing pipeline<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-25\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#Version_the_metadata_profile\" >Version the metadata profile<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-26\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#Integrate_metadata_generation_into_CICD\" >Integrate metadata generation into CI\/CD<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-27\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#9_Conclusion\" >9. Conclusion<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-28\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/#Technical_references\" >Technical references<\/a><\/li><\/ul><\/li><\/ul><\/li><\/ul><\/nav><\/div>\n<h1><span class=\"ez-toc-section\" id=\"Automated_Metadata_Generation_and_ISO_Standards_Compliance_with_Python\"><\/span>Automated Metadata Generation and ISO Standards Compliance with Python<span class=\"ez-toc-section-end\"><\/span><\/h1>\n<h2><span class=\"ez-toc-section\" id=\"1_Introduction\"><\/span>1. Introduction<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-1456\" src=\"https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_z8dnlaz8dnlaz8dn-1024x559.jpg\" alt=\"Diagram illustrating automated metadata generation Python workflows for spatial data and ISO standards compliance\" width=\"1024\" height=\"559\" srcset=\"https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_z8dnlaz8dnlaz8dn-1024x559.jpg 1024w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_z8dnlaz8dnlaz8dn-300x164.jpg 300w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_z8dnlaz8dnlaz8dn-768x419.jpg 768w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_z8dnlaz8dnlaz8dn.jpg 1408w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<p>Mastering <b data-path-to-node=\"7,0\" data-index-in-node=\"11\">Automated Metadata Generation Python<\/b> scripts helps organizations transition from manual documentation to automated<a href=\"https:\/\/helpwithmygisassignment.com\/blog\/export-vector-pdf-svg-gis\/\"> spatial data<\/a> governance. Spatial data pipelines frequently produce valuable datasets without producing the metadata needed to understand, maintain, discover, or reuse them. A workflow may generate a road network, a digital elevation model, a land-cover classification, or a collection of administrative boundaries, yet leave basic questions unanswered: Who created the data? What area does it cover? Which coordinate reference system does it use? When was the dataset updated? What do its attributes mean?<\/p>\n<p>This problem becomes more serious as organizations accumulate hundreds or thousands of spatial files. Manually documenting every output is labor-intensive, and metadata maintenance is often overlooked when processing scripts change frequently.<\/p>\n<p>Automated metadata generation using Python and ISO 19115 concepts helps address this problem by extracting technical properties directly from spatial files and converting them into standardized metadata records. Such automation supports dataset discovery, interoperability, auditability, and data governance.<\/p>\n<p>Two standards are particularly relevant:<\/p>\n<ul>\n<li>\n<div>\n<div>\n<p>ISO 19115-1 defines the conceptual framework for describing geographic information and services, including identification, extent, quality, spatial reference, content, distribution, and other metadata properties.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>FGDC CSDGM is the US Federal Geographic Data Committee&#8217;s Content Standard for Digital Geospatial Metadata. It remains a legacy standard, while FGDC guidance also encourages the use of endorsed ISO geospatial metadata standards.<\/p>\n<div>\n<div><\/div>\n<\/div>\n<\/div>\n<\/div>\n<\/li>\n<\/ul>\n<p>For XML exchange, ISO 19115-3:2023 defines an XML implementation of ISO 19115-1 and ISO 19115-2. The examples below use <code>pygeometa<\/code> to generate ISO 19139 XML, an established encoding used in many existing GIS infrastructures. This is not the same as targeting the newer ISO 19115-3 XML implementation, so the required output schema must be selected according to the receiving organization&#8217;s profile.<\/p>\n<div>\n<div><\/div>\n<\/div>\n<p>This tutorial builds a metadata pipeline that:<\/p>\n<ol>\n<li>\n<div>\n<div>\n<p>Reads vector and raster datasets programmatically.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Extracts spatial bounds, CRS information, schemas, dimensions, feature counts, and filesystem timestamps.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Generates standardized JSON sidecars based on a defined organizational profile.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Creates ISO metadata control files that can be rendered into ISO 19139 XML after the required descriptive information has been reviewed.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Processes entire directories and records failures for investigation.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<\/ol>\n<p>The core principle is straightforward: automate what the dataset can prove, and require human input for what the dataset cannot reliably tell you.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"2_What_information_can_be_extracted_automatically\"><\/span>2. What information can be extracted automatically?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>A useful spatial metadata record combines technical facts with descriptive and governance information.<\/p>\n<div>\n<table>\n<tbody>\n<tr>\n<th>Metadata element<\/th>\n<th>Extraction method<\/th>\n<th>Automation suitability<\/th>\n<\/tr>\n<tr>\n<td>Spatial bounding box<\/td>\n<td>Fiona or Rasterio dataset bounds<\/td>\n<td>High<\/td>\n<\/tr>\n<tr>\n<td>CRS and EPSG code<\/td>\n<td>Dataset CRS parsed using PyProj<\/td>\n<td>High, when CRS metadata is present<\/td>\n<\/tr>\n<tr>\n<td>Geometry type<\/td>\n<td>Vector layer schema<\/td>\n<td>High<\/td>\n<\/tr>\n<tr>\n<td>Attribute names and data types<\/td>\n<td>Fiona schema<\/td>\n<td>High<\/td>\n<\/tr>\n<tr>\n<td>Feature count<\/td>\n<td>Vector layer count<\/td>\n<td>High when supported by the driver<\/td>\n<\/tr>\n<tr>\n<td>Raster dimensions and bands<\/td>\n<td>Rasterio dataset properties<\/td>\n<td>High<\/td>\n<\/tr>\n<tr>\n<td>File size<\/td>\n<td>Python filesystem metadata<\/td>\n<td>High<\/td>\n<\/tr>\n<tr>\n<td>Last-modified timestamp<\/td>\n<td>Filesystem <code>mtime<\/code><\/td>\n<td>High, subject to filesystem semantics<\/td>\n<\/tr>\n<tr>\n<td>File creation timestamp<\/td>\n<td>Platform-specific filesystem properties<\/td>\n<td>Conditional<\/td>\n<\/tr>\n<tr>\n<td>Dataset title<\/td>\n<td>Filename, then curator refinement<\/td>\n<td>Partial<\/td>\n<\/tr>\n<tr>\n<td>Abstract and purpose<\/td>\n<td>Data dictionary, project configuration, or curator<\/td>\n<td>Requires authoritative input<\/td>\n<\/tr>\n<tr>\n<td>Data creation or publication date<\/td>\n<td>Source documentation or data registry<\/td>\n<td>Requires authoritative input<\/td>\n<\/tr>\n<tr>\n<td>Data lineage<\/td>\n<td>Pipeline configuration, processing logs, or project records<\/td>\n<td>Partial<\/td>\n<\/tr>\n<tr>\n<td>License, ownership, and contact information<\/td>\n<td>Organizational data-governance register<\/td>\n<td>Requires authoritative input<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p>This distinction matters because a file&#8217;s last-modified timestamp does not necessarily indicate when its geographic content was collected or created. A GeoTIFF copied to another disk yesterday could contain imagery captured ten years ago.<\/p>\n<p>Similarly, an EPSG code can be extracted only if the source CRS can be identified. A file without trustworthy CRS metadata should be flagged rather than assigned a guessed coordinate system.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"3_Environment_setup\"><\/span>3. Environment setup<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<h3><span class=\"ez-toc-section\" id=\"Install_the_Python_dependencies\"><\/span>Install the Python dependencies<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-1457\" src=\"https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_fboltzfboltzfbol-1024x559.jpg\" alt=\"Developer workspace setting up Python packages for automated metadata generation Python pipelines\" width=\"1024\" height=\"559\" srcset=\"https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_fboltzfboltzfbol-1024x559.jpg 1024w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_fboltzfboltzfbol-300x164.jpg 300w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_fboltzfboltzfbol-768x419.jpg 768w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_fboltzfboltzfbol.jpg 1408w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<p>Create an environment and install the libraries:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>python -m venv metadata_env<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Activate it on Windows:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>metadata_env\\Scripts\\activate<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Or on Linux:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>source metadata_env\/bin\/activate<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Install the packages:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>python -m pip install fiona rasterio geopandas pyproj shapely pandas pyyaml pygeometa<\/code><\/pre>\n<\/div>\n<\/div>\n<p>The primary dependencies are:<\/p>\n<ul>\n<li>\n<div>\n<div>\n<p><code>fiona<\/code> \u2014 reads vector layer bounds, geometry types, attribute schemas, and feature counts.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><code>rasterio<\/code> \u2014 reads raster dimensions, bands, CRS, resolution, and geospatial bounds.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><code>pyproj<\/code> \u2014 normalizes coordinate reference information and extracts EPSG identifiers when available.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><code>pygeometa<\/code> \u2014 generates standards-based metadata from a Metadata Control File (MCF).<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><code>pyyaml<\/code> \u2014 writes and reads structured YAML metadata configuration.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><code>geopandas<\/code> and <code>shapely<\/code> \u2014 useful for additional spatial inspection and validation.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<\/ul>\n<p><code>pygeometa<\/code> supports metadata creation through YAML-based Metadata Control Files and can generate ISO 19139 XML.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"4_Step_1_%E2%80%94_Read_vector_and_raster_datasets_programmatically\"><\/span>4. Step 1 \u2014 Read vector and raster datasets programmatically<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Spatial metadata extraction starts by opening each dataset with the appropriate reader.<\/p>\n<p>For vector data, Fiona exposes the layer schema, CRS, bounds, and feature count. For raster data, Rasterio exposes dimensions, band count, resolution, CRS, and bounds. Rasterio&#8217;s bounding box is derived from its geospatial transformation<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Reading_a_vector_layer_with_Fiona\"><\/span>Reading a vector layer with Fiona<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<div>\n<div>\n<pre dir=\"ltr\"><code>import fionavector_path = \"data\/roads.gpkg\"# A GeoPackage can contain more than one vector layer.layers = fiona.listlayers(vector_path)print(\"Available layers:\", layers)# Read the first layer for demonstration.layer_name = layers[0]with fiona.open(vector_path, layer=layer_name) as src:    print(\"Driver:\", src.driver)    print(\"CRS:\", src.crs)    print(\"Bounds:\", src.bounds)    print(\"Geometry type:\", src.schema[\"geometry\"])    print(\"Feature count:\", len(src))    # Extract the attribute schema.    for field_name, field_type in src.schema[\"properties\"].items():        print(field_name, field_type)<\/code><\/pre>\n<\/div>\n<div><\/div>\n<\/div>\n<p>This code provides the technical foundation for an automated spatial metadata script.<\/p>\n<p>The bounds generally take the form:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>(minx, miny, maxx, maxy)<\/code><\/pre>\n<\/div>\n<\/div>\n<p>These coordinates are expressed in the dataset&#8217;s CRS. They should not automatically be interpreted as longitude and latitude.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Reading_a_raster_with_Rasterio\"><\/span>Reading a raster with Rasterio<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<div>\n<div>\n<pre dir=\"ltr\"><code>import rasterioraster_path = \"data\/elevation.tif\"with rasterio.open(raster_path) as src:    print(\"CRS:\", src.crs)    print(\"Bounds:\", src.bounds)    print(\"Width:\", src.width)    print(\"Height:\", src.height)    print(\"Band count:\", src.count)    print(\"Resolution:\", src.res)    print(\"Data types:\", src.dtypes)    print(\"NoData values:\", src.nodatavals)<\/code><\/pre>\n<\/div>\n<div><\/div>\n<\/div>\n<p>Unlike a vector dataset, a raster generally does not have a feature count. It has dimensions, pixel data types, and band information. A metadata pipeline should preserve those differences rather than forcing every dataset into the same schema.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"5_Step_2_%E2%80%94_Extract_the_bounding_box_CRS_schema_and_timestamps\"><\/span>5. Step 2 \u2014 Extract the bounding box, CRS, schema, and timestamps<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The next stage normalizes the extracted information into a consistent internal representation.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"51_Parse_CRS_information_and_calculate_a_geographic_bounding_box\"><\/span>5.1 Parse CRS information and calculate a geographic bounding box<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>A projected dataset might have coordinates in metres, while a geographic dataset might use longitude and latitude in degrees.<\/p>\n<p>Store the original bounding box together with the CRS in which it is expressed. When possible, also calculate a WGS 84 bounding box for geographic discovery and standards-oriented metadata exchange.<\/p>\n<p>Rasterio&#8217;s <code>transform_bounds()<\/code> transforms bounds between coordinate reference systems while densifying the edges to better account for nonlinear transformations.<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>from pyproj import CRSfrom rasterio.warp import transform_boundsdef extract_crs_metadata(crs_input):    \"\"\"    Normalize a CRS and extract its authority code when available.    \"\"\"    if not crs_input:        return None    try:        crs = CRS.from_user_input(crs_input)        authority = crs.to_authority()        return {            \"authority\": authority[0] if authority else None,            \"code\": authority[1] if authority else None,            \"epsg\": crs.to_epsg(),            \"wkt\": crs.to_wkt()        }    except Exception:        # Retain an explicit indication that CRS parsing failed.        return {            \"authority\": None,            \"code\": None,            \"epsg\": None,            \"wkt\": str(crs_input),            \"parse_warning\": \"CRS could not be normalized\"        }def extract_bounds_metadata(bounds, crs_metadata):    \"\"\"    Store source-CRS bounds and, if possible, WGS 84 bounds.    \"\"\"    if bounds is None or crs_metadata is None:        return {            \"source_crs\": None,            \"wgs84\": None,            \"warning\": \"Missing bounds or source CRS\"        }    minx, miny, maxx, maxy = map(float, bounds)    result = {        \"source_crs\": {            \"minx\": minx,            \"miny\": miny,            \"maxx\": maxx,            \"maxy\": maxy        },        \"wgs84\": None    }    try:        left, bottom, right, top = transform_bounds(            crs_metadata[\"wkt\"],            \"EPSG:4326\",            minx,            miny,            maxx,            maxy,            densify_pts=21        )        result[\"wgs84\"] = {            \"west_longitude\": left,            \"south_latitude\": bottom,            \"east_longitude\": right,            \"north_latitude\": top        }    except Exception as exc:        result[\"warning\"] = (            f\"Geographic bounding box transformation failed: {exc}\"        )    return result<\/code><\/pre>\n<\/div>\n<div><\/div>\n<\/div>\n<p>This function deliberately keeps the original and transformed extents separate. That prevents a common metadata error: labeling projected coordinates as longitude and latitude.<\/p>\n<p>For extremely large extents, datasets crossing the antimeridian, or unusual coordinate systems, inspect the transformed bounding box carefully. A bounding box is an approximation of the geographic footprint, and specialized datasets may need more careful extent processing.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"52_Extract_attribute_names_and_data_types\"><\/span>5.2 Extract attribute names and data types<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Attribute schemas are particularly useful when generating documentation for vector layers.<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>def extract_vector_schema(schema):    \"\"\"    Convert a Fiona schema into a serializable field list.    \"\"\"    fields = []    for field_name, field_type in schema[\"properties\"].items():        fields.append({            \"name\": field_name,            \"source_type\": str(field_type)        })    return {        \"geometry_type\": schema.get(\"geometry\"),        \"fields\": fields    }<\/code><\/pre>\n<\/div>\n<div><\/div>\n<\/div>\n<p>For example, a roads layer might contain fields such as <code>road_id<\/code>, <code>road_name<\/code>, <code>surface_type<\/code>, and <code>length_m<\/code>. The script can extract their names and storage types without manually opening the dataset.<\/p>\n<p>However, technical types do not necessarily explain the meaning of attributes. A field such as <code>class = 2<\/code> still needs a data dictionary describing the meaning of value <code>2<\/code>.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"53_Extract_filesystem_timestamps\"><\/span>5.3 Extract filesystem timestamps<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Python&#8217;s <code>pathlib<\/code> and <code>os.stat()<\/code> allow a pipeline to capture file size and modification times.<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>import osfrom datetime import datetime, timezonedef extract_file_metadata(path):    \"\"\"    Return filesystem facts without treating them as data lineage.    \"\"\"    stat = path.stat()    modified_utc = datetime.fromtimestamp(        stat.st_mtime,        tz=timezone.utc    ).isoformat()    # A reliable filesystem creation timestamp is platform-dependent.    if hasattr(stat, \"st_birthtime\"):        created_timestamp = stat.st_birthtime    elif os.name == \"nt\":        # On Windows, st_ctime represents creation time.        created_timestamp = stat.st_ctime    else:        # On many Unix filesystems, st_ctime means metadata-change time.        created_timestamp = None    created_utc = (        datetime.fromtimestamp(            created_timestamp,            tz=timezone.utc        ).isoformat()        if created_timestamp is not None        else None    )    return {        \"file_name\": path.name,<\/code><\/pre>\n<\/div>\n<div><\/div>\n<\/div>\n<p>The script does not substitute filesystem timestamps for true dataset creation dates. That distinction is essential when preparing metadata records for formal publication or audit.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"6_Step_3_%E2%80%94_Format_standardized_metadata_as_JSON_and_ISO_XML\"><\/span>6. Step 3 \u2014 Format standardized metadata as JSON and ISO XML<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>A maintainable pipeline benefits from separating technical metadata extraction from the final output format.<\/p>\n<p>The internal metadata record can be converted to JSON for automated catalogs and diagnostic review. A separate standards-specific step can use the same extracted facts to populate an ISO metadata template.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"61_Define_a_JSON_metadata_profile\"><\/span>6.1 Define a JSON metadata profile<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>The following is an example of a local JSON metadata structure based on ISO 19115 concepts.<\/p>\n<p>Important: This JSON structure is an organizational sidecar format, not by itself a formally validated ISO JSON encoding. JSON interchange should follow the specific profile and schema required by the receiving platform. ISO 19115-4 concerns a JSON implementation of metadata fundamentals and, as listed by ISO on October 20, 2026, is under publication.<\/p>\n<p>&nbsp;<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>{\r\n  \"profile\": \"organization-spatial-metadata-v1\",\r\n  \"identifier\": \"urn:example:roads:metadata\",\r\n  \"metadata_date_utc\": \"2026-10-20T08:00:00Z\",\r\n  \"resource\": {\r\n    \"title\": \"Road Network\",\r\n    \"type\": \"vector\",\r\n    \"abstract\": \"REVIEW REQUIRED: describe the dataset.\"\r\n  },\r\n  \"spatial\": {\r\n    \"source_crs\": {\r\n      \"authority\": \"EPSG\",\r\n      \"code\": \"32637\"\r\n    },\r\n    \"bbox_source_crs\": {\r\n      \"minx\": 200000.0,\r\n      \"miny\": 9800000.0,\r\n      \"maxx\": 220000.0,\r\n      \"maxy\": 9820000.0\r\n    },\r\n    \"bbox_wgs84\": {\r\n      \"west_longitude\": 36.0,\r\n      \"south_latitude\": -1.8,\r\n      \"east_longitude\": 36.2,\r\n      \"north_latitude\": -1.6\r\n    }\r\n  },\r\n  \"content\": {\r\n    \"feature_count\": 15230,\r\n    \"geometry_type\": \"LineString\",\r\n    \"fields\": [\r\n      {\r\n        \"name\": \"road_id\",\r\n        \"source_type\": \"int\"\r\n      },\r\n      {\r\n        \"name\": \"road_name\",\r\n        \"source_type\": \"str\"\r\n      }\r\n    ]\r\n  },\r\n  \"file\": {\r\n    \"file_size_bytes\": 1250000,\r\n    \"filesystem_modified_utc\": \"2026-10-19T14:20:00+00:00\",\r\n    \"filesystem_created_utc\": null\r\n  }\r\n}<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Illustrative example only; the displayed coordinates, counts, and dates are not extracted from a real dataset.<\/p>\n<p>A JSON profile makes it easy to index metadata, compare datasets, detect missing CRS values, and generate reports. By assigning the profile a version number, an organization can update its field definitions without silently changing existing metadata records.<\/p>\n<p>The generation code can then serialize the record:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>import jsonfrom pathlib import Pathdef write_json_metadata(metadata, output_path):    \"\"\"    Write a UTF-8 JSON metadata sidecar.    \"\"\"    output_path = Path(output_path)    output_path.parent.mkdir(parents=True, exist_ok=True)    with output_path.open(\"w\", encoding=\"utf-8\") as file:        json.dump(            metadata,            file,            indent=2,            ensure_ascii=False        )<\/code><\/pre>\n<\/div>\n<div><\/div>\n<\/div>\n<h3><span class=\"ez-toc-section\" id=\"62_Create_an_ISO_Metadata_Control_File\"><\/span>6.2 Create an ISO Metadata Control File<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>For ISO XML, <code>pygeometa<\/code> uses a Metadata Control File (MCF). The MCF separates metadata content from the particular schema used to render it.<\/p>\n<p>A simplified MCF template might look like this:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>mcf:\r\n  version: 2.0\r\n\r\nmetadata:\r\n  identifier: \"REPLACE-WITH-STABLE-RECORD-UUID\"\r\n  language: en\r\n  charset: utf8\r\n  hierarchylevel: dataset\r\n  dates:\r\n    creation: \"2026-10-20\"\r\n\r\nspatial:\r\n  datatype: vector\r\n  geomtype: curve\r\n\r\nidentification:\r\n  title:\r\n    en: \"Road Network\"\r\n  abstract:\r\n    en: \"REVIEW REQUIRED: describe purpose and content.\"\r\n  dates:\r\n    creation: \"REVIEW REQUIRED: enter the actual dataset date.\"\r\n  url: \"REPLACE-WITH-DATASET-URL\"\r\n  rights:\r\n    en: \"REVIEW REQUIRED: enter the correct rights statement.\"\r\n    url: \"REPLACE-WITH-LICENSE-OR-RIGHTS-URL\"\r\n  status: completed\r\n  accessconstraints: otherRestrictions\r\n  extents:\r\n    spatial:\r\n      - bbox: [36.0, -1.8, 36.2, -1.6]\r\n        crs: 4326\r\n\r\ncontact:\r\n  pointOfContact:\r\n    organization: \"REPLACE-WITH-ORGANIZATION\"\r\n    url: \"REPLACE-WITH-ORGANIZATION-URL\"\r\n    individualname: \"REPLACE-WITH-RESPONSIBLE-PERSON\"\r\n    positionname: \"REPLACE-WITH-ROLE\"\r\n    phone: \"REPLACE-WITH-PHONE\"\r\n    fax: \"REPLACE-WITH-FAX-OR-APPROVED-VALUE\"\r\n    address: \"REPLACE-WITH-ADDRESS\"\r\n    city: \"REPLACE-WITH-CITY\"\r\n    administrativearea: \"REPLACE-WITH-REGION\"\r\n    postalcode: \"REPLACE-WITH-POSTAL-CODE\"\r\n    country: \"REPLACE-WITH-COUNTRY\"\r\n    email: \"REPLACE-WITH-CONTACT-EMAIL\"<\/code><\/pre>\n<\/div>\n<\/div>\n<p>This template shows the separation between automatically extracted properties, such as the extent, and the descriptive properties requiring an authoritative source.<\/p>\n<p>The date under <code>metadata.dates.creation<\/code> refers to the creation of the metadata record. The date under <code>identification.dates.creation<\/code> refers to creation of the resource itself. They are not interchangeable. The second date must come from an authoritative data record or be reviewed before publication.<\/p>\n<p>The bounding box provided to the MCF is expressed in WGS 84 coordinate order:<\/p>\n<p><code>[west longitude, south latitude, east longitude, north latitude]<\/code><\/p>\n<p>The corresponding source-CRS bounding box should also be retained in the internal JSON sidecar.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"63_Generate_ISO_19139_XML_using_pygeometa\"><\/span>6.3 Generate ISO 19139 XML using pygeometa<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>After the MCF is populated with valid contact, rights, resource-date, and identification information, generate the XML document:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code># Validate the Metadata Control File.\r\npygeometa metadata validate road_network.mcf.yml\r\n\r\n# Generate ISO 19139 XML.\r\npygeometa metadata generate road_network.mcf.yml \\\r\n  --schema=iso19139 \\\r\n  --output=road_network_metadata.xml<\/code><\/pre>\n<\/div>\n<\/div>\n<p>The generated XML uses the selected <code>pygeometa<\/code> schema implementation rather than relying on manually assembled XML tags. This reduces the risk of incorrect namespace structures or missing standard-specific elements.<\/p>\n<p>The generated XML should still be checked against the intended implementation schema and any organizational or national metadata profile. A valid MCF is not, by itself, proof that the resulting XML satisfies every requirement of the target metadata profile.<\/p>\n<div>\n<div><\/div>\n<\/div>\n<p>If the consuming system specifically requires ISO 19115-3:2023, configure a generator and validator that support that XML implementation. Do not label ISO 19139 output as ISO 19115-3 XML simply because both are associated with ISO geographic metadata.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"7_Step_4_%E2%80%94_Automate_metadata_generation_across_a_directory\"><\/span>7. Step 4 \u2014 Automate metadata generation across a directory<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The following script combines the extraction functions into a batch workflow. It processes vector and raster files, saves JSON sidecars, and prepares MCF files for XML generation. When an authoritative ISO profile is configured, it can also generate ISO 19139 XML.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Configure_the_workflow\"><\/span>Configure the workflow<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Create a folder structure such as:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>gis_project\/\r\n\u251c\u2500\u2500 spatial_data\/\r\n\u2502   \u251c\u2500\u2500 roads.gpkg\r\n\u2502   \u251c\u2500\u2500 boundaries.geojson\r\n\u2502   \u2514\u2500\u2500 elevation.tif\r\n\u251c\u2500\u2500 metadata_output\/\r\n\u2514\u2500\u2500 metadata_harvester.py<\/code><\/pre>\n<\/div>\n<\/div>\n<p>The script below assumes that <code>metadata_output<\/code> is separate from <code>spatial_data<\/code>, which prevents generated metadata from being mistaken for new input data.<\/p>\n<p>It handles common file-based vector formats and raster formats. For multi-layer GeoPackages, it creates one metadata record per vector layer.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Full_batch_metadata_generator\"><\/span>Full batch metadata generator<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Save the following as <code>metadata_harvester.py<\/code>.<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>import osimport jsonimport loggingimport uuidfrom datetime import datetime, timezonefrom pathlib import Pathimport fionaimport rasterioimport yamlfrom pyproj import CRSfrom rasterio.warp import transform_bounds# ---------------------------------------------------------# Configuration# ---------------------------------------------------------DATA_ROOT = Path(\"spatial_data\")OUTPUT_ROOT = Path(\"metadata_output\")VECTOR_EXTENSIONS = {    \".shp\", \".gpkg\", \".geojson\", \".json\",    \".gml\", \".kml\", \".fgb\"}RASTER_EXTENSIONS = {    \".tif\", \".tiff\", \".img\", \".jp2\",    \".vrt\", \".asc\"}# Enter true organizational metadata before XML publication.# The default values intentionally leave publication disabled.ISO_PROFILE = {    \"resource_url\": \"\",    \"resource_date\": \"\",   # Actual resource creation\/publication date.    \"abstract\": \"\",    \"rights\": \"\",    \"rights_url\": \"\",    \"lineage\": \"\",    \"contact\": {        \"organization\": \"\",        \"url\": \"\",        \"individualname\": \"\",        \"positionname\": \"\",        \"phone\": \"\",        \"fax\": \"\",        \"address\": \"\",        \"city\": \"\",        \"administrativearea\": \"\",        \"postalcode\": \"\",        \"country\": \"\",        \"email\": \"\"    }}<\/code><\/pre>\n<\/div>\n<div><\/div>\n<\/div>\n<h3><span class=\"ez-toc-section\" id=\"Run_the_batch_processor\"><\/span>Run the batch processor<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>From the project directory, execute:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>python metadata_harvester.py<\/code><\/pre>\n<\/div>\n<\/div>\n<p>With the default settings, the script produces JSON sidecars and MCF drafts but does not publish ISO XML because the authoritative governance profile is intentionally incomplete.<\/p>\n<p>A possible output structure is:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>metadata_output\/\r\n\u251c\u2500\u2500 roads__transport.mcf.yml\r\n\u251c\u2500\u2500 roads__transport.metadata.json\r\n\u251c\u2500\u2500 boundaries__admin.mcf.yml\r\n\u251c\u2500\u2500 boundaries__admin.metadata.json\r\n\u251c\u2500\u2500 elevation.metadata.json\r\n\u2514\u2500\u2500 elevation.mcf.yml<\/code><\/pre>\n<\/div>\n<\/div>\n<p>For a GeoPackage containing multiple layers, each layer receives its own metadata record. For a raster, the record contains its dimensions, number of bands, pixel data types, CRS, extent, and resolution rather than a vector feature count.<\/p>\n<p>The script logs successful records and processing failures. In a production pipeline, send those logs to a centralized logging service or retain them as build artifacts.<\/p>\n<p>Important review point: The MCF drafts intentionally mark unknown descriptive details for review. Do not publish them unchanged. Populate the responsible organization, contact information, rights, resource dates, abstract, lineage, and correct dataset URL. Set <code>GENERATE_ISO19139_XML = True<\/code> only after configuring an authoritative profile and reviewing the required fields. For datasets with different resource dates, use a per-dataset date registry instead of applying one date to every file.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"8_Validation_and_enterprise_best_practices\"><\/span>8. Validation and enterprise best practices<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Automating metadata creation is only one component of metadata governance. A reliable enterprise process also needs validation, provenance, change management, and clear responsibility for maintaining descriptive content.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Validate_technical_metadata_before_publication\"><\/span>Validate technical metadata before publication<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>At minimum, check that:<\/p>\n<ul>\n<li>\n<div>\n<div>\n<p>A spatial dataset has valid bounds.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>The source CRS is present or explicitly flagged as unknown.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Geographic bounding boxes are correctly transformed.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Vector geometry types and attribute schemas can be read.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Counts and dimensions are plausible.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Filesystem dates are not mislabeled as acquisition or resource-creation dates.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Every generated record has a stable identifier within the organization&#8217;s identifier policy.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<\/ul>\n<p>The example script creates a draft when the source data lacks a geographic extent. Such cases should be routed to review rather than published as complete spatial metadata.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Validate_against_the_intended_ISO_implementation\"><\/span>Validate against the intended ISO implementation<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>ISO standards describe more than a list of fields. Their implementations and organizational profiles define the expected encoding, constraints, mandatory elements, code lists, and requirements for individual metadata records.<\/p>\n<p>The ISO 19115-3:2023 implementation includes XML schemas and Schematron rules for constraints that XML schema validation alone does not cover.<\/p>\n<div>\n<div><\/div>\n<\/div>\n<p>A production quality-control stage should therefore distinguish:<\/p>\n<div>\n<table>\n<tbody>\n<tr>\n<th>Validation level<\/th>\n<th>What it establishes<\/th>\n<\/tr>\n<tr>\n<td>Parsing<\/td>\n<td>Metadata JSON, YAML, or XML is readable<\/td>\n<\/tr>\n<tr>\n<td>Structural validation<\/td>\n<td>Required fields and organizational data types are present<\/td>\n<\/tr>\n<tr>\n<td>Schema validation<\/td>\n<td>XML or JSON satisfies the selected schema<\/td>\n<\/tr>\n<tr>\n<td>Profile validation<\/td>\n<td>Organization-specific rules and controlled vocabularies are satisfied<\/td>\n<\/tr>\n<tr>\n<td>Governance review<\/td>\n<td>Rights, lineage, purpose, and responsible parties are accurate<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p>A record can pass structural validation and still fail a profile requirement or contain inaccurate descriptive information.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Capture_lineage_directly_from_the_processing_pipeline\"><\/span>Capture lineage directly from the processing pipeline<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>The most reliable time to document data lineage is while the data is being generated.<\/p>\n<p>For example, a DEM-processing workflow already knows which source raster was used, which interpolation method was applied, what parameters were selected, and which software version performed the operation. Capturing these details at runtime is more dependable than trying to reconstruct them from the output file later.<\/p>\n<p>Useful lineage information includes source dataset identifiers, processing timestamps, software and version numbers, transformation parameters, data-quality checks, and references to reproducible processing scripts.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Version_the_metadata_profile\"><\/span>Version the metadata profile<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Treat the metadata profile as a maintained software interface. Keep its definition in version control, define required and optional fields explicitly, and document changes to field names or meanings.<\/p>\n<p>The profile should also specify how identifiers are generated, how dates are interpreted, which CRS representations are allowed, and how missing values should be reported.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Integrate_metadata_generation_into_CICD\"><\/span>Integrate metadata generation into CI\/CD<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>For recurring spatial ETL workflows, metadata creation should run automatically when a dataset is produced or updated.<\/p>\n<p>A typical process is:<\/p>\n<div>\n<div>\n<p>1. Spatial data processing<\/p>\n<p>Produce raster and vector outputs.<\/p>\n<\/div>\n<div>\n<p>2. Metadata harvesting<\/p>\n<p>Extract extent, CRS, schema, counts, and timestamps.<\/p>\n<\/div>\n<div>\n<p>3. Validation and review<\/p>\n<p>Check schemas, descriptive content, lineage, and rights.<\/p>\n<\/div>\n<div>\n<p>4. Standards-based export<\/p>\n<p>Generate XML or JSON for the selected profile.<\/p>\n<\/div>\n<div>\n<p>5. Publication and registry update<\/p>\n<p>Publish validated records alongside discoverable datasets.<\/p>\n<\/div>\n<\/div>\n<p>The pipeline should fail or quarantine a record if a mandatory requirement is not satisfied. Metadata generation should not silently fabricate a CRS, responsible party, creation date, license, or processing history just to make a file pass validation.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"9_Conclusion\"><\/span>9. Conclusion<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Automated metadata generation with Python reduces repetitive documentation work and makes spatial data pipelines more consistent. Fiona and Rasterio supply technical facts about vector and raster datasets; PyProj helps normalize coordinate systems and spatial extents; and YAML-based metadata control files provide a maintainable path to standards-oriented XML generation.<\/p>\n<p>The strongest approach is to separate automatic extraction from human-authored governance information, validate each metadata record against its intended standard and profile, and integrate these checks into the same pipeline that produces the spatial data.<\/p>\n<p>For organizations managing large GIS inventories, this turns metadata from a neglected final task into a reproducible part of data production, quality assurance, and publication.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Technical_references\"><\/span>Technical references<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<ul>\n<li>\n<div>\n<div>\n<p><a href=\"https:\/\/www.iso.org\/standard\/53798.html?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">ISO 19115-1 \u2014 Geographic information: Metadata fundamentals<\/a><\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><a href=\"https:\/\/www.iso.org\/standard\/80874.html?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">ISO 19115-3:2023 \u2014 XML schema implementation<\/a><\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><a href=\"https:\/\/www.fgdc.gov\/metadata\/geospatial-metadata-standards\/?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">FGDC \u2014 Geospatial metadata standards and guidelines<\/a><\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><a href=\"https:\/\/geopython.github.io\/pygeometa\/tutorial\/?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">pygeometa \u2014 Metadata generation tutorial<\/a><\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><a href=\"https:\/\/geopython.github.io\/pygeometa\/reference\/mcf\/?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">pygeometa \u2014 Metadata Control File reference<\/a><\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><a href=\"https:\/\/rasterio.readthedocs.io\/en\/stable\/api\/rasterio.warp.html?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">Rasterio \u2014 Geospatial coordinate transformations<\/a><\/p>\n<\/div>\n<\/div>\n<\/li>\n<\/ul>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Automated Metadata Generation and ISO Standards Compliance with Python 1. Introduction Mastering Automated Metadata Generation Python scripts helps organizations transition from manual documentation to automated spatial data governance. Spatial data pipelines frequently produce valuable datasets without producing the metadata needed to understand, maintain, discover, or reuse them. A workflow may generate a road network, a &#8230; <a title=\"Automated Metadata Generation and ISO Standards Compliance with Python\" class=\"read-more\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/automated-metadata-generation-python\/\" aria-label=\"Read more about Automated Metadata Generation and ISO Standards Compliance with Python\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":1457,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1,4],"tags":[],"class_list":["post-1323","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","category-gis-articles"],"_links":{"self":[{"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/posts\/1323","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/comments?post=1323"}],"version-history":[{"count":4,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/posts\/1323\/revisions"}],"predecessor-version":[{"id":1458,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/posts\/1323\/revisions\/1458"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/media\/1457"}],"wp:attachment":[{"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/media?parent=1323"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/categories?post=1323"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/tags?post=1323"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}