Skip to content

Commit bc90ac2

Browse files
committed
migrate Jython-based Java application to Python-based impl with JPype
- build orchestration with gradle
1 parent 80fec2c commit bc90ac2

15 files changed

Lines changed: 348 additions & 836 deletions

File tree

README.md

Lines changed: 63 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,100 @@
11
# REGI-Headless
22

33
> [!IMPORTANT]
4-
> **Notice: Project Refactor in Progress**
5-
> This project is undergoing a large refactor for CWMS Data API support.
6-
> It will transition from a Java project consuming Jython scripts to a **Python project** that
7-
> utilizes **JPype** to call underlying REGI Java libraries.
4+
> **Project Refactor in Progress**
5+
> This project has transitioned from a Java project consuming Jython scripts to a **Python bridge** that utilizes **JPype** to call underlying REGI Java libraries.
86
9-
`REGI-Headless` is a Java-based command-line tool and library designed to run
10-
**REGI** calculations in a headless environment.
11-
It allows users to execute complex hydrological calculations and manage gate settings via Jython
12-
scripts without the need for a graphical interface.
7+
`REGI-Headless` provides a Python-based interface for running **REGI** calculations in a headless environment. It allows users to execute complex hydrological calculations and manage gate settings via Python scripts, leveraging the performance and stability of the original REGI Java libraries.
138

149
## Features
1510

16-
- **Headless Execution**: Run REGI calculations as part of automated workflows or on servers.
17-
- **Database Integration**: Connects to CWMS data retrieval and storage.
18-
- **Modular Calculations**: Includes support for:
11+
- **Headless Execution**: Run REGI calculations as part of automated workflows, CI/CD pipelines, or on servers.
12+
- **Python-First API**: Write calculation scripts in standard Python 3.
13+
- **Java Interoperability**: Direct access to REGI Java libraries via JPype.
14+
- **Modular Calculations**: Full support for:
1915
- Inflow calculations (Clone, Compute, Auto-Adjust, Balance All, etc.)
20-
- Flow Group and gate settings calculations.
16+
- Flow Group
17+
- Gate settings calculations.
2118

2219
## Project Structure
2320

24-
- `regi-headless/`: Core Java implementation, including `RegiCLI`.
25-
- `district-scripts/`: Example scripts and district-specific configurations.
21+
- `regi-headless/`: Core implementation.
22+
- `src/main/python/`: The `regi-python` package source.
23+
- `src/main/java/`: Java-based headless support and factories.
24+
- `district-scripts/`: Legacy Jython scripts and district-specific configurations.
2625
- `docs/`: Additional documentation.
2726

2827
## Getting Started
2928

3029
### Prerequisites
3130

32-
- Java JDK 21 or higher.
33-
- Access to a CWMS
31+
- **Java JDK 21** or higher.
32+
- **Python 3.11** or higher.
33+
- Access to a **CWMS Data API (CDA)** instance.
3434

3535
### Building
3636

37+
The project uses Gradle to manage both Java and Python builds. To build the Python wheel including all Java dependencies:
3738

3839
```powershell
39-
./gradlew build
40+
./gradlew buildPythonWheel
4041
```
4142

42-
Details TBD.
43+
The resulting wheel file will be located in `regi-headless/build/install/regi-python/dist/`.
4344

4445
## Usage
4546

46-
TBD
47+
### Installation
4748

48-
### Command Line Options
49+
Install the built wheel into your Python environment:
4950

50-
TBD
51+
```powershell
52+
pip install regi_python-*.whl
53+
```
54+
55+
### Script Example
56+
57+
The Python bridge uses a context manager to handle the JVM lifecycle and a callback mechanism for calculations.
58+
59+
```python
60+
from regi_python import regi_session, run_headless
61+
from java.util import Calendar, TimeZone
62+
63+
def my_calculations(registry):
64+
# 'registry' is a RegiCalcRegistry instance
65+
gate_calc = registry.getCalculation(1.0, "Gate Flow")
66+
67+
# Configure time window
68+
tz = TimeZone.getTimeZone("US/Central")
69+
start = Calendar.getInstance(tz)
70+
start.set(2025, 0, 1) # Jan 1, 2025
71+
72+
end = Calendar.getInstance(tz)
73+
end.set(2025, 0, 2) # Jan 2, 2025
74+
75+
# Execute calculation
76+
gate_calc.computeAll("OFFICE", "PROJECT", start.getTimeInMillis(), end.getTimeInMillis())
77+
78+
if __name__ == "__main__":
79+
with regi_session():
80+
run_headless(my_calculations)
81+
```
5182

52-
### Example
83+
### Environment Variables
5384

54-
TBD
85+
- `JAVA_HOME`: Path to your Java installation (required for JPype).
86+
- `REGI_LOG_LEVEL`: Logging level (e.g., `DEBUG`, `INFO`, `ERROR`).
87+
- `CDA_URL`: URL for the CWMS Data API.
88+
- `CDA_API_KEY`: API Key for CDA authentication.
89+
- `OFFICE_ID`: Office ID for CDA authentication.
5590

5691
## Testing
5792

58-
TBD
93+
To run the automated tests which build the wheel and execute a test script in a virtual environment:
94+
95+
```powershell
96+
./gradlew testPythonWheel
97+
```
5998

6099
## Maintainers
61100

build.gradle

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,52 @@ plugins {
33
id "org.sonarqube" version "7.3.1.8318"
44
}
55

6-
def versionLabel(gitInfo) {
7-
def branch = gitInfo.branchName // all branches are snapshots, only tags get released
8-
def tag = gitInfo.lastTag
9-
// tag is returned as is. Branch may need cleanup
10-
return branch == null ? tag : "99." + branch.replace("/","-") + "-SNAPSHOT"
6+
def gitOutput(String... args) {
7+
return providers.exec {
8+
commandLine(['git'] + args.toList())
9+
ignoreExitValue = true
10+
}.standardOutput.asText.get().trim()
11+
}
12+
13+
def pep440VersionFromLatestTag() {
14+
def tag = gitOutput('describe', '--tags', '--abbrev=0')
15+
if (!tag) {
16+
tag = '0.0.0'
17+
}
18+
19+
def baseVersion = tag.startsWith('v') ? tag.substring(1) : tag
20+
21+
def distanceText = tag == '0.0.0'
22+
? gitOutput('rev-list', 'HEAD', '--count')
23+
: gitOutput('rev-list', "${tag}..HEAD", '--count')
24+
def distance = distanceText?.isInteger() ? distanceText.toInteger() : 0
25+
26+
def hash = gitOutput('rev-parse', '--short=7', 'HEAD')
27+
def dirty = gitOutput('status', '--porcelain') ? true : false
28+
29+
if (distance == 0 && !dirty) {
30+
return baseVersion
31+
}
32+
33+
def version = "${baseVersion}.post${distance}"
34+
def localParts = []
35+
36+
if (hash) {
37+
localParts.add("g${hash}")
38+
}
39+
40+
if (dirty) {
41+
localParts.add("dirty")
42+
}
43+
44+
if (!localParts.isEmpty()) {
45+
version += "+" + localParts.join(".")
46+
}
47+
48+
return version
1149
}
1250

1351
allprojects {
14-
group = 'mil.army.wmist.regi-headless'
15-
version = versionLabel(versionDetails())
52+
group = 'mil.army.wmist.regi-python'
53+
version = pep440VersionFromLatestTag()
1654
}

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ regi-tools-regi-cwms = {module = "mil.army.wmist.regi-tools:regi-cwms", version.
3636
# Third Party
3737
jython-standalone = {module = "org.python:jython-standalone", version.ref = "jython-standalone"}
3838
args4j = {module = "args4j:args4j", version.ref = "args4j"}
39+
otel = {module = "io.opentelemetry:opentelemetry-api", version="1.58.0"}
3940

4041
# Test
4142
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }

regi-headless/build.gradle

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import com.pswidersk.gradle.python.VenvTask
2+
13
plugins {
24
id 'regi-headless.deps-conventions'
35
id 'regi-headless.java-conventions'
6+
id 'com.pswidersk.python-plugin' version '3.2.16'
47
}
58

69
dependencies {
@@ -45,6 +48,85 @@ jar {
4548
}
4649
}
4750

48-
test {
49-
useJUnitPlatform()
51+
pythonPlugin {
52+
pythonVersion = "3.11.15"
53+
condaVersion = "26.3.2-2"
54+
condaInstaller = "Miniforge3"
55+
installDir = file(layout.buildDirectory.dir("python"))
56+
}
57+
58+
tasks.register('installPythonBuildTools', VenvTask) {
59+
group = 'build setup'
60+
description = 'Installs Python packages needed to build the wheel.'
61+
62+
venvExec = 'pip'
63+
args = ['install', '--upgrade', 'pip', 'build']
64+
}
65+
66+
tasks.register('buildPythonWheel', VenvTask) {
67+
group = "distribution"
68+
description = "Builds a Python .whl file using the Gradle-managed Python environment."
69+
70+
dependsOn bundlePython
71+
dependsOn installPythonBuildTools
72+
73+
workingDir = file("${buildDir}/install/regi-python")
74+
venvExec = "python"
75+
args = ["-m", "build", "--wheel"]
76+
77+
doLast {
78+
println "Python Wheel built in: ${workingDir}/dist"
79+
}
80+
}
81+
82+
tasks.register('bundlePython', Sync) {
83+
description = 'Bundles Python scripts and creates the java_lib directory'
84+
into "${buildDir}/install/regi-python/"
85+
86+
from('src/main/python') {
87+
include 'pyproject.toml'
88+
filter { line -> line.replaceAll('@VERSION@', project.version.toString()) }
89+
}
90+
91+
from('src/main/python') {
92+
exclude 'pyproject.toml'
93+
}
94+
95+
into('regi-python/lib') {
96+
from configurations.runtimeClasspath
97+
from jar.archiveFile
98+
exclude "**/*.nbm"
99+
}
100+
}
101+
102+
tasks.register('testPythonWheel') {
103+
group = 'verification'
104+
description = 'Creates a venv, installs the wheel, and runs a test script.'
105+
dependsOn buildPythonWheel
106+
107+
doLast {
108+
def venvName = "test_venv"
109+
def wheelDir = "${projectDir}/build/install/regi-headless/dist"
110+
def testScript = "src\\test\\resources\\usace\\rowcps\\headless\\examples\\GateFlowCalc2_Jpype.py"
111+
112+
// 1. Create VENV if it doesn't exist
113+
exec {
114+
commandLine 'cmd', '/c', "python -m venv ${venvName}"
115+
}
116+
117+
// 2. Install Wheel and Run Script
118+
// We chain these using '&&' so they run in the same shell session where the venv is active
119+
exec {
120+
workingDir projectDir
121+
def wheelFile = fileTree(dir: wheelDir, include: '*.whl').singleFile.absolutePath
122+
def venvActivate = file("${venvName}/Scripts/activate.bat").absolutePath
123+
environment 'CDA_URL', project.findProperty('CDA_URL') ?: ""
124+
environment 'API_KEY', project.findProperty('API_KEY') ?: ""
125+
environment 'OFFICE_ID', project.findProperty('OFFICE_ID') ?: ""
126+
environment 'JAVA_HOME', System.getProperty('java.home')
127+
commandLine 'cmd', '/c', "\"${venvActivate}\" && pip install --force-reinstall \"${wheelFile}\" && python -u ${testScript}"
128+
standardOutput = System.out
129+
errorOutput = System.err
130+
}
131+
}
50132
}

0 commit comments

Comments
 (0)