diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 0eb1f3f..b4f242e 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -28,10 +28,13 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v6 - name: Install dependencies - working-directory: v3 # Set the working directory to v3 where pyproject.toml is located + working-directory: v3 run: | sudo apt-get install libldap2-dev libsasl2-dev uv sync --all-extras --dev - - name: Testing with ruff - working-directory: v3 # Ensure ruff runs in the correct directory + - name: Check with ruff + working-directory: v3 run: uv run ruff check + - name: Format with ruff + working-directory: v3 + run: uv run ruff format --diff \ No newline at end of file diff --git a/v3/server/api.py b/v3/server/api.py index 174b581..8b5cba0 100644 --- a/v3/server/api.py +++ b/v3/server/api.py @@ -1,20 +1,20 @@ +# 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 # -# 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. +# 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 asfquart -APP = asfquart.APP +APP = asfquart.APP diff --git a/v3/server/bin/asf-load-ldap.py b/v3/server/bin/asf-load-ldap.py index 161e02a..d5faefb 100755 --- a/v3/server/bin/asf-load-ldap.py +++ b/v3/server/bin/asf-load-ldap.py @@ -1,19 +1,21 @@ #!/usr/bin/env python3 -# 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 +# 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 +# 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. +# 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 pathlib import logging @@ -43,13 +45,12 @@ def main(): pdb.db.conn.execute('BEGIN TRANSACTION') client = ldap.initialize(LDAP_URL) - binddn, bindpw = [ s.strip() - for s in open(THIS_DIR / 'bind.txt').readlines()[:2] ] - #print('BIND:', binddn, bindpw) + binddn, bindpw = [s.strip() for s in open(THIS_DIR / 'bind.txt').readlines()[:2]] + # print('BIND:', binddn, bindpw) client.simple_bind_s(binddn, bindpw) with asfpy.stopwatch.Stopwatch('run LDAP full scan'): - results = client.search_s(LDAP_DN, ldap.SCOPE_SUBTREE, 'uid=*', attrlist=None) #[LDAP_ATTR,]) + results = client.search_s(LDAP_DN, ldap.SCOPE_SUBTREE, 'uid=*', attrlist=None) count = 0 for r in results: diff --git a/v3/server/bin/fetch-bootstrap.sh b/v3/server/bin/fetch-bootstrap.sh index 0ee49ab..26f9c34 100755 --- a/v3/server/bin/fetch-bootstrap.sh +++ b/v3/server/bin/fetch-bootstrap.sh @@ -1,19 +1,21 @@ #!/usr/bin/env bash -# 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 +# 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 +# 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. +# 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. # Fetch latest bootstrap, and places files into our git working copy. diff --git a/v3/server/bin/load-fakedata.py b/v3/server/bin/load-fakedata.py index 79ab6fb..82ed768 100755 --- a/v3/server/bin/load-fakedata.py +++ b/v3/server/bin/load-fakedata.py @@ -1,19 +1,21 @@ #!/usr/bin/env python3 -# 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 +# 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 +# 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. +# 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. # Load a bunch of fake data into the database, for stuff to work with. @@ -25,6 +27,7 @@ import faker import steve.election + ### we shouldn't need this. do so, for now. import steve.crypto @@ -49,8 +52,7 @@ def main(args): def gen_election(owner_pid, issue_count=10): title = FAKE.sentence() e = steve.election.Election.create(DB_FNAME, title, owner_pid) - _LOGGER.info(f'Created election[E:{e.eid}]: "{title}",' - f' by owner "{owner_pid}"') + _LOGGER.info(f'Created election[E:{e.eid}]: "{title}", by owner "{owner_pid}"') for _ in range(issue_count): title = FAKE.sentence() @@ -78,13 +80,28 @@ def random_owner(): if __name__ == '__main__': logging.basicConfig(level=logging.INFO) - parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument('--elections', type=int, required=False, default=10, - help="The number of elections to create.") - parser.add_argument('--issues', type=int, required=False, default=10, - help="The number of issues per election to create.") - parser.add_argument('--owner-pid', type=str, required=False, - help="The owner's Apache ID to use for created elections." - " If not set, pick a random existing person.") - + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument( + '--elections', + type=int, + required=False, + default=10, + help='The number of elections to create.', + ) + parser.add_argument( + '--issues', + type=int, + required=False, + default=10, + help='The number of issues per election to create.', + ) + parser.add_argument( + '--owner-pid', + type=str, + required=False, + help="The owner's Apache ID to use for created elections." + ' If not set, pick a random existing person.', + ) main(parser.parse_args()) diff --git a/v3/server/config.yaml.example b/v3/server/config.yaml.example index 5d3c952..5fd8777 100644 --- a/v3/server/config.yaml.example +++ b/v3/server/config.yaml.example @@ -1,19 +1,19 @@ +# 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 # -# 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. +# 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. server: # Default port for the server. Typical usage is that a proxy sits diff --git a/v3/server/main.py b/v3/server/main.py index 5557c2e..4c5786f 100755 --- a/v3/server/main.py +++ b/v3/server/main.py @@ -1,20 +1,21 @@ #!/usr/bin/env python3 + +# 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 # -# 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. +# 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 sys import logging @@ -28,11 +29,12 @@ def main(): - logging.basicConfig(level=logging.DEBUG, - style='{', - format='[{asctime}|{levelname}|{name}] {message}', - datefmt=DATE_FORMAT, - ) + logging.basicConfig( + level=logging.DEBUG, + style='{', + format='[{asctime}|{levelname}|{name}] {message}', + datefmt=DATE_FORMAT, + ) # Switch some loggers to INFO, rather than DEBUG logging.getLogger('selector_events').setLevel(logging.INFO) @@ -45,8 +47,11 @@ def main(): ### is this really needed right now? # Avoid OIDC import asfquart.generics - asfquart.generics.OAUTH_URL_INIT = "https://oauth.apache.org/auth?state=%s&redirect_uri=%s" - asfquart.generics.OAUTH_URL_CALLBACK = "https://oauth.apache.org/token?code=%s" + + asfquart.generics.OAUTH_URL_INIT = ( + 'https://oauth.apache.org/auth?state=%s&redirect_uri=%s' + ) + asfquart.generics.OAUTH_URL_CALLBACK = 'https://oauth.apache.org/token?code=%s' app = asfquart.construct('steve', app_dir=THIS_DIR, static_folder=None) @@ -62,21 +67,19 @@ def main(): # There are other things to watch, and cause a reload. extra_files = { steve.election.QUERIES, - } + } - kwargs = { } + kwargs = {} if app.cfg.server.certfile: kwargs['certfile'] = CERTS_DIR / app.cfg.server.certfile kwargs['keyfile'] = CERTS_DIR / app.cfg.server.keyfile extra_files.update((kwargs['certfile'], kwargs['keyfile'])) # Spool up the app! - app.runx(port=app.cfg.server.port, - extra_files=extra_files, - **kwargs) + app.runx(port=app.cfg.server.port, extra_files=extra_files, **kwargs) - #print('LOGGERS:', sorted(_LOGGER.manager.loggerDict.keys())) - #print(_LOGGER.manager.loggerDict['sslproto']) + # print('LOGGERS:', sorted(_LOGGER.manager.loggerDict.keys())) + # print(_LOGGER.manager.loggerDict['sslproto']) if __name__ == '__main__': diff --git a/v3/server/pages.py b/v3/server/pages.py index 7769f63..f4efc20 100644 --- a/v3/server/pages.py +++ b/v3/server/pages.py @@ -1,26 +1,24 @@ +# 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 # -# 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. +# 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. -### ### NOTE: the voting handlers require login for ASF committers only. ### Obviously, this is not a general purpose solution. Something for ### the future, to figure out how we'd like to do configuration ### authorization for various install scenarios and authn systems. -### import pathlib import datetime @@ -66,15 +64,25 @@ async def basic_info(): # EasyDict objects for use by templates. # NOTE: .flash() is called with (message, category), but the # .get_flashed_messages() returns tuples of (category, message) - basic.flashes = [ edict(message=f[1], category=f[0]) - for f in quart.get_flashed_messages(with_categories=True) ] + basic.flashes = [ + edict(message=f[1], category=f[0]) + for f in quart.get_flashed_messages(with_categories=True) + ] s = await asfquart.session.read() if s: - basic.update(uid=s['uid'], name=s['fullname'], email=s['email'],) + basic.update( + uid=s['uid'], + name=s['fullname'], + email=s['email'], + ) else: # No session. - basic.update(uid=None, name=None, email=None,) + basic.update( + uid=None, + name=None, + email=None, + ) ### generate a real token and store in the session basic.csrf_token = 'placeholder' @@ -118,7 +126,7 @@ async def voter_page(): election = steve.election.Election.open_to_pid(DB_FNAME, result.uid) owned = steve.election.Election.owned_elections(DB_FNAME, result.uid) - result.election = [ postprocess_election(e) for e in election ] + result.election = [postprocess_election(e) for e in election] result.len_election = len(election) result.len_owned = len(owned) @@ -131,7 +139,6 @@ def load_election(func): @functools.wraps(func) async def loader(eid): - try: e = steve.election.Election(DB_FNAME, eid) except steve.election.ElectionNotFound: @@ -156,7 +163,6 @@ def load_election_issue(func): @functools.wraps(func) async def loader(eid, iid): - try: e = steve.election.Election(DB_FNAME, eid) except steve.election.ElectionNotFound: @@ -220,7 +226,7 @@ async def admin_page(): election = steve.election.Election.open_to_pid(DB_FNAME, result.uid) owned = steve.election.Election.owned_elections(DB_FNAME, result.uid) - result.owned = [ postprocess_election(e) for e in owned ] + result.owned = [postprocess_election(e) for e in owned] ### owned.owner_name should be based on OWNER_PID. That might not be ### "me" because of authz access to manage issues. @@ -332,8 +338,9 @@ async def do_add_issue_endpoint(election): ### does add_issue() return an edict for the added issue? issue = edict(iid=steve.crypto.create_id(), title=form.title) - _LOGGER.info(f'User[U:{result.uid}] added issue[I:{issue.iid}]' - f' to election[E:{election.eid}]') + _LOGGER.info( + f'User[U:{result.uid}] added issue[I:{issue.iid}] to election[E:{election.eid}]' + ) await flash_success(f'Issue "{issue.title}" has been added.') @@ -354,11 +361,12 @@ async def do_edit_issue_endpoint(election, issue): # Update the title/description. ### for now, no way to update the vtype or KV pairs. - election.add_issue(issue.iid, form.title, form.description, - issue.vtype, issue.kv) + election.add_issue(issue.iid, form.title, form.description, issue.vtype, issue.kv) - _LOGGER.info(f'User[U:{result.uid}] edited issue[I:{issue.iid}]' - f' in election[E:{election.eid}]') + _LOGGER.info( + f'User[U:{result.uid}] edited issue[I:{issue.iid}]' + f' in election[E:{election.eid}]' + ) # Use the new TITLE for this. await flash_success(f'Issue "{form.title}" has been updated.') @@ -378,8 +386,10 @@ async def do_delete_issue_endpoint(election, issue): # Issue exists, and was loaded. No errors to handle? election.delete_issue(issue.iid) - _LOGGER.info(f'User[U:{result.uid}] deleted issue[I:{issue.iid}]' - f' from election[E:{election.eid}]') + _LOGGER.info( + f'User[U:{result.uid}] deleted issue[I:{issue.iid}]' + f' from election[E:{election.eid}]' + ) await flash_success(f'Issue "{issue.title}" has been deleted.') @@ -429,6 +439,8 @@ async def about_page(): @APP.get('/static/') async def serve_static(filename): return await quart.send_from_directory(STATICDIR, filename) + + @APP.get('/favicon.ico') async def serve_favicon(): return await quart.send_from_directory(STATICDIR, 'favicon.ico') diff --git a/v3/steve/__init__.py b/v3/steve/__init__.py index 7064491..13a8339 100644 --- a/v3/steve/__init__.py +++ b/v3/steve/__init__.py @@ -1 +1,16 @@ -# TBD +# 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. diff --git a/v3/steve/crypto.py b/v3/steve/crypto.py index 682c783..da6ce42 100644 --- a/v3/steve/crypto.py +++ b/v3/steve/crypto.py @@ -1,22 +1,19 @@ +# 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 # -# 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. -# -# ### TBD docco -# +# 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 base64 import secrets @@ -56,7 +53,7 @@ def _b64_vote_key(vote_token: bytes, salt: bytes) -> bytes: algorithm=hashes.SHA256(), length=32, # 32-byte key for XChaCha20-Poly1305 salt=salt, - info=b"xchacha20_key" + info=b'xchacha20_key', ) vote_key = keymaker.derive(vote_token) return base64.urlsafe_b64encode(vote_key) @@ -70,9 +67,7 @@ def create_vote(vote_token: bytes, salt: bytes, votestring: str) -> bytes: return f.encrypt(votestring.encode()) -def decrypt_votestring(vote_token: bytes, - salt: bytes, - ciphertext: bytes) -> str: +def decrypt_votestring(vote_token: bytes, salt: bytes, ciphertext: bytes) -> str: "Decrypt CIPHERTEXT into a VOTESTRING." b64key = _b64_vote_key(vote_token, salt) @@ -94,7 +89,7 @@ def shuffle(x): # cryptographically-safe (aka unpredictable) shuffling of elements. # Count backwards, "fixing" a chosen element into place. - for i in range(len(x)-1, 0, -1): + for i in range(len(x) - 1, 0, -1): # Choose element to fix from remaining pool. j = secrets.randbelow(i + 1) diff --git a/v3/steve/election.py b/v3/steve/election.py index 55a0688..5e446e6 100644 --- a/v3/steve/election.py +++ b/v3/steve/election.py @@ -1,24 +1,19 @@ +# 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 # -# 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. -# -# ---- -# -# ### TBD: DOCCO -# +# 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 logging import json @@ -38,7 +33,6 @@ class Election: - # Current state of an election. S_EDITABLE = 'editable' S_OPEN = 'open' @@ -46,8 +40,7 @@ class Election: @staticmethod def open_database(db_fname): - return asfpy.db.DB(db_fname, - yaml_fname=QUERIES, yaml_section='election') + return asfpy.db.DB(db_fname, yaml_fname=QUERIES, yaml_section='election') def __init__(self, db_fname, eid): _LOGGER.debug(f'Opening election ID "{eid}"') @@ -89,7 +82,6 @@ def delete(self): self.db = None def open(self, pdb): - # Double-check the Election is in the editing state. assert self.is_editable() @@ -120,8 +112,10 @@ def gather_election_data(self, pdb): self.q_issues.perform(self.eid) # Use an f-string to render "None" if a column is NULL. - idata = ''.join(f'{i.iid}{i.title}{i.description}{i.type}{i.kv}' - for i in self.q_issues.fetchall()) + idata = ''.join( + f'{i.iid}{i.title}{i.description}{i.type}{i.kv}' + for i in self.q_issues.fetchall() + ) # Include the PID and EMAIL for each Person. ### we don't want all people. Just those who are allowed to @@ -154,7 +148,7 @@ def add_salts(self): self.q_all_issues.perform(self.eid) for mayvote in self.q_all_issues.fetchall(): # MAYVOTE is a 1-tuple: _ROWID_ - #print('COLUMNS:', dir(mayvote)) + # print('COLUMNS:', dir(mayvote)) # Use a distinct cursor to insert the SALT value. salt = crypto.gen_salt() @@ -204,8 +198,7 @@ def get_issue(self, iid): raise IssueNotFound(iid) # NEVER return issue.salt - return (issue.title, issue.description, issue.type, - self.json2kv(issue.kv)) + return (issue.title, issue.description, issue.type, self.json2kv(issue.kv)) def add_issue(self, iid, title, description, vtype, kv): "Add or update an issue designated by IID." @@ -214,8 +207,9 @@ def add_issue(self, iid, title, description, vtype, kv): # If we ADD, then SALT will be NULL. If we UPDATE, then it will not # be touched (it should be NULL). - self.c_add_issue.perform(iid, self.eid, title, description, vtype, - self.kv2json(kv)) + self.c_add_issue.perform( + iid, self.eid, title, description, vtype, self.kv2json(kv) + ) def delete_issue(self, iid): "Delete the Issue designated by IID." @@ -234,15 +228,16 @@ def list_issues(self): "Return ordered EasyDicgt for all ISSUES." def extract_issue(row): - return easydict.EasyDict(iid=row.iid, - title=row.title, - description=row.description, - type=row.type, - kv=self.json2kv(row.kv), - ) + return easydict.EasyDict( + iid=row.iid, + title=row.title, + description=row.description, + type=row.type, + kv=self.json2kv(row.kv), + ) self.q_issues.perform(self.eid) - return [ extract_issue(row) for row in self.q_issues.fetchall() ] + return [extract_issue(row) for row in self.q_issues.fetchall()] def add_voter(self, pid: str, iid: str | None = None) -> None: "Add PID (Person) to Issue IID, or to all Issues (None)." @@ -289,7 +284,7 @@ def tally_issue(self, iid): issue = self.q_get_issue.first_row(iid) # Accumulate all MOST-RECENT votes for Issue IID. - votes = [ ] + votes = [] # Use mayvote to determine all potential voters for Issue IID. self.q_tally.perform(iid) @@ -298,13 +293,18 @@ def tally_issue(self, iid): # For the given Person PID found, compute a VOTE_TOKEN. vote_token = crypto.gen_vote_token( - md.opened_key, mayvote.pid, iid, mayvote.salt, + md.opened_key, + mayvote.pid, + iid, + mayvote.salt, ) # We don't need/want all columns, so only pick CIPHERTEXT. row = self.q_recent_vote.first_row(vote_token) votestring = crypto.decrypt_votestring( - vote_token, mayvote.salt, row.ciphertext, + vote_token, + mayvote.salt, + row.ciphertext, ) votes.append(votestring) @@ -323,26 +323,28 @@ def has_voted_upon(self, pid): # The Election should be open. md = self._all_metadata(self.S_OPEN) - voted_upon = { } + voted_upon = {} self.q_find_issues.perform(pid, self.eid) for row in self.q_find_issues.fetchall(): - #print('COLUMNS:', dir(row)) + # print('COLUMNS:', dir(row)) # Query is mayvote.* ... so ROW is: PID, IID, SALT vote_token = crypto.gen_vote_token( - md.opened_key, pid, row.iid, row.salt, + md.opened_key, + pid, + row.iid, + row.salt, ) # Is any vote present? (wicked fast) voted = self.q_has_voted.first_row(vote_token) - voted_upon[row.iid] = (voted is not None) + voted_upon[row.iid] = voted is not None return voted_upon def is_tampered(self, pdb): - # The Election should be open. md = self._all_metadata(self.S_OPEN) @@ -392,29 +394,31 @@ def _compute_state(cls, md): @staticmethod def kv2json(kv): - 'Convert a structured KV into a JSON string for storage.' + "Convert a structured KV into a JSON string for storage." # Note: avoid serializing None. return kv and json.dumps(kv) @staticmethod def json2kv(j): - 'Convert the KV JSON string back into its structured value.' + "Convert the KV JSON string back into its structured value." return j and json.loads(j) @classmethod - def create(cls, db_fname, title, owner_pid, - authz=None, open_at=None, close_at=None): + def create( + cls, db_fname, title, owner_pid, authz=None, open_at=None, close_at=None + ): # Open in autocommit conn = sqlite3.connect(db_fname, isolation_level=None) while True: eid = crypto.create_id() try: - conn.execute('INSERT INTO election' - ' (eid, title, owner_pid,' - ' authz, open_at, close_at)' - ' VALUES (?, ?, ?, ?, ?, ?)', - (eid, title, owner_pid, - authz, open_at, close_at)) + conn.execute( + 'INSERT INTO election' + ' (eid, title, owner_pid,' + ' authz, open_at, close_at)' + ' VALUES (?, ?, ?, ?, ?, ?)', + (eid, title, owner_pid, authz, open_at, close_at), + ) break except sqlite3.IntegrityError: _LOGGER.debug('EID conflict(!!) ... trying again.') @@ -434,8 +438,10 @@ def open_to_pid(cls, db_fname, pid): db = cls.open_database(db_fname) # Run the generator to get all rows. Returned as EasyDicts. - db.q_open_to_me.perform(pid,) - return [ row for row in db.q_open_to_me.fetchall() ] + db.q_open_to_me.perform( + pid, + ) + return [row for row in db.q_open_to_me.fetchall()] @classmethod def owned_elections(cls, db_fname, pid): @@ -447,8 +453,10 @@ def owned_elections(cls, db_fname, pid): # SALT or OPENED_KEY values. # # Run the generator to get all rows. Returned as EasyDicts. - db.q_owned.perform(pid,) - return [ row for row in db.q_owned.fetchall() ] + db.q_owned.perform( + pid, + ) + return [row for row in db.q_owned.fetchall()] def not_found(cursor, key): @@ -473,8 +481,10 @@ def __init__(self, eid, current, required): super().__init__(str(self)) def __str__(self): - return (f'Election[E:{self.eid}]' - f' is "{self.current}" but should be "{self.required}"') + return ( + f'Election[E:{self.eid}]' + f' is "{self.current}" but should be "{self.required}"' + ) class IssueNotFound(Exception): diff --git a/v3/steve/persondb.py b/v3/steve/persondb.py index 34b4a56..a87fe17 100644 --- a/v3/steve/persondb.py +++ b/v3/steve/persondb.py @@ -1,23 +1,19 @@ +# 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 # -# 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. -# -# ---- -# -# ### TBD: DOCCO +# 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 pathlib @@ -28,10 +24,8 @@ class PersonDB: - def __init__(self, db_fname): - self.db = asfpy.db.DB(db_fname, - yaml_fname=QUERIES, yaml_section='person') + self.db = asfpy.db.DB(db_fname, yaml_fname=QUERIES, yaml_section='person') def __getattr__(self, name): "Proxy the cursors." diff --git a/v3/steve/vtypes/__init__.py b/v3/steve/vtypes/__init__.py index 00c217d..b386b78 100644 --- a/v3/steve/vtypes/__init__.py +++ b/v3/steve/vtypes/__init__.py @@ -1,20 +1,22 @@ -# 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 +# 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 +# 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. +# 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. # NOTE: not using a dynamic system. Just define/import what we know. -TYPES = { 'yna', 'stv' } +TYPES = {'yna', 'stv'} from . import yna # noqa: E402,F401 from . import stv # noqa: E402,F401 diff --git a/v3/steve/vtypes/stv.py b/v3/steve/vtypes/stv.py index 14277e1..f82f891 100644 --- a/v3/steve/vtypes/stv.py +++ b/v3/steve/vtypes/stv.py @@ -1,23 +1,19 @@ +# 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 # -# 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. -# -# ---- -# -# ### TBD: DOCCO +# 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 os.path import importlib @@ -68,6 +64,8 @@ def tally(votestrings, kv, names=None): human = '\n'.join( f'{c.name:40}{" " if c.status == stv_tool.ELECTED else " not "}elected' for c in results.l - ) - data = { 'raw': results, } + ) + data = { + 'raw': results, + } return human, data diff --git a/v3/steve/vtypes/yna.py b/v3/steve/vtypes/yna.py index c80f04e..086c472 100644 --- a/v3/steve/vtypes/yna.py +++ b/v3/steve/vtypes/yna.py @@ -1,27 +1,33 @@ +# 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 # -# 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. -# -# ---- -# -# ### TBD: DOCCO +# 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. # The votestring will be lower-cased. What is the result? -YES = { 'y', 'yes', '1', 'true', } -NO = { 'n', 'no', '0', 'false', } +YES = { + 'y', + 'yes', + '1', + 'true', +} +NO = { + 'n', + 'no', + '0', + 'false', +} # "abstain" is any other (non-affirmative) value. @@ -35,9 +41,13 @@ def tally(votestrings, _kv): else: a += 1 - human = f'''\ + human = f"""\ Yes: {y:#4} No: {n:#4} -Abstain: {a:#4}''' +Abstain: {a:#4}""" - return human, {'y': y, 'n': n, 'a': a,} + return human, { + 'y': y, + 'n': n, + 'a': a, + } diff --git a/v3/tests/check_coverage.py b/v3/tests/check_coverage.py index b7ddc9a..0b22c6c 100755 --- a/v3/tests/check_coverage.py +++ b/v3/tests/check_coverage.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# + # 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 @@ -16,25 +16,18 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -# -# ---- -# -# ### TBD: DOCCO -# - import sys -import os.path +import os import sqlite3 import logging import pathlib -import coverage # pip3 install coverage +import coverage # Ensure that we can import the "steve" package. THIS_DIR = pathlib.Path(__file__).resolve().parent PARENT_DIR = THIS_DIR.parent -sys.path.insert(0, str(PARENT_DIR)) TESTING_DB = THIS_DIR / 'covtest.db' SCHEMA_FILE = PARENT_DIR / 'schema.sql' @@ -76,16 +69,22 @@ def touch_every_line(): i3 = steve.crypto.create_id() e.add_issue(i1, 'issue A', None, 'yna', None) - e.add_issue(i2, 'issue B', None, 'stv', { - 'seats': 3, - 'labelmap': { - 'a': 'Alice', - 'b': 'Bob', - 'c': 'Carlos', - 'd': 'David', - 'e': 'Eve', + e.add_issue( + i2, + 'issue B', + None, + 'stv', + { + 'seats': 3, + 'labelmap': { + 'a': 'Alice', + 'b': 'Bob', + 'c': 'Carlos', + 'd': 'David', + 'e': 'Eve', }, - }) + }, + ) _ = e.list_issues() e.add_issue(i3, 'issue C', None, 'yna', None) e.delete_issue(i3) @@ -127,9 +126,12 @@ def touch_every_line(): def main(): cov = coverage.Coverage( - data_file=None, branch=True, config_file=False, - source_pkgs=['steve'], messages=True, - ) + data_file=None, + branch=True, + config_file=False, + source_pkgs=['steve'], + messages=True, + ) cov.start() try: @@ -143,9 +145,10 @@ def main(): if __name__ == '__main__': DATE_FORMAT = '%m/%d %H:%M' - logging.basicConfig(level=logging.DEBUG, - style='{', - format='[{asctime}|{levelname}|{module}] {message}', - datefmt=DATE_FORMAT, - ) + logging.basicConfig( + level=logging.DEBUG, + style='{', + format='[{asctime}|{levelname}|{module}] {message}', + datefmt=DATE_FORMAT, + ) main() diff --git a/v3/tests/populate_v2_stv.sh b/v3/tests/populate_v2_stv.sh index e46b0a7..2dfebb4 100755 --- a/v3/tests/populate_v2_stv.sh +++ b/v3/tests/populate_v2_stv.sh @@ -1,5 +1,5 @@ -#!/bin/bash -# +#!/usr/bin/env bash + # 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 @@ -16,13 +16,10 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -# -# # Gather "all" known STV voting processing into a reference directory, # with full debug output. These files can then be used as a comparison # to future developments on STV tooling, to ensure consistency. -# if test "$1" = ""; then echo "USAGE: $0 MEETINGS_DIR"; exit 1; fi MEETINGS_DIR="$1" diff --git a/v3/tests/run_stv.py b/v3/tests/run_stv.py index 6af113b..ef623f7 100755 --- a/v3/tests/run_stv.py +++ b/v3/tests/run_stv.py @@ -17,7 +17,6 @@ # specific language governing permissions and limitations # under the License. - # USAGE: uv run run_stv.py .../Meetings/yyyymmdd import sys @@ -46,7 +45,7 @@ def main(mtgdir): kv = { 'labelmap': labelmap, 'seats': 9, - } + } # NOTE: for backwards-compat, the tally() function accepts a # list of names with caller-defined sorting.