SWMM Feature Publisher Function
Azure Functions-based service that processes SWMM (Stormwater Management Model) INP files and publishes feature layers to ArcGIS Online.
Overview
This Azure Function App automatically publishes SWMM projects to ArcGIS Online when INP files are uploaded to blob storage. The blob trigger detects uploads and processes them automatically.
Architecture
End-to-End Flow
Automatic Publishing Flow (Blob Trigger - Primary)
- INP Upload: ASP.NET API uploads INP file to
swmm-input/{projectId}/{filename}.inp - Blob Trigger Activates: Function automatically detects upload
- Status Update: "Uploaded input file" → "Processing"
- Publish: Parse INP, generate GeoJSON, publish to ArcGIS with prefix
SD1_{projectId} - Complete: Update status to "Complete", store feature metadata, and merge map center onto the project row (see below)
Projects table: map center (WGS84)
After a successful INP feature publish, the function computes a default view center from all vertices in the published GeoJSON (mean latitude / longitude in EPSG:4326) and upserts these optional columns on the same Projects row (PartitionKey = projects, RowKey = project id):
| Column | Type | Description |
|---|---|---|
MapCenterLatitude | Edm.Double | Mean latitude of network geometry |
MapCenterLongitude | Edm.Double | Mean longitude of network geometry |
Clients (e.g. SWMM web maps) can use this pair as { lat, lng } for the default 2D map home and 3D scene framing when a project is selected. If geometry yields no coordinates, these fields are not updated on that run (any previous values remain). SLPK-only publish does not recompute center; the value from the INP publish remains.
Hybrid GeoJSON publishing
When USE_HYBRID_GEOJSON is enabled (default), Junction, Conduit, Outfall, Subcatchment, and Raingage layers are built using a hybrid INP + engine approach:
- INP parser (
publisher.pyswmm_feat.utils.inp_parser) supplies structure: nodes, coordinates, tags, inflows, treatment, conduits, outfalls. - Engine inspection (
publisher.pyswmm_feat.utils.inspect_network) supplies hydraulic attributes from pyswmm (invert elevation, depths, offsets, losses, etc.).
The resulting GeoJSON uses the same field names and schema as the demo notebooks (pyswmm/examples/junction_geojson_full_demo.ipynb, tests/junction_geojson_full_demo_feat.ipynb) so that the API and frontend see stable property names (e.g. Name, Node_Type, Link_Type, Invert_Elev). Geometry is reprojected to WGS84 for ArcGIS Online.
- Schema:
publisher/geojson_schema.pymaps conceptual keys to ArcGIS field names using an externally supplied ordered field map (loaded from AzureAttributeMap). - Builders:
publisher/geojson_builders.pyimplements the hybrid builders, including:build_subcatchment_polygon_features(...)->Subcatchment(Polygon)build_subcatchment_centroid_features(...)->Subcatchment_Centroid(Point)
- Subcatchment rule:
Subcatchmentis strict polygon geometry from[Polygons]in the INP (no fallback to centroid geometry). - Fallback: If engine initialization or inspection fails, publishing falls back to INP-only mode for that run and logs a warning.
- Toggle: Set
USE_HYBRID_GEOJSON=false(or0/no) to use the legacy INP-only GeoJSON generator for all layers.
Developer note: The schema (publisher/geojson_schema.py) and builders (publisher/geojson_builders.py) are the canonical implementation shared with the pyswmm demo notebook. When adding or changing published fields for Junction/Conduit/Outfall, update the conceptual dicts in the builders and ensure the corresponding ordered field maps in Azure AttributeMap cover the new conceptual keys.
Components
- Blob Trigger: Monitors
swmm-inputcontainer for INP uploads - Blob Storage: Stores INP input files in
{projectId}/{filename}.inpformat - Table Storage:
Projectstable: Project processing status, EPSG, and optionalMapCenterLatitude/MapCenterLongitudeafter successful feature publishProjectFeatureMetaDatatable: Published layer metadata
- ArcGIS Online: Target platform for feature layers
- HTTP Health Check:
/api/healthendpoint for monitoring
Required Azure Resources
Storage Account
- Container:
swmm-input(or configured name) - Tables:
Projects,ProjectFeatureMetaData
Application Insights
- For monitoring, logging, and telemetry
Azure Function App
- Python 3.11+ runtime
- Linux hosting (recommended)
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
AzureWebJobsStorage | Yes | - | Connection string for Functions runtime |
0b61f1_STORAGE | Yes | - | Connection string for blob trigger |
AZURE_STORAGE_ACCOUNT_NAME | Yes* | - | Storage account name (if using SAS) |
AZURE_STORAGE_SAS_TOKEN | Yes* | - | SAS token (if not using connection string) |
AZURE_STORAGE_CONNECTION_STRING | Yes* | - | Full connection string (alternative to SAS) |
SWMM_INP_CONTAINER | Yes | - | Blob container name for INP files (e.g., swmm-input) |
PROJECT_TABLE_NAME | Yes | - | Table name for project status |
FEATURES_TABLE_NAME | No | ProjectFeatureMetaData | Table name for feature metadata |
ENABLE_INDEX_BUILDING | No | true | Whether to build NodeIdIndex and LinkIdIndex tables during publish |
ARCGIS_URL | No | https://www.arcgis.com | ArcGIS Online URL |
ARCGIS_API_KEY | Yes | - | ArcGIS API key for authentication |
MAX_ATTEMPTS | No | 3 | Maximum retry attempts (1-10) |
USE_HYBRID_GEOJSON | No | true | Use hybrid INP + engine builders for Junction/Conduit/Outfall/Subcatchment/Raingage layers (see Hybrid GeoJSON publishing) |
Notes:
- *Either connection string OR (account name + SAS token) is required
0b61f1_STORAGEis used for blob trigger bindings- For local development, use full connection string with
AccountKey
Blob Trigger Workflow (Automatic Publishing)
The blob trigger automatically publishes projects when INP files are uploaded:
Trigger Pattern
- Path:
swmm-input/{projectId}/{filename}.inp - Any
.inpfile in a project folder triggers publishing - Example:
swmm-input/rivershore/rivershore.inp→ Auto-publishes
Status Lifecycle
- "Uploaded input file" - Blob detected, acknowledged
- "Processing" - Publishing in progress
- "Complete" or "Failed" - Final state
Automatic Behavior
- projectId extracted from folder name
- titlePrefix automatically set to
SD1_{projectId} - Idempotency: Re-uploading won't reprocess if already "Complete"
- Error handling: Automatic retries up to
MAX_ATTEMPTS
Testing
Quick test:
# Upload test INP file to trigger automatic publishing
python scripts/test_blob_upload.py --project-id testproject --inp-file scripts/rivershore.inp
Expected behavior:
- Blob trigger activates automatically
- Status transitions: "Uploaded input file" → "Processing" → "Complete"
- Layers published to ArcGIS with prefix
SD1_testproject
Verification:
- Check function logs for processing messages
- Query Projects table to verify status
- Check ArcGIS Online for published layers
- Verify ProjectFeatureMetaData table has entries
Testing idempotency: Re-uploading the same file should skip processing if status is already "Complete".
Testing error handling:
- Upload empty INP file → Should fail with error message
- Upload invalid projectId → Should fail validation
- Test max attempts by setting
MAX_ATTEMPTS=1and uploading invalid file
Project Status Tracking
The function maintains comprehensive status in the Projects table:
| Field | Type | Description |
|---|---|---|
PartitionKey | string | Always "projects" |
RowKey | string | Project ID |
ProjectStatus | string | "Uploaded input file", "Processing", "Complete", "Failed" |
AttemptCount | int | Number of processing attempts |
LastAttemptAt | datetime | Timestamp of last attempt |
ProcessingStartedAt | datetime | First processing start time |
ProcessingCompletedAt | datetime | Completion timestamp |
LastErrorMessage | string | Error details (if failed, max 2000 chars) |
NeedsIntervention | bool | True if manual intervention required |
Local Development
Prerequisites
- Python 3.11+
- Azure Functions Core Tools v4
- Azure Storage Account (or Azurite)
Setup
Clone repository
cd c:\Git\gqc\hydrotrek-ssc-suite\swmm-feature-publisher-functionCreate virtual environment
python -m venv .venv
.venv\Scripts\activate # Windows
source .venv/bin/activate # Linux/MacInstall dependencies
pip install -r requirements.txtConfigure local settings
Copy
local.settings.json.exampletolocal.settings.jsonand fill in values:{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net",
"FUNCTIONS_WORKER_RUNTIME": "python",
"0b61f1_STORAGE": "DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net",
"AZURE_STORAGE_ACCOUNT_NAME": "your-account",
"AZURE_STORAGE_SAS_TOKEN": "?sv=2025-07-05&...",
"SWMM_INP_CONTAINER": "swmm-input",
"PROJECT_TABLE_NAME": "Projects",
"FEATURES_TABLE_NAME": "ProjectFeatureMetaData",
"ARCGIS_URL": "https://www.arcgis.com",
"ARCGIS_API_KEY": "your-api-key",
"MAX_ATTEMPTS": "3"
}
}Run locally
func start
Deployment
For detailed deployment instructions, see DEPLOYMENT.md (same folder as this file).
The function can be deployed to Azure using:
- Azure Portal (recommended for first-time setup)
- Azure CLI (automated script)
- Managed Identity (production best practice)
- CI/CD (GitHub Actions)
Quick deployment:
func azure functionapp publish <your-function-app-name>
Monitoring
Application Insights
Application Insights is automatically configured via the APPLICATIONINSIGHTS_CONNECTION_STRING environment variable.
View in Azure Portal:
- Navigate to your Function App
- Click "Application Insights" in left menu
- Or go directly to Application Insights resource
Health Check
curl https://<function-app-name>.azurewebsites.net/api/health
Response:
{
"status": "healthy",
"service": "swmm-feature-publisher"
}
Key Metrics
Success Rate
requests
| where cloud_RoleName == "swmm-feature-publisher"
| where name == "swmmBlobTriggerPublisher"
| summarize
Total = count(),
Success = countif(success == true),
Failed = countif(success == false),
SuccessRate = 100.0 * countif(success == true) / count()
| project Total, Success, Failed, SuccessRate
Execution Duration
requests
| where cloud_RoleName == "swmm-feature-publisher"
| where name == "swmmBlobTriggerPublisher"
| summarize
avg(duration),
percentiles(duration, 50, 90, 95, 99),
max(duration)
by bin(timestamp, 1h)
| render timechart
Publishing Success Rate
traces
| where message contains "Published" and message contains "layer(s) to ArcGIS"
| extend
project_id = extract("project_id=([^\\s]+)", 1, message),
layer_count = toint(extract("Published ([0-9]+) layer", 1, message))
| summarize
count(),
avg(layer_count),
sum(layer_count)
by bin(timestamp, 1h)
| render timechart
Log Queries
Recent Function Executions
traces
| where cloud_RoleName == "swmm-feature-publisher"
| where message contains "Function execution started"
or message contains "Successfully completed processing"
| project timestamp, message, severityLevel
| order by timestamp desc
| take 100
Failed Executions
traces
| where cloud_RoleName == "swmm-feature-publisher"
| where severityLevel >= 3 // Warning and above
| where message contains "project_id"
| extend project_id = extract("project_id=([^,\\s]+)", 1, message)
| project timestamp, project_id, message, severityLevel
| order by timestamp desc
Projects Needing Intervention
traces
| where message contains "Max attempts reached"
or message contains "needs_intervention=True"
| extend project_id = extract("project_id=([^,\\s]+)", 1, message)
| summarize count() by project_id, bin(timestamp, 1d)
| order by timestamp desc
Alerts
Critical Alerts
1. Function Failures Alert
requests
| where name == "swmmBlobTriggerPublisher"
| where success == false
| summarize count() by bin(timestamp, 5m)
| where count_ > 5
2. Projects Needing Intervention
traces
| where message contains "Max attempts reached"
| summarize count() by bin(timestamp, 1h)
| where count_ > 0
3. High Processing Duration
requests
| where name == "swmmBlobTriggerPublisher"
| where duration > 300000 // 5 minutes
| summarize count() by bin(timestamp, 15m)
Dashboards
Create custom dashboard in Azure Portal with these tiles:
Success Rate (Last 24h):
requests
| where timestamp > ago(24h)
| where name == "swmmBlobTriggerPublisher"
| summarize
SuccessRate = 100.0 * countif(success == true) / count()
| project SuccessRate
Executions Over Time:
requests
| where timestamp > ago(24h)
| summarize
Successful = countif(success == true),
Failed = countif(success == false)
by bin(timestamp, 1h)
| render timechart
Average Processing Time:
requests
| where timestamp > ago(24h)
| where name == "swmmBlobTriggerPublisher"
| summarize avg(duration) by bin(timestamp, 1h)
| render timechart
Availability Test
Create availability test in Application Insights:
- URL:
https://<function-app>.azurewebsites.net/api/health - Frequency: Every 5 minutes
- Locations: Multiple regions
- Alert if 2+ locations fail
Error Handling & Recovery
Retry Logic
The function automatically retries failed executions based on the retry policy configured in host.json:
- Exponential backoff strategy
- Maximum retry count: 2 (configurable)
- Retry intervals: 2-15 seconds
Manual Intervention
Projects with NeedsIntervention = true require manual review:
- Check
LastErrorMessagein Projects table - Verify INP file exists and is valid
- Check ArcGIS permissions
- Reset status if needed:
python scripts/reset_project_status.py --project-id <id>
Performance Tuning
Concurrency
Blob triggers process files automatically as they're uploaded. The function runtime handles concurrency automatically based on available resources.
Timeouts
- Blob download: 300s (5 min)
- Function timeout: 600s (10 min, configurable in Azure Portal)
Security
- Secrets: Never commit
local.settings.json - Production: Use Managed Identity instead of connection strings
- SAS Tokens: Use least-privilege permissions, short expiration
- Input Validation: All blob paths and project IDs validated for security
Troubleshooting
Common Issues
Blob trigger doesn't activate
Check:
- Function is running (
func start) 0b61f1_STORAGEconnection string is correct inlocal.settings.json- Blob was uploaded to correct container (
swmm-input) - File has
.inpextension
Solution: Check function logs for any startup errors
Function activates but fails immediately
Check:
- All environment variables are set in
local.settings.json - ArcGIS API key is valid
- Storage credentials have correct permissions
Solution: Review function logs for specific error messages
Status stuck at "Processing"
Check:
- Function didn't crash (check logs for exceptions)
- ArcGIS publishing didn't timeout or fail
- Network connectivity to ArcGIS Online
Solution: Check LastErrorMessage field in Projects table
Multiple triggers for same upload
Possible causes:
- Blob being modified/overwritten multiple times
- Function runtime creating multiple instances
Solution: Check idempotency logic is working (should skip if "Complete")
Debugging Steps
- Check Function Logs: Review Application Insights or function logs for errors
- Verify Storage: Ensure blob exists and is accessible
- Check Table Status: Query Projects table for current status
- Verify ArcGIS: Check ArcGIS Online for published layers
- Review Metadata: Check ProjectFeatureMetaData table for stored URLs