This is an automated email from the ASF dual-hosted git repository.
gstein pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/steve.git
The following commit(s) were added to refs/heads/trunk by this push:
new 418b3f3 feat: add create-election.py script and election.yaml.sample
418b3f3 is described below
commit 418b3f3a0a9ecbc6944840cb1dbc2ce7ed499c88
Author: Greg Stein <[email protected]>
AuthorDate: Tue Feb 3 03:21:51 2026 -0600
feat: add create-election.py script and election.yaml.sample
Co-authored-by: aider (openrouter/x-ai/grok-code-fast-1) <[email protected]>
---
v3/server/bin/create-election.py | 123 +++++++++++++++++++++++++++++++++++++
v3/server/bin/election.yaml.sample | 28 +++++++++
2 files changed, 151 insertions(+)
diff --git a/v3/server/bin/create-election.py b/v3/server/bin/create-election.py
new file mode 100644
index 0000000..d3235f1
--- /dev/null
+++ b/v3/server/bin/create-election.py
@@ -0,0 +1,123 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import argparse
+import datetime
+import pathlib
+import logging
+import yaml
+
+import steve.election
+import steve.persondb
+
+_LOGGER = logging.getLogger(__name__)
+
+THIS_DIR = pathlib.Path(__file__).resolve().parent
+DB_FNAME = THIS_DIR.parent / 'steve.db'
+
+# Supported vote types
+VALID_VTYPES = {'yna', 'stv'}
+
+def parse_datetime(dt_str):
+ """Parse ISO datetime string to Unix timestamp."""
+ if not dt_str:
+ return None
+ dt = datetime.datetime.fromisoformat(dt_str)
+ return int(dt.timestamp())
+
+def validate_issue(issue):
+ """Validate an issue dict from YAML."""
+ if 'vtype' not in issue or issue['vtype'] not in VALID_VTYPES:
+ raise ValueError(f"Invalid vtype: {issue.get('vtype')}")
+ if issue['vtype'] == 'stv':
+ kv = issue.get('kv', {})
+ if not all(k in kv for k in ['version', 'labelmap', 'seats']):
+ raise ValueError("STV issue missing required kv fields: version,
labelmap, seats")
+ if not isinstance(kv['seats'], int) or kv['seats'] <= 0:
+ raise ValueError("STV seats must be a positive integer")
+ return issue
+
+def main(yaml_file):
+ with open(yaml_file, 'r') as f:
+ data = yaml.safe_load(f)
+
+ # Extract election data
+ election_data = data.get('election', {})
+ title = election_data.get('title')
+ owner_pid = election_data.get('owner_pid')
+ authz = election_data.get('authz')
+ open_at = parse_datetime(election_data.get('open_at'))
+ close_at = parse_datetime(election_data.get('close_at'))
+
+ if not title or not owner_pid:
+ raise ValueError("Election must have title and owner_pid")
+
+ # Extract issues
+ issues = data.get('issues', [])
+ for issue in issues:
+ validate_issue(issue)
+
+ # Extract voters tag (placeholder: assume "members" means all persons)
+ eligible_voters = data.get('eligible_voters')
+ if eligible_voters != 'members':
+ raise ValueError("Only 'members' is supported for eligible_voters")
+
+ # Start transaction for safety
+ pdb = steve.persondb.PersonDB(DB_FNAME)
+ pdb.db.conn.execute('BEGIN TRANSACTION')
+
+ try:
+ # Create election
+ election = steve.election.Election.create(DB_FNAME, title, owner_pid,
authz, open_at, close_at)
+ _LOGGER.info(f'Created election[E:{election.eid}]: "{title}" by owner
"{owner_pid}"')
+
+ # Add issues
+ for issue_data in issues:
+ iid = election.add_issue(
+ issue_data['title'],
+ issue_data.get('description'),
+ issue_data['vtype'],
+ issue_data.get('kv') if issue_data['vtype'] == 'stv' else None
+ )
+ _LOGGER.info(f'Added issue[I:{iid}] to election[E:{election.eid}]')
+
+ # Add voters: All persons in persondb to all issues
+ all_persons = pdb.list_persons()
+ for person in all_persons:
+ election.add_voter(person.pid)
+ _LOGGER.info(f'Added {len(all_persons)} voters to
election[E:{election.eid}]')
+
+ pdb.db.conn.execute('COMMIT')
+ _LOGGER.info(f'Election[E:{election.eid}] fully created from
{yaml_file}')
+
+ except Exception as e:
+ pdb.db.conn.execute('ROLLBACK')
+ _LOGGER.error(f'Failed to create election from {yaml_file}: {e}')
+ raise
+
+if __name__ == '__main__':
+ logging.basicConfig(level=logging.INFO)
+
+ parser = argparse.ArgumentParser(
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter
+ )
+ parser.add_argument(
+ 'yaml_file',
+ help='Path to the YAML file defining the election.',
+ )
+ args = parser.parse_args()
+ main(args.yaml_file)
diff --git a/v3/server/bin/election.yaml.sample
b/v3/server/bin/election.yaml.sample
new file mode 100644
index 0000000..e5277f6
--- /dev/null
+++ b/v3/server/bin/election.yaml.sample
@@ -0,0 +1,28 @@
+# Sample YAML file for defining an election to be created via
create-election.py
+# Copy this file, edit as needed, and run: python create-election.py
<your_file.yaml>
+
+election:
+ title: "Board Election 2024"
+ owner_pid: "alice" # Apache ID of the election owner
+ authz: "pmc" # Optional authorization level
+ open_at: "2024-01-01T00:00:00" # Optional ISO datetime for opening
+ close_at: "2024-01-31T23:59:59" # Optional ISO datetime for closing
+
+issues:
+ - title: "Approve Budget"
+ description: "Vote yes/no/abstain on the proposed budget."
+ vtype: "yna" # Yes/No/Abstain vote type
+
+ - title: "Elect Board Members"
+ description: "Rank candidates for board seats using STV."
+ vtype: "stv" # Single Transferable Vote type
+ kv:
+ version: 1 # KV format version
+ labelmap:
+ a: "Alice"
+ b: "Bob"
+ c: "Carlos"
+ d: "David"
+ seats: 3 # Number of seats to elect
+
+eligible_voters: "members" # Placeholder: all persons in the database are
eligible