add type hints (Python 3.11)

cv2_1
scito 2 years ago
parent f933cd0d32
commit 201e6510f8

@ -47,7 +47,7 @@ jobs:
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=200 --statistics flake8 . --count --exit-zero --max-complexity=10 --max-line-length=200 --statistics
- name: Type checking with mypy - name: Type checking with mypy
run: | run: |
mypy *.py mypy --strict *.py
if: matrix.python-version == "3.x" if: matrix.python-version == "3.x"
- name: Test with pytest - name: Test with pytest
run: pytest run: pytest

@ -239,10 +239,12 @@ The data parameter is a base64 encoded proto3 message (Google Protocol Buffers).
Command for regeneration of Python code from proto3 message definition file (only necessary in case of changes of the proto3 message definition or new protobuf versions): Command for regeneration of Python code from proto3 message definition file (only necessary in case of changes of the proto3 message definition or new protobuf versions):
protoc --python_out=protobuf_generated_python google_auth.proto protoc --python_out=protobuf_generated_python google_auth.proto --mypy_out=protobuf_generated_python
The generated protobuf Python code was generated by protoc 21.12 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.12). The generated protobuf Python code was generated by protoc 21.12 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.12).
https://github.com/nipunn1313/mypy-protobuf
## References ## References
* Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial * Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial

@ -1,10 +1,11 @@
import pytest import pytest
from typing import Any
def pytest_addoption(parser): def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption("--relaxed", action='store_true', help="run tests in relaxed mode") parser.addoption("--relaxed", action='store_true', help="run tests in relaxed mode")
@pytest.fixture @pytest.fixture
def relaxed(request): def relaxed(request: pytest.FixtureRequest) -> Any:
return request.config.getoption("--relaxed") return request.config.getoption("--relaxed")

@ -52,15 +52,16 @@ import sys
import urllib.parse as urlparse import urllib.parse as urlparse
from enum import Enum from enum import Enum
from operator import add from operator import add
from typing import TextIO, Any, TypedDict
from qrcode import QRCode # type: ignore from qrcode import QRCode # type: ignore
import protobuf_generated_python.google_auth_pb2 # type: ignore import protobuf_generated_python.google_auth_pb2 as migration_protobuf
try: try:
import cv2 # type: ignore import cv2 # type: ignore
import numpy # type: ignore import numpy
try: try:
import pyzbar.pyzbar as zbar # type: ignore import pyzbar.pyzbar as zbar # type: ignore
@ -75,8 +76,17 @@ Exception: {e}""")
except ImportError: except ImportError:
qreader_available = False qreader_available = False
verbose: int # Types
quiet: bool Args = argparse.Namespace
OtpUrl = str
Otp = TypedDict('Otp', {'name': str, 'secret': str, 'issuer': str, 'type': str, 'counter': int | None, 'url': OtpUrl})
Otps = list[Otp]
OtpUrls = list[OtpUrl]
# Global variable declaration
verbose: int = 0
quiet: bool = False
def sys_main() -> None: def sys_main() -> None:
@ -95,7 +105,7 @@ def main(sys_args: list[str]) -> None:
write_json(args, otps) write_json(args, otps)
def parse_args(sys_args: list[str]) -> argparse.Namespace: def parse_args(sys_args: list[str]) -> Args:
global verbose, quiet global verbose, quiet
description_text = "Extracts one time password (OTP) secret keys from QR codes, e.g. from Google Authenticator app." description_text = "Extracts one time password (OTP) secret keys from QR codes, e.g. from Google Authenticator app."
if qreader_available: if qreader_available:
@ -133,17 +143,17 @@ b) image file containing a QR code or = for stdin for an image containing a QR c
return args return args
def extract_otps(args): def extract_otps(args: Args) -> Otps:
if not args.infile: if not args.infile:
return extract_otps_from_camera(args) return extract_otps_from_camera(args)
else: else:
return extract_otps_from_files(args) return extract_otps_from_files(args)
def extract_otps_from_camera(args): def extract_otps_from_camera(args: Args) -> Otps:
if verbose: print("Capture QR codes from camera") if verbose: print("Capture QR codes from camera")
otp_urls = [] otp_urls: OtpUrls = []
otps = [] otps: Otps = []
QRMode = Enum('QRMode', ['QREADER', 'DEEP_QREADER', 'CV2'], start=0) QRMode = Enum('QRMode', ['QREADER', 'DEEP_QREADER', 'CV2'], start=0)
qr_mode = QRMode.QREADER qr_mode = QRMode.QREADER
@ -215,7 +225,7 @@ def extract_otps_from_camera(args):
return otps return otps
def extract_otps_from_otp_url(otp_url, otp_urls, otps, args): def extract_otps_from_otp_url(otp_url: str, otp_urls: OtpUrls, otps: Otps, args: Args) -> None:
if otp_url and verbose: print(otp_url) if otp_url and verbose: print(otp_url)
if otp_url and otp_url not in otp_urls: if otp_url and otp_url not in otp_urls:
otp_urls.append(otp_url) otp_urls.append(otp_url)
@ -223,8 +233,8 @@ def extract_otps_from_otp_url(otp_url, otp_urls, otps, args):
if verbose: print(f"{len(otps)} otp{'s'[:len(otps) != 1]} from {len(otp_urls)} QR code{'s'[:len(otp_urls) != 1]} extracted") if verbose: print(f"{len(otps)} otp{'s'[:len(otps) != 1]} from {len(otp_urls)} QR code{'s'[:len(otp_urls) != 1]} extracted")
def extract_otps_from_files(args): def extract_otps_from_files(args: Args) -> Otps:
otps = [] otps: Otps = []
i = j = k = 0 i = j = k = 0
if verbose: print(f"Input files: {args.infile}") if verbose: print(f"Input files: {args.infile}")
@ -240,7 +250,7 @@ def extract_otps_from_files(args):
return otps return otps
def get_otp_urls_from_file(filename): def get_otp_urls_from_file(filename: str) -> OtpUrls:
# stdin stream cannot be rewinded, thus distinguish, use - for utf-8 stdin and = for binary image stdin # stdin stream cannot be rewinded, thus distinguish, use - for utf-8 stdin and = for binary image stdin
if filename != '=': if filename != '=':
check_file_exists(filename) check_file_exists(filename)
@ -255,7 +265,7 @@ def get_otp_urls_from_file(filename):
return [] return []
def read_lines_from_text_file(filename): def read_lines_from_text_file(filename: str) -> list[str]:
if verbose: print(f"Reading lines of {filename}") if verbose: print(f"Reading lines of {filename}")
finput = fileinput.input(filename) finput = fileinput.input(filename)
try: try:
@ -268,18 +278,18 @@ def read_lines_from_text_file(filename):
lines.append(line) lines.append(line)
if not lines: if not lines:
eprint(f"WARN: {filename.replace('-', 'stdin')} is empty") eprint(f"WARN: {filename.replace('-', 'stdin')} is empty")
return lines
except UnicodeDecodeError: except UnicodeDecodeError:
if filename == '-': if filename == '-':
abort("\nERROR: Unable to open text file form stdin. " abort("\nERROR: Unable to open text file form stdin. "
"In case you want read an image file from stdin, you must use '=' instead of '-'.") "In case you want read an image file from stdin, you must use '=' instead of '-'.")
else: # The file is probably an image, process below else: # The file is probably an image, process below
return None return []
finally: finally:
finput.close() finput.close()
return lines
def extract_otp_from_otp_url(otpauth_migration_url, otps, i, j, infile, args): def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, i: int, j: int, infile: str, args: Args) -> int:
payload = get_payload_from_otp_url(otpauth_migration_url, i, infile) payload = get_payload_from_otp_url(otpauth_migration_url, i, infile)
# pylint: disable=no-member # pylint: disable=no-member
@ -290,7 +300,7 @@ def extract_otp_from_otp_url(otpauth_migration_url, otps, i, j, infile, args):
if verbose: print('OTP enum type:', get_enum_name_by_number(raw_otp, 'type')) if verbose: print('OTP enum type:', get_enum_name_by_number(raw_otp, 'type'))
otp_type = get_otp_type_str_from_code(raw_otp.type) otp_type = get_otp_type_str_from_code(raw_otp.type)
otp_url = build_otp_url(secret, raw_otp) otp_url = build_otp_url(secret, raw_otp)
otp = { otp: Otp = {
"name": raw_otp.name, "name": raw_otp.name,
"secret": secret, "secret": secret,
"issuer": raw_otp.issuer, "issuer": raw_otp.issuer,
@ -311,7 +321,7 @@ def extract_otp_from_otp_url(otpauth_migration_url, otps, i, j, infile, args):
return j return j
def convert_img_to_otp_url(filename): def convert_img_to_otp_url(filename: str) -> OtpUrls:
if verbose: print(f"Reading image {filename}") if verbose: print(f"Reading image {filename}")
try: try:
if filename != '=': if filename != '=':
@ -321,7 +331,7 @@ def convert_img_to_otp_url(filename):
stdin = sys.stdin.buffer.read() stdin = sys.stdin.buffer.read()
except AttributeError: except AttributeError:
# Workaround for pytest, since pytest cannot monkeypatch sys.stdin.buffer # Workaround for pytest, since pytest cannot monkeypatch sys.stdin.buffer
stdin = sys.stdin.read() stdin = sys.stdin.read() # type: ignore # Workaround for pytest fixtures
if not stdin: if not stdin:
eprint("WARN: stdin is empty") eprint("WARN: stdin is empty")
try: try:
@ -338,13 +348,12 @@ def convert_img_to_otp_url(filename):
decoded_text = QReader().detect_and_decode(img) decoded_text = QReader().detect_and_decode(img)
if decoded_text is None: if decoded_text is None:
abort(f"\nERROR: Unable to read QR Code from file.\ninput file: {filename}") abort(f"\nERROR: Unable to read QR Code from file.\ninput file: {filename}")
return [decoded_text]
except Exception as e: except Exception as e:
abort(f"\nERROR: Encountered exception '{e}'.\ninput file: {filename}") abort(f"\nERROR: Encountered exception '{e}'.\ninput file: {filename}")
return [decoded_text]
def get_payload_from_otp_url(otpauth_migration_url, i, input_source): def get_payload_from_otp_url(otpauth_migration_url: str, i: int, input_source: str) -> migration_protobuf.MigrationPayload:
if not otpauth_migration_url.startswith('otpauth-migration://'): if not otpauth_migration_url.startswith('otpauth-migration://'):
eprint(f"\nWARN: line is not a otpauth-migration:// URL\ninput: {input_source}\nline '{otpauth_migration_url}'\nProbably a wrong file was given") eprint(f"\nWARN: line is not a otpauth-migration:// URL\ninput: {input_source}\nline '{otpauth_migration_url}'\nProbably a wrong file was given")
parsed_url = urlparse.urlparse(otpauth_migration_url) parsed_url = urlparse.urlparse(otpauth_migration_url)
@ -352,7 +361,7 @@ def get_payload_from_otp_url(otpauth_migration_url, i, input_source):
try: try:
params = urlparse.parse_qs(parsed_url.query, strict_parsing=True) params = urlparse.parse_qs(parsed_url.query, strict_parsing=True)
except Exception: # Not necessary for Python >= 3.11 except Exception: # Not necessary for Python >= 3.11
params = [] params = {}
if verbose > 2: print(f"\nDEBUG: querystring params={params}") if verbose > 2: print(f"\nDEBUG: querystring params={params}")
if 'data' not in params: if 'data' not in params:
abort(f"\nERROR: no data query parameter in input URL\ninput file: {input_source}\nline '{otpauth_migration_url}'\nProbably a wrong file was given") abort(f"\nERROR: no data query parameter in input URL\ninput file: {input_source}\nline '{otpauth_migration_url}'\nProbably a wrong file was given")
@ -361,7 +370,7 @@ def get_payload_from_otp_url(otpauth_migration_url, i, input_source):
data_base64_fixed = data_base64.replace(' ', '+') data_base64_fixed = data_base64.replace(' ', '+')
if verbose > 2: print(f"\nDEBUG: data_base64_fixed={data_base64_fixed}") if verbose > 2: print(f"\nDEBUG: data_base64_fixed={data_base64_fixed}")
data = base64.b64decode(data_base64_fixed, validate=True) data = base64.b64decode(data_base64_fixed, validate=True)
payload = protobuf_generated_python.google_auth_pb2.MigrationPayload() payload = migration_protobuf.MigrationPayload()
try: try:
payload.ParseFromString(data) payload.ParseFromString(data)
except Exception: except Exception:
@ -374,28 +383,28 @@ def get_payload_from_otp_url(otpauth_migration_url, i, input_source):
# https://stackoverflow.com/questions/40226049/find-enums-listed-in-python-descriptor-for-protobuf # https://stackoverflow.com/questions/40226049/find-enums-listed-in-python-descriptor-for-protobuf
def get_enum_name_by_number(parent, field_name): def get_enum_name_by_number(parent: Any, field_name: str) -> str:
field_value = getattr(parent, field_name) field_value = getattr(parent, field_name)
return parent.DESCRIPTOR.fields_by_name[field_name].enum_type.values_by_number.get(field_value).name return parent.DESCRIPTOR.fields_by_name[field_name].enum_type.values_by_number.get(field_value).name # type: ignore # generic code
def get_otp_type_str_from_code(otp_type): def get_otp_type_str_from_code(otp_type: int) -> str:
return 'totp' if otp_type == 2 else 'hotp' return 'totp' if otp_type == 2 else 'hotp'
def convert_secret_from_bytes_to_base32_str(bytes): def convert_secret_from_bytes_to_base32_str(bytes: bytes) -> str:
return str(base64.b32encode(bytes), 'utf-8').replace('=', '') return str(base64.b32encode(bytes), 'utf-8').replace('=', '')
def build_otp_url(secret, raw_otp): def build_otp_url(secret: str, raw_otp: migration_protobuf.MigrationPayload.OtpParameters) -> str:
url_params = {'secret': secret} url_params = {'secret': secret}
if raw_otp.type == 1: url_params['counter'] = raw_otp.counter if raw_otp.type == 1: url_params['counter'] = str(raw_otp.counter)
if raw_otp.issuer: url_params['issuer'] = raw_otp.issuer if raw_otp.issuer: url_params['issuer'] = raw_otp.issuer
otp_url = f"otpauth://{get_otp_type_str_from_code(raw_otp.type)}/{urlparse.quote(raw_otp.name)}?" + urlparse.urlencode(url_params) otp_url = f"otpauth://{get_otp_type_str_from_code(raw_otp.type)}/{urlparse.quote(raw_otp.name)}?" + urlparse.urlencode(url_params)
return otp_url return otp_url
def print_otp(otp): def print_otp(otp: Otp) -> None:
print(f"Name: {otp['name']}") print(f"Name: {otp['name']}")
print(f"Secret: {otp['secret']}") print(f"Secret: {otp['secret']}")
if otp['issuer']: print(f"Issuer: {otp['issuer']}") if otp['issuer']: print(f"Issuer: {otp['issuer']}")
@ -406,7 +415,7 @@ def print_otp(otp):
print(otp['url']) print(otp['url'])
def save_qr(otp, args, j): def save_qr(otp: Otp, args: Args, j: int) -> str:
dir = args.saveqr dir = args.saveqr
if not (os.path.exists(dir)): os.makedirs(dir, exist_ok=True) if not (os.path.exists(dir)): os.makedirs(dir, exist_ok=True)
pattern = re.compile(r'[\W_]+') pattern = re.compile(r'[\W_]+')
@ -416,21 +425,21 @@ def save_qr(otp, args, j):
return file_otp_issuer return file_otp_issuer
def save_qr_file(args, data, name): def save_qr_file(args: Args, otp_url: OtpUrl, name: str) -> None:
qr = QRCode() qr = QRCode()
qr.add_data(data) qr.add_data(otp_url)
img = qr.make_image(fill_color='black', back_color='white') img = qr.make_image(fill_color='black', back_color='white')
if verbose: print(f"Saving to {name}") if verbose: print(f"Saving to {name}")
img.save(name) img.save(name)
def print_qr(args, data): def print_qr(args: Args, otp_url: str) -> None:
qr = QRCode() qr = QRCode()
qr.add_data(data) qr.add_data(otp_url)
qr.print_ascii() qr.print_ascii()
def write_csv(args, otps): def write_csv(args: Args, otps: Otps) -> None:
if args.csv and len(otps) > 0: if args.csv and len(otps) > 0:
with open_file_or_stdout_for_csv(args.csv) as outfile: with open_file_or_stdout_for_csv(args.csv) as outfile:
writer = csv.DictWriter(outfile, otps[0].keys()) writer = csv.DictWriter(outfile, otps[0].keys())
@ -439,7 +448,7 @@ def write_csv(args, otps):
if not quiet: print(f"Exported {len(otps)} otp{'s'[:len(otps) != 1]} to csv {args.csv}") if not quiet: print(f"Exported {len(otps)} otp{'s'[:len(otps) != 1]} to csv {args.csv}")
def write_keepass_csv(args, otps): def write_keepass_csv(args: Args, otps: Otps) -> None:
if args.keepass and len(otps) > 0: if args.keepass and len(otps) > 0:
has_totp = has_otp_type(otps, 'totp') has_totp = has_otp_type(otps, 'totp')
has_hotp = has_otp_type(otps, 'hotp') has_hotp = has_otp_type(otps, 'hotp')
@ -479,34 +488,34 @@ def write_keepass_csv(args, otps):
if count_hotp_entries > 0: print(f"Exported {count_hotp_entries} hotp entrie{'s'[:count_hotp_entries != 1]} to keepass csv file {otp_filename_hotp}") if count_hotp_entries > 0: print(f"Exported {count_hotp_entries} hotp entrie{'s'[:count_hotp_entries != 1]} to keepass csv file {otp_filename_hotp}")
def write_json(args, otps): def write_json(args: Args, otps: Otps) -> None:
if args.json: if args.json:
with open_file_or_stdout(args.json) as outfile: with open_file_or_stdout(args.json) as outfile:
json.dump(otps, outfile, indent=4) json.dump(otps, outfile, indent=4)
if not quiet: print(f"Exported {len(otps)} otp{'s'[:len(otps) != 1]} to json {args.json}") if not quiet: print(f"Exported {len(otps)} otp{'s'[:len(otps) != 1]} to json {args.json}")
def has_otp_type(otps, otp_type): def has_otp_type(otps: Otps, otp_type: str) -> bool:
for otp in otps: for otp in otps:
if otp['type'] == otp_type: if otp['type'] == otp_type:
return True return True
return False return False
def add_pre_suffix(file, pre_suffix): def add_pre_suffix(file: str, pre_suffix: str) -> str:
'''filename.ext, pre -> filename.pre.ext''' '''filename.ext, pre -> filename.pre.ext'''
name, ext = os.path.splitext(file) name, ext = os.path.splitext(file)
return name + "." + pre_suffix + (ext if ext else "") return name + "." + pre_suffix + (ext if ext else "")
def open_file_or_stdout(filename): def open_file_or_stdout(filename: str) -> TextIO:
'''stdout is denoted as "-". '''stdout is denoted as "-".
Note: Set before the following line: Note: Set before the following line:
sys.stdout.close = lambda: None''' sys.stdout.close = lambda: None'''
return open(filename, "w", encoding='utf-8') if filename != '-' else sys.stdout return open(filename, "w", encoding='utf-8') if filename != '-' else sys.stdout
def open_file_or_stdout_for_csv(filename): def open_file_or_stdout_for_csv(filename: str) -> TextIO:
'''stdout is denoted as "-". '''stdout is denoted as "-".
newline='' newline=''
Note: Set before the following line: Note: Set before the following line:
@ -514,13 +523,13 @@ def open_file_or_stdout_for_csv(filename):
return open(filename, "w", encoding='utf-8', newline='') if filename != '-' else sys.stdout return open(filename, "w", encoding='utf-8', newline='') if filename != '-' else sys.stdout
def check_file_exists(filename): def check_file_exists(filename: str) -> None:
if filename != '-' and not os.path.isfile(filename): if filename != '-' and not os.path.isfile(filename):
abort(f"\nERROR: Input file provided is non-existent or not a file." abort(f"\nERROR: Input file provided is non-existent or not a file."
f"\ninput file: {filename}") f"\ninput file: {filename}")
def is_binary(line): def is_binary(line: str) -> bool:
try: try:
line.startswith('#') line.startswith('#')
return False return False
@ -528,12 +537,12 @@ def is_binary(line):
return True return True
def eprint(*args, **kwargs): def eprint(*args: Any, **kwargs: Any) -> None:
'''Print to stderr.''' '''Print to stderr.'''
print(*args, file=sys.stderr, **kwargs) print(*args, file=sys.stderr, **kwargs)
def abort(*args, **kwargs): def abort(*args: Any, **kwargs: Any) -> None:
eprint(*args, **kwargs) eprint(*args, **kwargs)
sys.exit(1) sys.exit(1)

@ -0,0 +1,108 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
"""
import builtins
import collections.abc
import google.protobuf.descriptor
import google.protobuf.internal.containers
import google.protobuf.internal.enum_type_wrapper
import google.protobuf.message
import sys
import typing
if sys.version_info >= (3, 10):
import typing as typing_extensions
else:
import typing_extensions
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
@typing_extensions.final
class MigrationPayload(google.protobuf.message.Message):
"""Copied from: https://github.com/beemdevelopment/Aegis/blob/master/app/src/main/proto/google_auth.proto"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _Algorithm:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _AlgorithmEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[MigrationPayload._Algorithm.ValueType], builtins.type): # noqa: F821
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
ALGO_INVALID: MigrationPayload._Algorithm.ValueType # 0
ALGO_SHA1: MigrationPayload._Algorithm.ValueType # 1
class Algorithm(_Algorithm, metaclass=_AlgorithmEnumTypeWrapper): ...
ALGO_INVALID: MigrationPayload.Algorithm.ValueType # 0
ALGO_SHA1: MigrationPayload.Algorithm.ValueType # 1
class _OtpType:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _OtpTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[MigrationPayload._OtpType.ValueType], builtins.type): # noqa: F821
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
OTP_INVALID: MigrationPayload._OtpType.ValueType # 0
OTP_HOTP: MigrationPayload._OtpType.ValueType # 1
OTP_TOTP: MigrationPayload._OtpType.ValueType # 2
class OtpType(_OtpType, metaclass=_OtpTypeEnumTypeWrapper): ...
OTP_INVALID: MigrationPayload.OtpType.ValueType # 0
OTP_HOTP: MigrationPayload.OtpType.ValueType # 1
OTP_TOTP: MigrationPayload.OtpType.ValueType # 2
@typing_extensions.final
class OtpParameters(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
SECRET_FIELD_NUMBER: builtins.int
NAME_FIELD_NUMBER: builtins.int
ISSUER_FIELD_NUMBER: builtins.int
ALGORITHM_FIELD_NUMBER: builtins.int
DIGITS_FIELD_NUMBER: builtins.int
TYPE_FIELD_NUMBER: builtins.int
COUNTER_FIELD_NUMBER: builtins.int
secret: builtins.bytes
name: builtins.str
issuer: builtins.str
algorithm: global___MigrationPayload.Algorithm.ValueType
digits: builtins.int
type: global___MigrationPayload.OtpType.ValueType
counter: builtins.int
def __init__(
self,
*,
secret: builtins.bytes = ...,
name: builtins.str = ...,
issuer: builtins.str = ...,
algorithm: global___MigrationPayload.Algorithm.ValueType = ...,
digits: builtins.int = ...,
type: global___MigrationPayload.OtpType.ValueType = ...,
counter: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["algorithm", b"algorithm", "counter", b"counter", "digits", b"digits", "issuer", b"issuer", "name", b"name", "secret", b"secret", "type", b"type"]) -> None: ...
OTP_PARAMETERS_FIELD_NUMBER: builtins.int
VERSION_FIELD_NUMBER: builtins.int
BATCH_SIZE_FIELD_NUMBER: builtins.int
BATCH_INDEX_FIELD_NUMBER: builtins.int
BATCH_ID_FIELD_NUMBER: builtins.int
@property
def otp_parameters(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MigrationPayload.OtpParameters]: ...
version: builtins.int
batch_size: builtins.int
batch_index: builtins.int
batch_id: builtins.int
def __init__(
self,
*,
otp_parameters: collections.abc.Iterable[global___MigrationPayload.OtpParameters] | None = ...,
version: builtins.int = ...,
batch_size: builtins.int = ...,
batch_index: builtins.int = ...,
batch_id: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["batch_id", b"batch_id", "batch_index", b"batch_index", "batch_size", b"batch_size", "otp_parameters", b"otp_parameters", "version", b"version"]) -> None: ...
global___MigrationPayload = MigrationPayload

@ -21,8 +21,10 @@
import io import io
import os import os
import sys import sys
import pathlib
import pytest import pytest
from pytest_mock import MockerFixture
import extract_otp_secret_keys import extract_otp_secret_keys
from utils import (file_exits, quick_and_dirty_workaround_encoding_problem, from utils import (file_exits, quick_and_dirty_workaround_encoding_problem,
@ -30,10 +32,10 @@ from utils import (file_exits, quick_and_dirty_workaround_encoding_problem,
read_file_to_str, read_json, read_json_str, read_file_to_str, read_json, read_json_str,
replace_escaped_octal_utf8_bytes_with_str) replace_escaped_octal_utf8_bytes_with_str)
qreader_available = extract_otp_secret_keys.qreader_available qreader_available: bool = extract_otp_secret_keys.qreader_available
def test_extract_stdout(capsys): def test_extract_stdout(capsys: pytest.CaptureFixture[str]) -> None:
# Act # Act
extract_otp_secret_keys.main(['example_export.txt']) extract_otp_secret_keys.main(['example_export.txt'])
@ -44,7 +46,7 @@ def test_extract_stdout(capsys):
assert captured.err == '' assert captured.err == ''
def test_extract_non_existent_file(capsys): def test_extract_non_existent_file(capsys: pytest.CaptureFixture[str]) -> None:
# Act # Act
with pytest.raises(SystemExit) as e: with pytest.raises(SystemExit) as e:
extract_otp_secret_keys.main(['test/non_existent_file.txt']) extract_otp_secret_keys.main(['test/non_existent_file.txt'])
@ -60,7 +62,7 @@ def test_extract_non_existent_file(capsys):
assert e.type == SystemExit assert e.type == SystemExit
def test_extract_stdin_stdout(capsys, monkeypatch): def test_extract_stdin_stdout(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange # Arrange
monkeypatch.setattr('sys.stdin', io.StringIO(read_file_to_str('example_export.txt'))) monkeypatch.setattr('sys.stdin', io.StringIO(read_file_to_str('example_export.txt')))
@ -74,7 +76,7 @@ def test_extract_stdin_stdout(capsys, monkeypatch):
assert captured.err == '' assert captured.err == ''
def test_extract_stdin_empty(capsys, monkeypatch): def test_extract_stdin_empty(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange # Arrange
monkeypatch.setattr('sys.stdin', io.StringIO()) monkeypatch.setattr('sys.stdin', io.StringIO())
@ -89,7 +91,7 @@ def test_extract_stdin_empty(capsys, monkeypatch):
# @pytest.mark.skipif(not qreader_available, reason='Test if cv2 and qreader are not available.') # @pytest.mark.skipif(not qreader_available, reason='Test if cv2 and qreader are not available.')
def test_extract_empty_file_no_qreader(capsys): def test_extract_empty_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> None:
if qreader_available: if qreader_available:
# Act # Act
with pytest.raises(SystemExit) as e: with pytest.raises(SystemExit) as e:
@ -116,7 +118,7 @@ def test_extract_empty_file_no_qreader(capsys):
@pytest.mark.qreader @pytest.mark.qreader
def test_extract_stdin_img_empty(capsys, monkeypatch): def test_extract_stdin_img_empty(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange # Arrange
monkeypatch.setattr('sys.stdin', io.BytesIO()) monkeypatch.setattr('sys.stdin', io.BytesIO())
@ -130,7 +132,7 @@ def test_extract_stdin_img_empty(capsys, monkeypatch):
assert captured.err == 'WARN: stdin is empty\n' assert captured.err == 'WARN: stdin is empty\n'
def test_extract_csv(capsys, tmp_path): def test_extract_csv(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
# Arrange # Arrange
output_file = str(tmp_path / 'test_example_output.csv') output_file = str(tmp_path / 'test_example_output.csv')
@ -149,7 +151,7 @@ def test_extract_csv(capsys, tmp_path):
assert captured.err == '' assert captured.err == ''
def test_extract_csv_stdout(capsys): def test_extract_csv_stdout(capsys: pytest.CaptureFixture[str]) -> None:
# Act # Act
extract_otp_secret_keys.main(['-c', '-', 'example_export.txt']) extract_otp_secret_keys.main(['-c', '-', 'example_export.txt'])
@ -165,7 +167,7 @@ def test_extract_csv_stdout(capsys):
assert captured.err == '' assert captured.err == ''
def test_extract_stdin_and_csv_stdout(capsys, monkeypatch): def test_extract_stdin_and_csv_stdout(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange # Arrange
monkeypatch.setattr('sys.stdin', io.StringIO(read_file_to_str('example_export.txt'))) monkeypatch.setattr('sys.stdin', io.StringIO(read_file_to_str('example_export.txt')))
@ -184,7 +186,7 @@ def test_extract_stdin_and_csv_stdout(capsys, monkeypatch):
assert captured.err == '' assert captured.err == ''
def test_keepass_csv(capsys, tmp_path): def test_keepass_csv(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
'''Two csv files .totp and .htop are generated.''' '''Two csv files .totp and .htop are generated.'''
# Arrange # Arrange
file_name = str(tmp_path / 'test_example_keepass_output.csv') file_name = str(tmp_path / 'test_example_keepass_output.csv')
@ -208,7 +210,7 @@ def test_keepass_csv(capsys, tmp_path):
assert captured.err == '' assert captured.err == ''
def test_keepass_csv_stdout(capsys): def test_keepass_csv_stdout(capsys: pytest.CaptureFixture[str]) -> None:
'''Two csv files .totp and .htop are generated.''' '''Two csv files .totp and .htop are generated.'''
# Act # Act
extract_otp_secret_keys.main(['-k', '-', 'test/example_export_only_totp.txt']) extract_otp_secret_keys.main(['-k', '-', 'test/example_export_only_totp.txt'])
@ -226,7 +228,7 @@ def test_keepass_csv_stdout(capsys):
assert captured.err == '' assert captured.err == ''
def test_single_keepass_csv(capsys, tmp_path): def test_single_keepass_csv(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
'''Does not add .totp or .hotp pre-suffix''' '''Does not add .totp or .hotp pre-suffix'''
# Act # Act
extract_otp_secret_keys.main(['-q', '-k', str(tmp_path / 'test_example_keepass_output.csv'), 'test/example_export_only_totp.txt']) extract_otp_secret_keys.main(['-q', '-k', str(tmp_path / 'test_example_keepass_output.csv'), 'test/example_export_only_totp.txt'])
@ -245,7 +247,7 @@ def test_single_keepass_csv(capsys, tmp_path):
assert captured.err == '' assert captured.err == ''
def test_extract_json(capsys, tmp_path): def test_extract_json(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
# Arrange # Arrange
output_file = str(tmp_path / 'test_example_output.json') output_file = str(tmp_path / 'test_example_output.json')
@ -264,7 +266,7 @@ def test_extract_json(capsys, tmp_path):
assert captured.err == '' assert captured.err == ''
def test_extract_json_stdout(capsys): def test_extract_json_stdout(capsys: pytest.CaptureFixture[str]) -> None:
# Act # Act
extract_otp_secret_keys.main(['-j', '-', 'example_export.txt']) extract_otp_secret_keys.main(['-j', '-', 'example_export.txt'])
@ -278,7 +280,7 @@ def test_extract_json_stdout(capsys):
assert captured.err == '' assert captured.err == ''
def test_extract_not_encoded_plus(capsys): def test_extract_not_encoded_plus(capsys: pytest.CaptureFixture[str]) -> None:
# Act # Act
extract_otp_secret_keys.main(['test/test_plus_problem_export.txt']) extract_otp_secret_keys.main(['test/test_plus_problem_export.txt'])
@ -311,7 +313,7 @@ Type: totp
assert captured.err == '' assert captured.err == ''
def test_extract_printqr(capsys): def test_extract_printqr(capsys: pytest.CaptureFixture[str]) -> None:
# Act # Act
extract_otp_secret_keys.main(['-p', 'example_export.txt']) extract_otp_secret_keys.main(['-p', 'example_export.txt'])
@ -324,7 +326,7 @@ def test_extract_printqr(capsys):
assert captured.err == '' assert captured.err == ''
def test_extract_saveqr(capsys, tmp_path): def test_extract_saveqr(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
# Act # Act
extract_otp_secret_keys.main(['-q', '-s', str(tmp_path), 'example_export.txt']) extract_otp_secret_keys.main(['-q', '-s', str(tmp_path), 'example_export.txt'])
@ -340,12 +342,12 @@ def test_extract_saveqr(capsys, tmp_path):
assert os.path.isfile(tmp_path / '4-piraspberrypi-raspberrypi.png') assert os.path.isfile(tmp_path / '4-piraspberrypi-raspberrypi.png')
def test_normalize_bytes(): def test_normalize_bytes() -> None:
assert replace_escaped_octal_utf8_bytes_with_str( assert replace_escaped_octal_utf8_bytes_with_str(
'Before\\\\302\\\\277\\\\303\nname: enc: \\302\\277\\303\\244\\303\\204\\303\\251\\303\\211?\nAfter') == 'Before\\\\302\\\\277\\\\303\nname: enc: ¿äÄéÉ?\nAfter' 'Before\\\\302\\\\277\\\\303\nname: enc: \\302\\277\\303\\244\\303\\204\\303\\251\\303\\211?\nAfter') == 'Before\\\\302\\\\277\\\\303\nname: enc: ¿äÄéÉ?\nAfter'
def test_extract_verbose(capsys, relaxed): def test_extract_verbose(capsys: pytest.CaptureFixture[str], relaxed: bool) -> None:
# Act # Act
extract_otp_secret_keys.main(['-v', 'example_export.txt']) extract_otp_secret_keys.main(['-v', 'example_export.txt'])
@ -367,7 +369,7 @@ def test_extract_verbose(capsys, relaxed):
assert captured.err == '' assert captured.err == ''
def test_extract_debug(capsys): def test_extract_debug(capsys: pytest.CaptureFixture[str]) -> None:
# Act # Act
extract_otp_secret_keys.main(['-vvv', 'example_export.txt']) extract_otp_secret_keys.main(['-vvv', 'example_export.txt'])
@ -381,7 +383,7 @@ def test_extract_debug(capsys):
assert captured.err == '' assert captured.err == ''
def test_extract_help(capsys): def test_extract_help(capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as e: with pytest.raises(SystemExit) as e:
# Act # Act
extract_otp_secret_keys.main(['-h']) extract_otp_secret_keys.main(['-h'])
@ -396,7 +398,7 @@ def test_extract_help(capsys):
assert e.value.code == 0 assert e.value.code == 0
def test_extract_no_arguments(capsys, mocker): def test_extract_no_arguments(capsys: pytest.CaptureFixture[str], mocker: MockerFixture) -> None:
if qreader_available: if qreader_available:
# Arrange # Arrange
otps = read_json('example_output.json') otps = read_json('example_output.json')
@ -429,7 +431,7 @@ def test_extract_no_arguments(capsys, mocker):
assert e.type == SystemExit assert e.type == SystemExit
def test_verbose_and_quiet(capsys): def test_verbose_and_quiet(capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as e: with pytest.raises(SystemExit) as e:
# Act # Act
extract_otp_secret_keys.main(['-v', '-q', 'example_export.txt']) extract_otp_secret_keys.main(['-v', '-q', 'example_export.txt'])
@ -444,7 +446,7 @@ def test_verbose_and_quiet(capsys):
assert e.type == SystemExit assert e.type == SystemExit
def test_wrong_data(capsys): def test_wrong_data(capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as e: with pytest.raises(SystemExit) as e:
# Act # Act
extract_otp_secret_keys.main(['test/test_export_wrong_data.txt']) extract_otp_secret_keys.main(['test/test_export_wrong_data.txt'])
@ -463,7 +465,7 @@ data=XXXX
assert e.type == SystemExit assert e.type == SystemExit
def test_wrong_content(capsys): def test_wrong_content(capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as e: with pytest.raises(SystemExit) as e:
# Act # Act
extract_otp_secret_keys.main(['test/test_export_wrong_content.txt']) extract_otp_secret_keys.main(['test/test_export_wrong_content.txt'])
@ -489,7 +491,7 @@ Probably a wrong file was given
assert e.type == SystemExit assert e.type == SystemExit
def test_wrong_prefix(capsys): def test_wrong_prefix(capsys: pytest.CaptureFixture[str]) -> None:
# Act # Act
extract_otp_secret_keys.main(['test/test_export_wrong_prefix.txt']) extract_otp_secret_keys.main(['test/test_export_wrong_prefix.txt'])
@ -514,14 +516,14 @@ Type: totp
assert captured.err == expected_stderr assert captured.err == expected_stderr
def test_add_pre_suffix(capsys): def test_add_pre_suffix(capsys: pytest.CaptureFixture[str]) -> None:
assert extract_otp_secret_keys.add_pre_suffix("name.csv", "totp") == "name.totp.csv" assert extract_otp_secret_keys.add_pre_suffix("name.csv", "totp") == "name.totp.csv"
assert extract_otp_secret_keys.add_pre_suffix("name.csv", "") == "name..csv" assert extract_otp_secret_keys.add_pre_suffix("name.csv", "") == "name..csv"
assert extract_otp_secret_keys.add_pre_suffix("name", "totp") == "name.totp" assert extract_otp_secret_keys.add_pre_suffix("name", "totp") == "name.totp"
@pytest.mark.qreader @pytest.mark.qreader
def test_img_qr_reader_from_file_happy_path(capsys): def test_img_qr_reader_from_file_happy_path(capsys: pytest.CaptureFixture[str]) -> None:
# Act # Act
extract_otp_secret_keys.main(['test/test_googleauth_export.png']) extract_otp_secret_keys.main(['test/test_googleauth_export.png'])
@ -533,7 +535,7 @@ def test_img_qr_reader_from_file_happy_path(capsys):
@pytest.mark.qreader @pytest.mark.qreader
def test_extract_multiple_files_and_mixed(capsys): def test_extract_multiple_files_and_mixed(capsys: pytest.CaptureFixture[str]) -> None:
# Act # Act
extract_otp_secret_keys.main([ extract_otp_secret_keys.main([
'example_export.txt', 'example_export.txt',
@ -549,7 +551,7 @@ def test_extract_multiple_files_and_mixed(capsys):
@pytest.mark.qreader @pytest.mark.qreader
def test_img_qr_reader_from_stdin(capsys, monkeypatch): def test_img_qr_reader_from_stdin(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange # Arrange
# sys.stdin.buffer should be monkey patched, but it does not work # sys.stdin.buffer should be monkey patched, but it does not work
monkeypatch.setattr('sys.stdin', read_binary_file_as_stream('test/test_googleauth_export.png')) monkeypatch.setattr('sys.stdin', read_binary_file_as_stream('test/test_googleauth_export.png'))
@ -582,7 +584,7 @@ Type: totp
@pytest.mark.qreader @pytest.mark.qreader
def test_img_qr_reader_from_stdin_wrong_symbol(capsys, monkeypatch): def test_img_qr_reader_from_stdin_wrong_symbol(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange # Arrange
# sys.stdin.buffer should be monkey patched, but it does not work # sys.stdin.buffer should be monkey patched, but it does not work
monkeypatch.setattr('sys.stdin', read_binary_file_as_stream('test/test_googleauth_export.png')) monkeypatch.setattr('sys.stdin', read_binary_file_as_stream('test/test_googleauth_export.png'))
@ -603,7 +605,7 @@ def test_img_qr_reader_from_stdin_wrong_symbol(capsys, monkeypatch):
@pytest.mark.qreader @pytest.mark.qreader
def test_extract_stdin_stdout_wrong_symbol(capsys, monkeypatch): def test_extract_stdin_stdout_wrong_symbol(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange # Arrange
monkeypatch.setattr('sys.stdin', io.StringIO(read_file_to_str('example_export.txt'))) monkeypatch.setattr('sys.stdin', io.StringIO(read_file_to_str('example_export.txt')))
@ -623,7 +625,7 @@ def test_extract_stdin_stdout_wrong_symbol(capsys, monkeypatch):
@pytest.mark.qreader @pytest.mark.qreader
def test_img_qr_reader_no_qr_code_in_image(capsys): def test_img_qr_reader_no_qr_code_in_image(capsys: pytest.CaptureFixture[str]) -> None:
# Act # Act
with pytest.raises(SystemExit) as e: with pytest.raises(SystemExit) as e:
extract_otp_secret_keys.main(['test/lena_std.tif']) extract_otp_secret_keys.main(['test/lena_std.tif'])
@ -640,7 +642,7 @@ def test_img_qr_reader_no_qr_code_in_image(capsys):
@pytest.mark.qreader @pytest.mark.qreader
def test_img_qr_reader_nonexistent_file(capsys): def test_img_qr_reader_nonexistent_file(capsys: pytest.CaptureFixture[str]) -> None:
# Act # Act
with pytest.raises(SystemExit) as e: with pytest.raises(SystemExit) as e:
extract_otp_secret_keys.main(['test/nonexistent.bmp']) extract_otp_secret_keys.main(['test/nonexistent.bmp'])
@ -656,7 +658,7 @@ def test_img_qr_reader_nonexistent_file(capsys):
assert e.type == SystemExit assert e.type == SystemExit
def test_non_image_file(capsys): def test_non_image_file(capsys: pytest.CaptureFixture[str]) -> None:
# Act # Act
with pytest.raises(SystemExit) as e: with pytest.raises(SystemExit) as e:
extract_otp_secret_keys.main(['test/text_masquerading_as_image.jpeg']) extract_otp_secret_keys.main(['test/text_masquerading_as_image.jpeg'])

@ -30,7 +30,7 @@ import extract_otp_secret_keys
class TestExtract(unittest.TestCase): class TestExtract(unittest.TestCase):
def test_extract_csv(self): def test_extract_csv(self) -> None:
extract_otp_secret_keys.main(['-q', '-c', 'test_example_output.csv', 'example_export.txt']) extract_otp_secret_keys.main(['-q', '-c', 'test_example_output.csv', 'example_export.txt'])
expected_csv = read_csv('example_output.csv') expected_csv = read_csv('example_output.csv')
@ -38,7 +38,7 @@ class TestExtract(unittest.TestCase):
self.assertEqual(actual_csv, expected_csv) self.assertEqual(actual_csv, expected_csv)
def test_extract_json(self): def test_extract_json(self) -> None:
extract_otp_secret_keys.main(['-q', '-j', 'test_example_output.json', 'example_export.txt']) extract_otp_secret_keys.main(['-q', '-j', 'test_example_output.json', 'example_export.txt'])
expected_json = read_json('example_output.json') expected_json = read_json('example_output.json')
@ -46,7 +46,7 @@ class TestExtract(unittest.TestCase):
self.assertEqual(actual_json, expected_json) self.assertEqual(actual_json, expected_json)
def test_extract_stdout_1(self): def test_extract_stdout_1(self) -> None:
with Capturing() as output: with Capturing() as output:
extract_otp_secret_keys.main(['example_export.txt']) extract_otp_secret_keys.main(['example_export.txt'])
@ -82,7 +82,7 @@ class TestExtract(unittest.TestCase):
self.assertEqual(output, expected_output) self.assertEqual(output, expected_output)
# Ref for capturing https://stackoverflow.com/a/40984270 # Ref for capturing https://stackoverflow.com/a/40984270
def test_extract_stdout_2(self): def test_extract_stdout_2(self) -> None:
out = io.StringIO() out = io.StringIO()
with redirect_stdout(out): with redirect_stdout(out):
extract_otp_secret_keys.main(['example_export.txt']) extract_otp_secret_keys.main(['example_export.txt'])
@ -118,7 +118,7 @@ Type: totp
''' '''
self.assertEqual(actual_output, expected_output) self.assertEqual(actual_output, expected_output)
def test_extract_not_encoded_plus(self): def test_extract_not_encoded_plus(self) -> None:
out = io.StringIO() out = io.StringIO()
with redirect_stdout(out): with redirect_stdout(out):
extract_otp_secret_keys.main(['test/test_plus_problem_export.txt']) extract_otp_secret_keys.main(['test/test_plus_problem_export.txt'])
@ -147,7 +147,7 @@ Type: totp
''' '''
self.assertEqual(actual_output, expected_output) self.assertEqual(actual_output, expected_output)
def test_extract_printqr(self): def test_extract_printqr(self) -> None:
out = io.StringIO() out = io.StringIO()
with redirect_stdout(out): with redirect_stdout(out):
extract_otp_secret_keys.main(['-p', 'example_export.txt']) extract_otp_secret_keys.main(['-p', 'example_export.txt'])
@ -157,7 +157,7 @@ Type: totp
self.assertEqual(actual_output, expected_output) self.assertEqual(actual_output, expected_output)
def test_extract_saveqr(self): def test_extract_saveqr(self) -> None:
extract_otp_secret_keys.main(['-q', '-s', 'testout/qr/', 'example_export.txt']) extract_otp_secret_keys.main(['-q', '-s', 'testout/qr/', 'example_export.txt'])
self.assertTrue(os.path.isfile('testout/qr/1-piraspberrypi-raspberrypi.png')) self.assertTrue(os.path.isfile('testout/qr/1-piraspberrypi-raspberrypi.png'))
@ -165,7 +165,7 @@ Type: totp
self.assertTrue(os.path.isfile('testout/qr/3-piraspberrypi.png')) self.assertTrue(os.path.isfile('testout/qr/3-piraspberrypi.png'))
self.assertTrue(os.path.isfile('testout/qr/4-piraspberrypi-raspberrypi.png')) self.assertTrue(os.path.isfile('testout/qr/4-piraspberrypi-raspberrypi.png'))
def test_extract_verbose(self): def test_extract_verbose(self) -> None:
if sys.implementation.name == 'pypy': self.skipTest("Encoding problems in verbose mode in pypy.") if sys.implementation.name == 'pypy': self.skipTest("Encoding problems in verbose mode in pypy.")
out = io.StringIO() out = io.StringIO()
with redirect_stdout(out): with redirect_stdout(out):
@ -176,7 +176,7 @@ Type: totp
self.assertEqual(actual_output, expected_output) self.assertEqual(actual_output, expected_output)
def test_extract_debug(self): def test_extract_debug(self) -> None:
out = io.StringIO() out = io.StringIO()
with redirect_stdout(out): with redirect_stdout(out):
extract_otp_secret_keys.main(['-vvv', 'example_export.txt']) extract_otp_secret_keys.main(['-vvv', 'example_export.txt'])
@ -187,7 +187,7 @@ Type: totp
self.assertGreater(len(actual_output), len(expected_stdout)) self.assertGreater(len(actual_output), len(expected_stdout))
self.assertTrue("DEBUG: " in actual_output) self.assertTrue("DEBUG: " in actual_output)
def test_extract_help_1(self): def test_extract_help_1(self) -> None:
out = io.StringIO() out = io.StringIO()
with redirect_stdout(out): with redirect_stdout(out):
try: try:
@ -201,7 +201,7 @@ Type: totp
self.assertGreater(len(actual_output), 0) self.assertGreater(len(actual_output), 0)
self.assertTrue("-h, --help" in actual_output and "--verbose, -v" in actual_output) self.assertTrue("-h, --help" in actual_output and "--verbose, -v" in actual_output)
def test_extract_help_2(self): def test_extract_help_2(self) -> None:
out = io.StringIO() out = io.StringIO()
with redirect_stdout(out): with redirect_stdout(out):
with self.assertRaises(SystemExit) as context: with self.assertRaises(SystemExit) as context:
@ -213,7 +213,7 @@ Type: totp
self.assertTrue("-h, --help" in actual_output and "--verbose, -v" in actual_output) self.assertTrue("-h, --help" in actual_output and "--verbose, -v" in actual_output)
self.assertEqual(context.exception.code, 0) self.assertEqual(context.exception.code, 0)
def test_extract_help_3(self): def test_extract_help_3(self) -> None:
with Capturing() as actual_output: with Capturing() as actual_output:
with self.assertRaises(SystemExit) as context: with self.assertRaises(SystemExit) as context:
extract_otp_secret_keys.main(['-h']) extract_otp_secret_keys.main(['-h'])
@ -222,13 +222,13 @@ Type: totp
self.assertTrue("-h, --help" in "\n".join(actual_output) and "--verbose, -v" in "\n".join(actual_output)) self.assertTrue("-h, --help" in "\n".join(actual_output) and "--verbose, -v" in "\n".join(actual_output))
self.assertEqual(context.exception.code, 0) self.assertEqual(context.exception.code, 0)
def setUp(self): def setUp(self) -> None:
self.cleanup() self.cleanup()
def tearDown(self): def tearDown(self) -> None:
self.cleanup() self.cleanup()
def cleanup(self): def cleanup(self) -> None:
remove_file('test_example_output.csv') remove_file('test_example_output.csv')
remove_file('test_example_output.json') remove_file('test_example_output.json')
remove_dir_with_files('testout/') remove_dir_with_files('testout/')

@ -25,7 +25,7 @@ import extract_otp_secret_keys
class TestQRImageExtract(unittest.TestCase): class TestQRImageExtract(unittest.TestCase):
def test_img_qr_reader_happy_path(self): def test_img_qr_reader_happy_path(self) -> None:
with Capturing() as actual_output: with Capturing() as actual_output:
extract_otp_secret_keys.main(['test/test_googleauth_export.png']) extract_otp_secret_keys.main(['test/test_googleauth_export.png'])
@ -36,7 +36,7 @@ class TestQRImageExtract(unittest.TestCase):
self.assertEqual(actual_output, expected_output) self.assertEqual(actual_output, expected_output)
def test_img_qr_reader_no_qr_code_in_image(self): def test_img_qr_reader_no_qr_code_in_image(self) -> None:
with Capturing() as actual_output: with Capturing() as actual_output:
with self.assertRaises(SystemExit) as context: with self.assertRaises(SystemExit) as context:
extract_otp_secret_keys.main(['test/lena_std.tif']) extract_otp_secret_keys.main(['test/lena_std.tif'])
@ -46,7 +46,7 @@ class TestQRImageExtract(unittest.TestCase):
self.assertEqual(actual_output, expected_output) self.assertEqual(actual_output, expected_output)
self.assertEqual(context.exception.code, 1) self.assertEqual(context.exception.code, 1)
def test_img_qr_reader_nonexistent_file(self): def test_img_qr_reader_nonexistent_file(self) -> None:
with Capturing() as actual_output: with Capturing() as actual_output:
with self.assertRaises(SystemExit) as context: with self.assertRaises(SystemExit) as context:
extract_otp_secret_keys.main(['test/nonexistent.bmp']) extract_otp_secret_keys.main(['test/nonexistent.bmp'])
@ -56,7 +56,7 @@ class TestQRImageExtract(unittest.TestCase):
self.assertEqual(actual_output, expected_output) self.assertEqual(actual_output, expected_output)
self.assertEqual(context.exception.code, 1) self.assertEqual(context.exception.code, 1)
def test_img_qr_reader_non_image_file(self): def test_img_qr_reader_non_image_file(self) -> None:
with Capturing() as actual_output: with Capturing() as actual_output:
with self.assertRaises(SystemExit) as context: with self.assertRaises(SystemExit) as context:
extract_otp_secret_keys.main(['test/text_masquerading_as_image.jpeg']) extract_otp_secret_keys.main(['test/text_masquerading_as_image.jpeg'])
@ -77,13 +77,13 @@ class TestQRImageExtract(unittest.TestCase):
self.assertEqual(actual_output, expected_output) self.assertEqual(actual_output, expected_output)
self.assertEqual(context.exception.code, 1) self.assertEqual(context.exception.code, 1)
def setUp(self): def setUp(self) -> None:
self.cleanup() self.cleanup()
def tearDown(self): def tearDown(self) -> None:
self.cleanup() self.cleanup()
def cleanup(self): def cleanup(self) -> None:
pass pass

@ -79,7 +79,7 @@ BASEVERSION=4
echo echo
interactive=true interactive=true
check_version=true ignore_version_check=true
while test $# -gt 0; do while test $# -gt 0; do
case $1 in case $1 in
@ -99,7 +99,7 @@ while test $# -gt 0; do
shift shift
;; ;;
-C) -C)
check_version=false ignore_version_check=false
shift shift
;; ;;
esac esac
@ -122,7 +122,7 @@ OLDVERSION=$(cat $BIN/$DEST/.VERSION.txt || echo "")
echo -e "\nProtoc remote version $VERSION\n" echo -e "\nProtoc remote version $VERSION\n"
echo -e "Protoc local version: $OLDVERSION\n" echo -e "Protoc local version: $OLDVERSION\n"
if [ "$OLDVERSION" != "$VERSION" ]; then if [ "$OLDVERSION" != "$VERSION" ] || ! $ignore_version_check; then
echo "Upgrade protoc from $OLDVERSION to $VERSION" echo "Upgrade protoc from $OLDVERSION to $VERSION"
NAME="protoc-$VERSION" NAME="protoc-$VERSION"
@ -162,7 +162,7 @@ if [ "$OLDVERSION" != "$VERSION" ]; then
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd" eval "$cmd"
cmd="$BIN/$DEST/bin/protoc --python_out=protobuf_generated_python google_auth.proto" cmd="$BIN/$DEST/bin/protoc --plugin=protoc-gen-mypy=/home/rkurmann/.local/bin/protoc-gen-mypy --python_out=protobuf_generated_python --mypy_out=protobuf_generated_python google_auth.proto"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd" eval "$cmd"
@ -196,6 +196,18 @@ cmd="$PIP install -U pipenv"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd" eval "$cmd"
cmd="sudo $PIP install --use-pep517 -U -r requirements.txt"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="sudo $PIP install --use-pep517 -U -r requirements-dev.txt"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="sudo $PIP install -U pipenv"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
$PIPENV --version $PIPENV --version
cmd="$PIPENV update && $PIPENV --rm && $PIPENV install" cmd="$PIPENV update && $PIPENV --rm && $PIPENV install"
@ -220,7 +232,7 @@ cmd="$MYPY --install-types --non-interactive"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd" eval "$cmd"
cmd="$MYPY *.py" cmd="$MYPY --strict *.py"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd" eval "$cmd"

@ -21,23 +21,26 @@ import os
import re import re
import shutil import shutil
import sys import sys
import pathlib
from typing import BinaryIO, Any
# Ref. https://stackoverflow.com/a/16571630 # Ref. https://stackoverflow.com/a/16571630
class Capturing(list): class Capturing(list[Any]):
'''Capture stdout and stderr '''Capture stdout and stderr
Usage: Usage:
with Capturing() as output: with Capturing() as output:
print("Output") print("Output")
''' '''
def __enter__(self): # TODO remove type ignore when fixed, see https://github.com/python/mypy/issues/11871, https://stackoverflow.com/questions/72174409/type-hinting-the-return-value-of-a-class-method-that-returns-self
def __enter__(self): # type: ignore
self._stdout = sys.stdout self._stdout = sys.stdout
sys.stdout = self._stringio_std = io.StringIO() sys.stdout = self._stringio_std = io.StringIO()
self._stderr = sys.stderr self._stderr = sys.stderr
sys.stderr = self._stringio_err = io.StringIO() sys.stderr = self._stringio_err = io.StringIO()
return self return self
def __exit__(self, *args): def __exit__(self, *args: Any) -> None:
self.extend(self._stringio_std.getvalue().splitlines()) self.extend(self._stringio_std.getvalue().splitlines())
del self._stringio_std # free up some memory del self._stringio_std # free up some memory
sys.stdout = self._stdout sys.stdout = self._stdout
@ -47,71 +50,71 @@ with Capturing() as output:
sys.stderr = self._stderr sys.stderr = self._stderr
def file_exits(file): def file_exits(file: str | pathlib.Path) -> bool:
return os.path.isfile(file) return os.path.isfile(file)
def remove_file(file): def remove_file(file: str | pathlib.Path) -> None:
if file_exits(file): os.remove(file) if file_exits(file): os.remove(file)
def remove_files(glob_pattern): def remove_files(glob_pattern: str) -> None:
for f in glob.glob(glob_pattern): for f in glob.glob(glob_pattern):
os.remove(f) os.remove(f)
def remove_dir_with_files(dir): def remove_dir_with_files(dir: str | pathlib.Path) -> None:
if os.path.exists(dir): shutil.rmtree(dir) if os.path.exists(dir): shutil.rmtree(dir)
def read_csv(filename): def read_csv(filename: str) -> list[list[str]]:
"""Returns a list of lines.""" """Returns a list of lines."""
with open(filename, "r", encoding="utf-8", newline='') as infile: with open(filename, "r", encoding="utf-8", newline='') as infile:
lines = [] lines: list[list[str]] = []
reader = csv.reader(infile) reader = csv.reader(infile)
for line in reader: for line in reader:
lines.append(line) lines.append(line)
return lines return lines
def read_csv_str(str): def read_csv_str(data_str: str) -> list[list[str]]:
"""Returns a list of lines.""" """Returns a list of lines."""
lines = [] lines: list[list[str]] = []
reader = csv.reader(str.splitlines()) reader = csv.reader(data_str.splitlines())
for line in reader: for line in reader:
lines.append(line) lines.append(line)
return lines return lines
def read_json(filename): def read_json(filename: str) -> Any:
"""Returns a list or a dictionary.""" """Returns a list or a dictionary."""
with open(filename, "r", encoding="utf-8") as infile: with open(filename, "r", encoding="utf-8") as infile:
return json.load(infile) return json.load(infile)
def read_json_str(str): def read_json_str(data_str: str) -> Any:
"""Returns a list or a dictionary.""" """Returns a list or a dictionary."""
return json.loads(str) return json.loads(data_str)
def read_file_to_list(filename): def read_file_to_list(filename: str) -> list[str]:
"""Returns a list of lines.""" """Returns a list of lines."""
with open(filename, "r", encoding="utf-8") as infile: with open(filename, "r", encoding="utf-8") as infile:
return infile.readlines() return infile.readlines()
def read_file_to_str(filename): def read_file_to_str(filename: str) -> str:
"""Returns a str.""" """Returns a str."""
return "".join(read_file_to_list(filename)) return "".join(read_file_to_list(filename))
def read_binary_file_as_stream(filename): def read_binary_file_as_stream(filename: str) -> BinaryIO:
"""Returns binary file content.""" """Returns binary file content."""
with open(filename, "rb",) as infile: with open(filename, "rb",) as infile:
return io.BytesIO(infile.read()) return io.BytesIO(infile.read())
def replace_escaped_octal_utf8_bytes_with_str(str): def replace_escaped_octal_utf8_bytes_with_str(str: str) -> str:
encoded_name_strings = re.findall(r'name: .*$', str, flags=re.MULTILINE) encoded_name_strings = re.findall(r'name: .*$', str, flags=re.MULTILINE)
for encoded_name_string in encoded_name_strings: for encoded_name_string in encoded_name_strings:
escaped_bytes = re.findall(r'((?:\\[0-9]+)+)', encoded_name_string) escaped_bytes = re.findall(r'((?:\\[0-9]+)+)', encoded_name_string)
@ -122,5 +125,5 @@ def replace_escaped_octal_utf8_bytes_with_str(str):
return str return str
def quick_and_dirty_workaround_encoding_problem(str): def quick_and_dirty_workaround_encoding_problem(str: str) -> str:
return re.sub(r'name: "encoding: .*$', '', str, flags=re.MULTILINE) return re.sub(r'name: "encoding: .*$', '', str, flags=re.MULTILINE)

Loading…
Cancel
Save