diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1349384..62bc646 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: - python-version: ["3.x", "3.11", "3.10", "3.9", "3.8", "3.7"] + python-version: ["3.x", "3.11", "3.10", "3.9", "3.8"] platform: [ubuntu-latest, macos-latest, windows-latest] # exclude: diff --git a/Pipfile b/Pipfile index bf5e770..c2697bc 100644 --- a/Pipfile +++ b/Pipfile @@ -7,7 +7,6 @@ name = "pypi" colorama = ">=0.4.6" opencv-contrib-python = "*" # for macOS: opencv-contrib-python = "<=4.7.0" -# for PYTHON <= 3.7: typing_extensions = "*" pillow = "*" pyzbar = "*" protobuf = "*" diff --git a/Pipfile.lock b/Pipfile.lock index a9537be..779c387 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -52,7 +52,7 @@ "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357", "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760" ], - "markers": "python_version >= '3.8'", + "markers": "python_version >= '3.10'", "version": "==1.25.2" }, "opencv-contrib-python": { @@ -489,7 +489,7 @@ "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0", "sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8" ], - "markers": "python_full_version >= '3.8.0'", + "markers": "python_version >= '3.8'", "version": "==2.11.0" }, "pyflakes": { @@ -497,7 +497,7 @@ "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774", "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc" ], - "markers": "python_full_version >= '3.8.0'", + "markers": "python_version >= '3.8'", "version": "==3.1.0" }, "pylint": { @@ -582,11 +582,11 @@ }, "wheel": { "hashes": [ - "sha256:55a0f0a5a84869bce5ba775abfd9c462e3a6b1b7b7ec69d72c0b83d673a5114d", - "sha256:7e9be3bbd0078f6147d82ed9ed957e323e7708f57e134743d2edef3a7b7972a9" + "sha256:12b911f083e876e10c595779709f8a88a59f45aacc646492a67fe9ef796c1b47", + "sha256:473219bd4cbedc62cea0cb309089b593e47c15c4a2531015f94e4e3b9a0f6981" ], "index": "pypi", - "version": "==0.41.0" + "version": "==0.41.1" }, "wrapt": { "hashes": [ diff --git a/README.md b/README.md index ddd56c0..0d9573d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen) [![License](https://img.shields.io/github/license/scito/extract_otp_secrets)](https://github.com/scito/extract_otp_secrets/blob/master/LICENSE) [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/scito/extract_otp_secrets?sort=semver)](https://github.com/scito/extract_otp_secrets/releases/latest) -![python versions](https://img.shields.io/badge/python-3.7%20%7C%203.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue) +![python versions](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue) [![Docker image](https://img.shields.io/badge/docker-image-blue)](https://hub.docker.com/repository/docker/scit0/extract_otp_secrets/general) [![Linux](https://img.shields.io/badge/os-linux-yellow)](https://github.com/scito/extract_otp_secrets/releases/latest) [![Windows](https://img.shields.io/badge/os-windows-yellow)](https://github.com/scito/extract_otp_secrets/releases/latest) @@ -367,7 +367,7 @@ python extract_otp_secrets.py = < example_export.png * macOS * Windows * Uses UTF-8 on all platforms -* Supports Python >= 3.7 +* Supports Python >= 3.8 * Installation of shared system libraries is optional (🆕 since v2.3) * Provides a debug mode (-d) for analyzing import problems * Written in modern Python using type hints and following best practices diff --git a/pyproject.toml b/pyproject.toml index a5eeb0a..c23b945 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,9 +47,6 @@ dependencies = [ "pyzbar", "qrcode", "qreader<2.0.0", - # workaround for PYTHON <= 3.7: compatibility - "typing_extensions; python_version<='3.7'", - "importlib_metadata; python_version<='3.7'", ] description = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps such as 'Google Authenticator'" dynamic = ["version"] diff --git a/src/extract_otp_secrets.py b/src/extract_otp_secrets.py index 368b62e..0a0f066 100644 --- a/src/extract_otp_secrets.py +++ b/src/extract_otp_secrets.py @@ -43,26 +43,15 @@ import re import sys import urllib.parse as urlparse from enum import Enum, IntEnum -from typing import Any, List, Optional, Sequence, TextIO, Tuple, Union, TYPE_CHECKING +from importlib.metadata import PackageNotFoundError, version +from typing import (Any, Final, List, Optional, Sequence, TextIO, Tuple, + TypedDict, Union) import colorama from qrcode import QRCode # type: ignore import protobuf_generated_python.google_auth_pb2 as pb -# workaround for PYTHON <= 3.7: compatibility -if sys.version_info >= (3, 8): - from typing import Final, TypedDict -else: - from typing_extensions import Final, TypedDict - -# workaround for PYTHON <= 3.7: compatibility -if sys.version_info >= (3, 8): - from importlib.metadata import PackageNotFoundError, version -else: - from importlib_metadata import PackageNotFoundError, version - - debug_mode = '-d' in sys.argv[1:] or '--debug' in sys.argv[1:] quiet = '-q' in sys.argv[1:] or '--quiet' in sys.argv[1:] headless: bool = False @@ -103,9 +92,8 @@ Exception: {e}\n""", file=sys.stderr) FONT_SCALE: Final[float] = 1.3 FONT_THICKNESS: Final[int] = 1 FONT_LINE_STYLE: Final[int] = cv2.LINE_AA - FONT_COLOR: Final[ColorBGR] = (255, 0, 0) + FONT_COLOR: Final[ColorBGR] = 255, 0, 0 BOX_THICKNESS: Final[int] = 5 - # workaround for PYTHON <= 3.7: must use () for assignments WINDOW_X: Final[int] = 0 WINDOW_Y: Final[int] = 1 WINDOW_WIDTH: Final[int] = 2 @@ -114,10 +102,10 @@ Exception: {e}\n""", file=sys.stderr) TEXT_HEIGHT: Final[int] = 1 BORDER: Final[int] = 5 START_Y: Final[int] = 20 - START_POS_TEXT: Final[Point] = (BORDER, START_Y) - NORMAL_COLOR: Final[ColorBGR] = (255, 0, 255) - SUCCESS_COLOR: Final[ColorBGR] = (0, 255, 0) - FAILURE_COLOR: Final[ColorBGR] = (0, 0, 255) + START_POS_TEXT: Final[Point] = BORDER, START_Y + NORMAL_COLOR: Final[ColorBGR] = 255, 0, 255 + SUCCESS_COLOR: Final[ColorBGR] = 0, 255, 0 + FAILURE_COLOR: Final[ColorBGR] = 0, 0, 255 CHAR_DX: Final[int] = (lambda text: cv2.getTextSize(text, FONT, FONT_SCALE, FONT_THICKNESS)[0][TEXT_WIDTH] // len(text))("28 QR codes capturedMMM") FONT_DY: Final[int] = cv2.getTextSize("M", FONT, FONT_SCALE, FONT_THICKNESS)[0][TEXT_HEIGHT] + 5 WINDOW_NAME: Final[str] = "Extract OTP Secrets: Capture QR Codes from Camera" @@ -130,12 +118,12 @@ except ImportError as e: if debug_mode: raise e -# Workaround for PYTHON <= 3.9: Union[int, None] used instead of int | None +# Workaround for PYTHON <= 3.9: Generally Union[int, None] used instead of int | None # Types Args = argparse.Namespace OtpUrl = str -# workaround for PYTHON <= 3.7: Otp = TypedDict('Otp', {'name': str, 'secret': str, 'issuer': str, 'type': str, 'counter': int | None, 'url': OtpUrl}) +# Workaround for PYTHON <= 3.9: Otp = TypedDict('Otp', {'name': str, 'secret': str, 'issuer': str, 'type': str, 'counter': int | None, 'url': OtpUrl}) Otp = TypedDict('Otp', {'name': str, 'secret': str, 'issuer': str, 'type': str, 'counter': Union[int, None], 'url': OtpUrl}) # workaround for PYTHON <= 3.9: Otps = list[Otp] Otps = List[Otp] @@ -275,9 +263,7 @@ def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count: def parse_args(sys_args: list[str]) -> Args: global verbose, quiet, colored - # For PYTHON <= 3.7: Use := - name = os.path.basename(sys.argv[0]) - cmd = f"python {name}" if name.endswith('.py') else f"{name}" + cmd = f"python {name}" if (name := os.path.basename(sys.argv[0])).endswith('.py') else f"{name}" description_text = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps" if cv2_available: description_text += "\nIf no infiles are provided, a GUI window starts and QR codes are captured from the camera." @@ -324,7 +310,7 @@ b) image file containing a QR code or = for stdin for an image containing a QR c quiet = True if args.quiet else False if verbose: print(f"QReader installed: {cv2_available}") if cv2_available: - if verbose >= LogLevel.VERBOSE: print(f"CV2 version: -") # TODO {cv2.__version__} + if verbose >= LogLevel.VERBOSE: print(f"CV2 version: {cv2.__version__}") # type: ignore # cv2.__version__ is not available if verbose: print(f"QR reading mode: {args.qr}\n") return args