Skip to main content

RSXML Python Package

The Riverscapes Consortium requires all data uploaded to the Data Exchange to be accompanied by a project.rs.xml metadata file. Writing this XML by hand is error-prone. The rsxml Python package provides typed Python classes that generate valid, schema-compliant XML files automatically.

rsxml is free, open-source, and available on PyPI:

pip install rsxml

Source code and examples: github.com/Riverscapes/rsxml-python

info

rsxml targets the V2 Riverscapes Project schema. The V2 XSD is published at: https://xml.riverscapes.net/Projects/XSD/V2/RiverscapesProject.xsd

Installation

pip install rsxml
# or with uv:
uv add rsxml

Requires Python 3.9 or newer.

Quick Start — Creating a Project

The following example creates a minimal but complete project.rs.xml file:

from datetime import datetime
from rsxml.project_xml import (
BoundingBox, Coords, Dataset, Geopackage, GeoPackageDatasetTypes,
GeopackageLayer, Meta, MetaData, Project, ProjectBounds, Realization,
)

project = Project(
name="My VBET Project",
proj_path="/data/my_project/project.rs.xml",
project_type="VBET",
summary="Valley Bottom Extraction for Big Creek HUC10",
meta_data=MetaData(values=[
Meta("ModelVersion", "1.0.0"),
Meta("HUC", "1706020406"),
Meta("Watershed", "Big Creek"),
]),
bounds=ProjectBounds(
centroid=Coords(lng=-115.5, lat=44.2), # NOTE: lng first
bounding_box=BoundingBox(
minLng=-116.0, minLat=43.8,
maxLng=-115.0, maxLat=44.6,
),
filepath="project_bounds.geojson",
),
realizations=[
Realization(
xml_id="REALIZATION1",
name="VBET Run",
product_version="1.0.0",
date_created=datetime.now(),
inputs=[
Dataset(xml_id="DEM", name="Digital Elevation Model",
path="inputs/dem.tif", ds_type="Raster"),
Geopackage(
xml_id="INPUTS_GPKG", name="Inputs",
path="inputs/inputs.gpkg",
layers=[
GeopackageLayer(
lyr_name="network",
name="Stream Network",
ds_type=GeoPackageDatasetTypes.VECTOR,
),
],
),
],
outputs=[
Geopackage(
xml_id="VBET_OUTPUTS", name="VBET Outputs",
path="outputs/vbet.gpkg",
layers=[
GeopackageLayer(
lyr_name="vbet_full",
name="Valley Bottom",
ds_type=GeoPackageDatasetTypes.VECTOR,
),
],
),
],
)
],
)

project.write()

Loading an Existing Project

from rsxml.project_xml import Project

project = Project.load_project("/data/my_project/project.rs.xml")
print(project.name)
print(project.project_type)

# Add new metadata
if not project.meta_data.find_meta("ProcessedDate"):
project.meta_data.add_meta("ProcessedDate", "2026-05-01", type="isodate")

project.write()

Key Classes

Project

The top-level class, maps to the <Project> XML root.

ParameterTypeRequiredDescription
namestrHuman-readable project name
proj_pathstrFull path to the output project.rs.xml
project_typestrMachine code e.g. VBET, BRAT, RSContext
summarystrOne-line description
descriptionstrLonger description
boundsProjectBoundsGeographic extent (strongly recommended)
meta_dataMetaDataProject-level key/value metadata
realizationslist[Realization]The analysis runs
common_datasetslist[Dataset]Datasets shared across realizations
warning

The library will emit a warning during write() if ModelVersion is not present in the project metadata. Always include it:

Meta("ModelVersion", "1.0.0")

Realization

One "run" of your analysis tool. A project should have at least one.

ParameterTypeRequiredDescription
xml_idstrUnique identifier within the project (e.g. REALIZATION1)
namestrHuman-readable name
product_versionstrTool version string in X.Y.Z format
date_createddatetimeMust be a datetime object (not a string)
inputslistInput data consumed by the tool
intermediateslistIntermediate products
outputslistFinal outputs
analyseslist[Analysis]Analysis results with metrics
meta_dataMetaDataRealization-level metadata

Dataset

Represents a single file (raster, vector, CSV, report, etc.).

Dataset(
xml_id="DEM",
name="Digital Elevation Model",
path="inputs/dem.tif", # relative path, forward slashes, no leading /
ds_type="Raster", # becomes the XML tag: <Raster id="DEM">
ds_type_attr="context", # optional type="" attribute for business logic filtering
summary="10m NED DEM",
description="...",
url="https://...", # optional source URL
ext_ref="...", # reference to dataset in another project (see below)
meta_data=MetaData(...),
)

Common ds_type values (becomes the XML element tag):

ds_typeXML elementUse for
"Raster"<Raster>GeoTIFF, other rasters
"DEM"<DEM>Digital elevation models
"Vector"<Vector>Shapefiles
"Geopackage"<Geopackage>Use the Geopackage class instead
"CSV"<CSV>CSV files
"HTMLFile"<HTMLFile>HTML reports
"LogFile"<LogFile>Use the Log class instead
"PDF"<PDF>PDF files

Path rules: relative to the project directory, forward slashes, no leading slash, max 256 characters.

Geopackage

Subclass of Dataset for GeoPackage files, with an explicit list of layers.

Geopackage(
xml_id="OUTPUTS",
name="Outputs GeoPackage",
path="outputs/outputs.gpkg",
layers=[
GeopackageLayer(
lyr_name="valley_bottom", # exact layer name inside the .gpkg
name="Valley Bottom",
ds_type=GeoPackageDatasetTypes.VECTOR,
lyr_type="vbet_full", # optional: business logic type tag
),
GeopackageLayer(
lyr_name="channel_area",
name="Channel Area",
ds_type=GeoPackageDatasetTypes.VECTOR,
),
GeopackageLayer(
lyr_name="elevation_stats",
name="Elevation Statistics",
ds_type=GeoPackageDatasetTypes.DATATABLE,
),
],
)

GeoPackageDatasetTypes constants: .VECTOR, .RASTER, .DATATABLE

MetaData and Meta

MetaData is a container for Meta key-value pairs.

MetaData(values=[
Meta("ModelVersion", "1.0.0"),
Meta("HUC", "1706020406"),
Meta("DateRun", "2026-05-01T12:00:00", type="isodate"),
Meta("SourceUrl", "https://nhd.usgs.gov/", type="url"),
Meta("LockedKey", "value", locked=True), # not editable via Data Exchange UI
])

Valid type values: guid, url, filepath, image, video, isodate, timestamp, float, boolean, int, richtext, markdown, json, hidden

Important:

  • add_meta() raises ValueError if the key already exists — check first with find_meta()
  • Keys must be unique within a MetaData block
# Safe pattern for updating metadata
if not project.meta_data.find_meta("RunDate"):
project.meta_data.add_meta("RunDate", "2026-05-01", type="isodate")

ProjectBounds

ProjectBounds(
centroid=Coords(lng=-115.5, lat=44.2), # longitude FIRST (not lat/lng)
bounding_box=BoundingBox(
minLng=-116.0, minLat=43.8,
maxLng=-115.0, maxLat=44.6,
),
filepath="project_bounds.geojson", # relative path to WGS84 GeoJSON polygon
)

The project_bounds.geojson file must be in WGS84 (EPSG:4326), must be under 200 KB, and should be a simple polygon (avoid complex concave hulls).

Analysis

For including summary metrics alongside your outputs:

from rsxml.project_xml import Analysis, MetaData, Meta

analysis = Analysis(
xml_id="ANALYSIS1",
name="Valley Bottom Summary",
metrics=MetaData(values=[
Meta("ValleyBottomArea", "1234.5", type="float"),
Meta("ChannelLength", "5678.9", type="float"),
]),
products=[
Dataset(xml_id="REPORT", name="Report",
path="outputs/report.html", ds_type="HTMLFile"),
],
)

Referencing Datasets from Another Project

Use ext_ref to indicate that a dataset originated from another project in the Data Exchange:

Dataset(
xml_id="DEM",
name="NED 10m DEM",
path="inputs/dem.tif",
ds_type="Raster",
ext_ref="f23b187a-537b-4dd0-8b71-4b7c4a6e9747:"
"Project/Realizations/Realization#REALIZATION1/Inputs/Raster#DEM",
)

The format is {project-guid}:{rsXPath} where the XPath uses #id notation to select elements.

Logging and Progress Bars

rsxml includes a shared logging utility used throughout the Riverscapes tools ecosystem:

from rsxml import Logger, ProgressBar

log = Logger("MyTool")
log.setup(log_path="/data/output/run.log", verbose=True)

log.title("== Starting My Tool ==")
log.info("Processing watershed 1706020406")
log.warning("Missing optional input, using defaults")
log.error("Failed to read DEM", exception=e)

# Progress bar for loops
total = len(features)
bar = ProgressBar(total=total, text="Processing features")
for i, feature in enumerate(features):
# ... do work ...
bar.update(i + 1)
bar.finish()

Validating a Project File

from rsxml.validation import validate_project_file

valid, errors = validate_project_file("/data/my_project/project.rs.xml")
if not valid:
for error in errors:
print(error)

Requires lxml to be installed (pip install lxml).

Common Patterns

Standard tool entry point

import argparse
from datetime import datetime
from rsxml import Logger
from rsxml.project_xml import Project, MetaData, Meta, Realization, Dataset, ProjectBounds, Coords, BoundingBox

def main():
parser = argparse.ArgumentParser()
parser.add_argument("project_path", help="Path to output project.rs.xml")
parser.add_argument("input_dem", help="Path to input DEM")
parser.add_argument("--verbose", action="store_true")
args = parser.parse_args()

log = Logger("MyTool")
log.setup(verbose=args.verbose)
log.title("My Tool v1.0.0")

project = Project(
name="My Analysis",
proj_path=args.project_path,
project_type="MYTOOL",
meta_data=MetaData(values=[
Meta("ModelVersion", "1.0.0"),
]),
realizations=[
Realization(
xml_id="REALIZATION1",
name="Run",
product_version="1.0.0",
date_created=datetime.now(),
inputs=[
Dataset(xml_id="DEM", name="Input DEM",
path="inputs/dem.tif", ds_type="Raster"),
],
outputs=[
Dataset(xml_id="OUTPUT", name="Output Layer",
path="outputs/result.tif", ds_type="Raster"),
],
)
],
)

# ... run your analysis ...

project.write()
log.info(f"Written to: {args.project_path}")

if __name__ == "__main__":
main()

Removing the Warehouse tag before re-uploading

New projects should not have a <Warehouse> tag. If copying an existing project as a template, remove it:

project = Project.load_project(template_path)
project.warehouse = None # remove warehouse tag
project.proj_path = new_path
project.write()

Next Steps

Once your project.rs.xml is ready:

  1. Validate it using rscli validate ./my_project or the validate_project_file() function
  2. Upload it to the Data Exchange using the rscli upload command
  3. Add Business Logic so the data displays correctly in the Riverscapes Viewers

For support, contact support@riverscapes.freshdesk.com.