diff --git a/PYTHON/downloadCats/generate_cats.py b/PYTHON/downloadCats/generate_cats.py index b6a59b9..8f5b9d8 100644 --- a/PYTHON/downloadCats/generate_cats.py +++ b/PYTHON/downloadCats/generate_cats.py @@ -12,8 +12,10 @@ import requests logging.basicConfig(level=logging.INFO) +MAX_REQUESTS = 90 + requests_send = 0 -while requests_send < 90: +while requests_send < MAX_REQUESTS: res = requests.get("https://api.thecatapi.com/v1/images/search?limit=100&api_key=") requests_send += 1 response = json.loads(res.text) diff --git a/PYTHON/keyboardCoop/main.py b/PYTHON/keyboardCoop/main.py index 623d3fa..c5669e5 100644 --- a/PYTHON/keyboardCoop/main.py +++ b/PYTHON/keyboardCoop/main.py @@ -27,6 +27,7 @@ KEY_SELECTED_COLOR = (150, 150, 200) KEY_AVAILABLE_COLOR = (100, 150, 100) TEXT_COLOR = (255, 255, 255) PLAYER_COLORS = [(255, 100, 100), (100, 100, 255)] +MIN_WORD_LENGTH = 3 # Keyboard layout KEYBOARD_LAYOUT = [ @@ -286,7 +287,7 @@ class KeyboardCoopGame: def calculate_score(self, word_length): """Calculate score exponentially based on word length.""" - if word_length < 3: + if word_length < MIN_WORD_LENGTH: return 0 return 2 ** (word_length - 2) @@ -317,7 +318,9 @@ class KeyboardCoopGame: def submit_word(self): """Submit the current word and check if it's valid.""" - if len(self.current_word) >= 3 and self.is_valid_word(self.current_word): + if len(self.current_word) >= MIN_WORD_LENGTH and self.is_valid_word( + self.current_word + ): points = self.calculate_score(len(self.current_word)) self.score += points self.message = ( @@ -329,8 +332,11 @@ class KeyboardCoopGame: self.generate_random_keyboard() self.key_positions = self.calculate_key_positions() - elif len(self.current_word) < 3: - self.message = f"'{self.current_word}' is too short! (minimum 3 letters)" + elif len(self.current_word) < MIN_WORD_LENGTH: + self.message = ( + f"'{self.current_word}' is too short! " + f"(minimum {MIN_WORD_LENGTH} letters)" + ) else: self.message = f"'{self.current_word}' is not a valid word!" diff --git a/PYTHON/lichess_bot/lichess_api.py b/PYTHON/lichess_bot/lichess_api.py index 15121e8..3c4859e 100644 --- a/PYTHON/lichess_bot/lichess_api.py +++ b/PYTHON/lichess_bot/lichess_api.py @@ -2,6 +2,7 @@ from collections.abc import Generator import contextlib +from http import HTTPStatus import json import logging import time @@ -45,7 +46,7 @@ class LichessAPI: raise elapsed = time.monotonic() - t0 status = r.status_code - if status >= 400: + if status >= HTTPStatus.BAD_REQUEST: # Log a brief error body snippet if available snippet = None try: @@ -88,7 +89,7 @@ class LichessAPI: logging.debug(f"Skipping non-JSON line: {line}") except requests.HTTPError as e: status = getattr(e.response, "status_code", None) - if status == 429: + if status == HTTPStatus.TOO_MANY_REQUESTS: logging.warning("Event stream hit 429; backing off") time.sleep(backoff) backoff = min(8.0, backoff * 2) @@ -160,11 +161,11 @@ class LichessAPI: """Submit a move to an active game.""" url = f"{LICHESS_API}/api/board/game/{game_id}/move/{move.uci()}" r = self._request("POST", url, timeout=30) - if r.status_code in (400, 409): + if r.status_code in (HTTPStatus.BAD_REQUEST, HTTPStatus.CONFLICT): # Likely not our turn or move already played; do not retry to avoid spam r.raise_for_status() return - if r.status_code == 429: + if r.status_code == HTTPStatus.TOO_MANY_REQUESTS: logging.warning(f"HTTP POST {url} -> 429; retrying once after 0.5s") time.sleep(0.5) r = self._request("POST", url, timeout=30) @@ -178,6 +179,6 @@ class LichessAPI: """Fetch the authenticated user's ID.""" url = f"{LICHESS_API}/api/account" r = self._request("GET", url, timeout=30) - if r.status_code == 200: + if r.status_code == HTTPStatus.OK: return r.json().get("id") return None diff --git a/PYTHON/lichess_bot/tools/generate_blunder_tests.py b/PYTHON/lichess_bot/tools/generate_blunder_tests.py index 73e9164..94c4b9a 100755 --- a/PYTHON/lichess_bot/tools/generate_blunder_tests.py +++ b/PYTHON/lichess_bot/tools/generate_blunder_tests.py @@ -45,6 +45,10 @@ import chess.pgn logging.basicConfig(level=logging.INFO) +# Expected columns in the log file: +# ply, side, move, played_eval, best_eval, loss, class, best_suggestion +EXPECTED_COLUMNS = 8 + @dataclass class Blunder: @@ -79,7 +83,7 @@ def parse_columns_for_blunders(text: str) -> list[Blunder]: parts = re.split(r"\s{2,}", ln.strip()) # Expected columns: # ply, side, move, played_eval, best_eval, loss, class, best_suggestion - if len(parts) < 8: + if len(parts) < EXPECTED_COLUMNS: continue try: ply = int(parts[0]) diff --git a/PYTHON/randomJPG/generateJpeg.py b/PYTHON/randomJPG/generateJpeg.py index e639c2b..a92f5c8 100644 --- a/PYTHON/randomJPG/generateJpeg.py +++ b/PYTHON/randomJPG/generateJpeg.py @@ -10,6 +10,8 @@ from PIL import Image logging.basicConfig(level=logging.INFO) +MAX_IMAGE_SIZE = 1000 + def generate_bloated_jpeg( size, color_list, block_size, output_path, quality, image_index, folder @@ -26,9 +28,11 @@ def generate_bloated_jpeg( image_index (int): Index of the image for unique naming. folder (str): Folder to save the image. """ - # Ensure size is divisible by block_size and does not exceed 1000 pixels - if size > 1000 or size % block_size != 0: - msg = "Size must be 1000 pixels or less and divisible by block_size" + # Ensure size is divisible by block_size and does not exceed MAX_IMAGE_SIZE + if size > MAX_IMAGE_SIZE or size % block_size != 0: + msg = ( + f"Size must be {MAX_IMAGE_SIZE} pixels or less and divisible by block_size" + ) raise ValueError(msg) # Create a new image diff --git a/PYTHON/randomize_numbers/random_digits.py b/PYTHON/randomize_numbers/random_digits.py index ac549b9..cc47949 100644 --- a/PYTHON/randomize_numbers/random_digits.py +++ b/PYTHON/randomize_numbers/random_digits.py @@ -8,8 +8,15 @@ import sys logging.basicConfig(level=logging.INFO) +DEFAULT_MIN_PERCENTAGE = 1 +DEFAULT_MAX_PERCENTAGE = 20 -def randomize_numbers(numbers, min_percentage=1, max_percentage=20): + +def randomize_numbers( + numbers, + min_percentage=DEFAULT_MIN_PERCENTAGE, + max_percentage=DEFAULT_MAX_PERCENTAGE, +): """Apply random percentage variation to a list of numbers.""" randomized_numbers = [] for number in numbers: @@ -43,8 +50,10 @@ def parse_input(input_string): return numbers, decimal_counts +MIN_ARGS = 2 + if __name__ == "__main__": - if len(sys.argv) < 2: + if len(sys.argv) < MIN_ARGS: logging.info( "Usage: python random_digits.py ... " "[min_percentage max_percentage]" @@ -54,8 +63,8 @@ if __name__ == "__main__": try: input_string = " ".join(sys.argv[1:]) numbers, decimal_counts = parse_input(input_string) - min_percentage = 1 - max_percentage = 20 + min_percentage = DEFAULT_MIN_PERCENTAGE + max_percentage = DEFAULT_MAX_PERCENTAGE if len(numbers) == 0: msg = "No valid numbers provided." diff --git a/PYTHON/screen_locker/screen_lock.py b/PYTHON/screen_locker/screen_lock.py index 7fadb12..bc02604 100755 --- a/PYTHON/screen_locker/screen_lock.py +++ b/PYTHON/screen_locker/screen_lock.py @@ -13,6 +13,15 @@ import tkinter as tk logging.basicConfig(level=logging.INFO) +# Validation limits for workout data +MAX_DISTANCE_KM = 100 +MAX_TIME_MINUTES = 600 +MAX_PACE_MIN_PER_KM = 20 +MIN_EXERCISE_NAME_LEN = 3 +MAX_SETS = 20 +MAX_REPS = 100 +MAX_WEIGHT_KG = 500 + class ScreenLocker: """Screen locker that requires workout logging to unlock.""" @@ -294,16 +303,20 @@ class ScreenLocker: pace = float(self.pace_entry.get()) # Sanity checks - if distance <= 0 or distance > 100: - self.show_error("Distance seems unrealistic (0-100 km)") + if distance <= 0 or distance > MAX_DISTANCE_KM: + self.show_error(f"Distance seems unrealistic (0-{MAX_DISTANCE_KM} km)") return - if time_mins <= 0 or time_mins > 600: - self.show_error("Time seems unrealistic (0-600 minutes)") + if time_mins <= 0 or time_mins > MAX_TIME_MINUTES: + self.show_error( + f"Time seems unrealistic (0-{MAX_TIME_MINUTES} minutes)" + ) return - if pace <= 0 or pace > 20: - self.show_error("Pace seems unrealistic (0-20 min/km)") + if pace <= 0 or pace > MAX_PACE_MIN_PER_KM: + self.show_error( + f"Pace seems unrealistic (0-{MAX_PACE_MIN_PER_KM} min/km)" + ) return # Calculate expected pace and check if close enough @@ -463,21 +476,21 @@ class ScreenLocker: return # Check for empty or lazy entries - if any(len(ex) < 3 for ex in exercises): + if any(len(ex) < MIN_EXERCISE_NAME_LEN for ex in exercises): self.show_error("Exercise names too short - be specific") return # Sanity checks - if any(s < 1 or s > 20 for s in sets): - self.show_error("Sets should be between 1-20") + if any(s < 1 or s > MAX_SETS for s in sets): + self.show_error(f"Sets should be between 1-{MAX_SETS}") return - if any(r < 1 or r > 100 for r in reps): - self.show_error("Reps should be between 1-100") + if any(r < 1 or r > MAX_REPS for r in reps): + self.show_error(f"Reps should be between 1-{MAX_REPS}") return - if any(w < 0 or w > 500 for w in weights): - self.show_error("Weights should be between 0-500 kg") + if any(w < 0 or w > MAX_WEIGHT_KG for w in weights): + self.show_error(f"Weights should be between 0-{MAX_WEIGHT_KG} kg") return # Calculate expected total weight diff --git a/PYTHON/stockfish_analysis/analyze_chess_game.py b/PYTHON/stockfish_analysis/analyze_chess_game.py index 5d32291..dd011ed 100755 --- a/PYTHON/stockfish_analysis/analyze_chess_game.py +++ b/PYTHON/stockfish_analysis/analyze_chess_game.py @@ -44,6 +44,10 @@ except Exception: # pragma: no cover logging.exception(" pip install -r PYTHON/stockfish_analysis/requirements.txt") raise +# Memory configuration constants +MEMINFO_PARTS_MIN = 2 +HIGH_THREAD_COUNT = 16 + def extract_pgn_text(raw: str) -> str | None: """Try to extract a PGN block from a possibly noisy file. @@ -97,6 +101,14 @@ def score_to_cp( return s.score(mate_score=None), None +# Centipawn loss thresholds for move quality classification (Lichess-like bands) +CP_LOSS_BEST = 10 +CP_LOSS_EXCELLENT = 20 +CP_LOSS_GOOD = 50 +CP_LOSS_INACCURACY = 99 +CP_LOSS_MISTAKE = 299 + + def classify_cp_loss(cp_loss: int | None) -> str: """Classify move quality using Lichess-like centipawn loss bands. @@ -111,15 +123,15 @@ def classify_cp_loss(cp_loss: int | None) -> str: """ if cp_loss is None: return "Unknown" - if cp_loss <= 10: + if cp_loss <= CP_LOSS_BEST: return "Best" - if cp_loss <= 20: + if cp_loss <= CP_LOSS_EXCELLENT: return "Excellent" - if cp_loss <= 50: + if cp_loss <= CP_LOSS_GOOD: return "Good" - if cp_loss <= 99: + if cp_loss <= CP_LOSS_INACCURACY: return "Inaccuracy" - if cp_loss <= 299: + if cp_loss <= CP_LOSS_MISTAKE: return "Mistake" return "Blunder" @@ -172,7 +184,7 @@ def _detect_total_mem_mb() -> int | None: for line in f: if line.startswith("MemTotal:"): parts = line.split() - if len(parts) >= 2 and parts[1].isdigit(): + if len(parts) >= MEMINFO_PARTS_MIN and parts[1].isdigit(): # Value is in kB kb = int(parts[1]) return kb // 1024 @@ -196,7 +208,7 @@ def _auto_hash_mb(threads_wanted: int, engine_options) -> int: if isinstance(max_allowed, int): target = min(target, max_allowed) # Some rough scaling: if very many threads, give a bit more (but not huge) - if threads_wanted >= 16: + if threads_wanted >= HIGH_THREAD_COUNT: target = min(target + 1024, (total_mb * 3) // 4) return max(64, int(target)) diff --git a/articles/test_server_api.py b/articles/test_server_api.py index 3e16366..ef63331 100644 --- a/articles/test_server_api.py +++ b/articles/test_server_api.py @@ -1,5 +1,6 @@ """Integration tests for the articles C server API.""" +from http import HTTPStatus import json import os from pathlib import Path @@ -62,19 +63,19 @@ def test_crud_roundtrip(tmp_path): "thumb": "data:image/png;base64,xyz", }, ) - assert code == 201 + assert code == HTTPStatus.CREATED created = json.loads(body) art_id = created["id"] # List code, body = _req(base + "/api/articles") - assert code == 200 + assert code == HTTPStatus.OK items = json.loads(body) assert any(a["id"] == art_id for a in items) # Get one code, body = _req(base + f"/api/articles/{art_id}") - assert code == 200 + assert code == HTTPStatus.OK got = json.loads(body) assert got["title"] == "T1" @@ -82,13 +83,13 @@ def test_crud_roundtrip(tmp_path): code, body = _req( base + f"/api/articles/{art_id}", method="PUT", data={"title": "T2"} ) - assert code == 200 + assert code == HTTPStatus.OK updated = json.loads(body) assert updated["title"] == "T2" # Delete code, _ = _req(base + f"/api/articles/{art_id}", method="DELETE") - assert code == 204 + assert code == HTTPStatus.NO_CONTENT # Ensure gone try: @@ -96,7 +97,7 @@ def test_crud_roundtrip(tmp_path): msg = "Expected 404" raise AssertionError(msg) except urllib.error.HTTPError as e: - assert e.code == 404 + assert e.code == HTTPStatus.NOT_FOUND finally: srv.terminate() diff --git a/pyproject.toml b/pyproject.toml index aec335f..7d07335 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,6 @@ ignore = [ "COM812", # Trailing comma missing (conflicts with formatter) "ISC001", # Implicit string concatenation (conflicts with formatter) # Relaxed for script-heavy repository - "PLR2004", # Magic value comparison - common in scripts "S101", # Use of assert - acceptable in this codebase "ANN001", # Missing type annotation for function argument "ANN002", # Missing type annotation for *args