begin adding types to the Python scripts

This commit is contained in:
Dmytro Meleshko 2021-04-18 18:06:47 +03:00
parent fa2406e572
commit 5585f9c693
7 changed files with 76 additions and 48 deletions

View file

@ -2,18 +2,21 @@ import sys
import os import os
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from typing import Iterable, NoReturn
if os.name == "posix": if os.name == "posix":
DOTFILES_CONFIG_DIR = Path.home() / ".config" / "dotfiles" DOTFILES_CONFIG_DIR: Path = Path.home() / ".config" / "dotfiles"
DOTFILES_CACHE_DIR = Path.home() / ".cache" / "dotfiles" DOTFILES_CACHE_DIR: Path = Path.home() / ".cache" / "dotfiles"
def platform_not_supported_error(): def platform_not_supported_error() -> NoReturn:
raise Exception("platform '{}' is not supported!".format(sys.platform)) raise Exception("platform '{}' is not supported!".format(sys.platform))
def run_chooser(choices, prompt=None, async_read=False): def run_chooser(
choices: Iterable[str], prompt: str = None, async_read: bool = False
) -> int:
supports_result_index = True supports_result_index = True
if os.isatty(sys.stderr.fileno()): if os.isatty(sys.stderr.fileno()):
process_args = [ process_args = [
@ -48,7 +51,7 @@ def run_chooser(choices, prompt=None, async_read=False):
pipe.write(choice.encode()) pipe.write(choice.encode())
pipe.write(b"\n") pipe.write(b"\n")
exit_code = chooser_process.wait() exit_code: int = chooser_process.wait()
if exit_code != 0: if exit_code != 0:
raise Exception("chooser process failed with exit code {}".format(exit_code)) raise Exception("chooser process failed with exit code {}".format(exit_code))
@ -56,7 +59,7 @@ def run_chooser(choices, prompt=None, async_read=False):
return chosen_index return chosen_index
def send_notification(title, message, url=None): def send_notification(title: str, message: str, url: str = None) -> None:
if sys.platform == "darwin": if sys.platform == "darwin":
process_args = [ process_args = [
"terminal-notifier", "terminal-notifier",
@ -82,7 +85,7 @@ def send_notification(title, message, url=None):
subprocess.run(process_args, check=True) subprocess.run(process_args, check=True)
def set_clipboard(text): def set_clipboard(text: str) -> None:
# TODO: somehow merge program selection with the logic in `zsh/functions.zsh` # TODO: somehow merge program selection with the logic in `zsh/functions.zsh`
if sys.platform == "darwin": if sys.platform == "darwin":
process_args = ["pbcopy"] process_args = ["pbcopy"]

View file

@ -5,21 +5,22 @@
# <https://www.devdungeon.com/content/working-binary-data-python> # <https://www.devdungeon.com/content/working-binary-data-python>
import struct import struct
from typing import Any, IO
def read_bool(buf): def read_bool(buf: IO[bytes]) -> bool:
return buf.read(1)[0] == 1 return buf.read(1)[0] == 1
def read_number(buf): def read_number(buf: IO[bytes]) -> float:
return struct.unpack("<d", buf.read(8))[0] return struct.unpack("<d", buf.read(8))[0]
def _read_length(buf): def _read_length(buf: IO[bytes]) -> int:
return struct.unpack("<I", buf.read(4))[0] return struct.unpack("<I", buf.read(4))[0]
def read_string(buf): def read_string(buf: IO[bytes]) -> str:
is_empty = read_bool(buf) is_empty = read_bool(buf)
if is_empty: if is_empty:
return "" return ""
@ -29,25 +30,25 @@ def read_string(buf):
return buf.read(len_).decode("utf8") return buf.read(len_).decode("utf8")
def read_dictionary(buf): def read_dictionary(buf: IO[bytes]) -> dict[str, Any]:
len_ = _read_length(buf) len_ = _read_length(buf)
value = {} value: dict[str, Any] = {}
for _ in range(len_): for _ in range(len_):
key = read_string(buf) key = read_string(buf)
value[key] = read(buf) value[key] = read(buf)
return value return value
def read_list(buf): def read_list(buf: IO[bytes]) -> list[Any]:
len_ = _read_length(buf) len_ = _read_length(buf)
value = [] value: list[Any] = []
for _ in range(len_): for _ in range(len_):
read_string(buf) read_string(buf)
value.append(read(buf)) value.append(read(buf))
return value return value
def read(buf): def read(buf: IO[bytes]) -> Any:
type_, _any_type_flag = buf.read(2) type_, _any_type_flag = buf.read(2)
if type_ == 0: if type_ == 0:
return None return None

View file

@ -24,10 +24,11 @@ def humanize_bytes(bytes):
units = ["B", "kB", "MB", "GB"] units = ["B", "kB", "MB", "GB"]
factor = 1 factor = 1
for _unit in units: unit = ""
for unit in units:
next_factor = factor << 10 next_factor = factor << 10
if bytes < next_factor: if bytes < next_factor:
break break
factor = next_factor factor = next_factor
return "%.2f %s" % (float(bytes) / factor, _unit) return "%.2f %s" % (float(bytes) / factor, unit)

View file

@ -2,8 +2,10 @@
import sys import sys
import os import os
from pathlib import Path
from configparser import ConfigParser from configparser import ConfigParser
import json import json
from typing import Any, Generator, Optional, Union
import urllib.parse import urllib.parse
import urllib.request import urllib.request
@ -15,23 +17,25 @@ DEFAULT_REGISTRY_DUMP_URL = "https://stronghold.crosscode.ru/~ccbot/emote-regist
if os.name == "posix": if os.name == "posix":
config_path = ( config_path: Path = (
common_script_utils.DOTFILES_CONFIG_DIR / "copy-crosscode-emoji-url.ini" common_script_utils.DOTFILES_CONFIG_DIR / "copy-crosscode-emoji-url.ini"
) )
default_registry_dump_file = common_script_utils.DOTFILES_CACHE_DIR / "dotfiles" default_registry_dump_file: Path = (
common_script_utils.DOTFILES_CACHE_DIR / "dotfiles"
)
else: else:
common_script_utils.platform_not_supported_error() common_script_utils.platform_not_supported_error()
config = ConfigParser(interpolation=None) config = ConfigParser(interpolation=None)
config.read(config_path) config.read(config_path)
emotes = [] emotes: list[dict[str, Any]] = []
def emote_downloader_and_iterator(): def emote_downloader_and_iterator() -> Generator[str, None, None]:
global emotes global emotes
registry_dump_file = config.get( registry_dump_file: Optional[Union[str, Path]] = config.get(
"default", "ccbot_emote_registry_dump_file", fallback=None "default", "ccbot_emote_registry_dump_file", fallback=None
) )
if registry_dump_file is not None: if registry_dump_file is not None:
@ -43,6 +47,7 @@ def emote_downloader_and_iterator():
"default", "ccbot_emote_registry_dump_url", fallback=DEFAULT_REGISTRY_DUMP_URL "default", "ccbot_emote_registry_dump_url", fallback=DEFAULT_REGISTRY_DUMP_URL
) )
emote_registry_data: dict[str, Any]
try: try:
with open(registry_dump_file, "r") as f: with open(registry_dump_file, "r") as f:
emote_registry_data = json.load(f) emote_registry_data = json.load(f)
@ -64,8 +69,8 @@ chosen_index = common_script_utils.run_chooser(
if chosen_index >= 0: if chosen_index >= 0:
chosen_emote = emotes[chosen_index] chosen_emote = emotes[chosen_index]
emote_url = urllib.parse.urlparse(chosen_emote["url"]) emote_url: urllib.parse.ParseResult = urllib.parse.urlparse(chosen_emote["url"])
emote_url_query = urllib.parse.parse_qs(emote_url.query) emote_url_query: dict[str, list[str]] = urllib.parse.parse_qs(emote_url.query)
if config.getboolean("default", "add_emote_name_to_url", fallback=False): if config.getboolean("default", "add_emote_name_to_url", fallback=False):
emote_url_query["name"] = [chosen_emote["name"]] emote_url_query["name"] = [chosen_emote["name"]]
@ -76,10 +81,12 @@ if chosen_index >= 0:
if default_emote_image_size is not None: if default_emote_image_size is not None:
emote_url_query["size"] = [str(default_emote_image_size)] emote_url_query["size"] = [str(default_emote_image_size)]
emote_url_query = urllib.parse.urlencode(emote_url_query, doseq=True) emote_url_query_str = urllib.parse.urlencode(emote_url_query, doseq=True)
emote_url = urllib.parse.urlunparse(emote_url._replace(query=emote_url_query)) emote_url_str = urllib.parse.urlunparse(
emote_url._replace(query=emote_url_query_str)
)
common_script_utils.set_clipboard(emote_url) common_script_utils.set_clipboard(emote_url_str)
common_script_utils.send_notification( common_script_utils.send_notification(
os.path.basename(__file__), os.path.basename(__file__),

View file

@ -16,6 +16,10 @@ import factorio.property_tree
with open(Path.home() / ".factorio" / "mods" / "mod-settings.dat", "rb") as f: with open(Path.home() / ".factorio" / "mods" / "mod-settings.dat", "rb") as f:
version_main: int
version_major: int
version_minor: int
version_developer: int
version_main, version_major, version_minor, version_developer = struct.unpack( version_main, version_major, version_minor, version_developer = struct.unpack(
"<HHHH", f.read(8) "<HHHH", f.read(8)
) )

View file

@ -3,9 +3,9 @@
# helper script for query-bookmarks.sh # helper script for query-bookmarks.sh
# currently supports only Firefox # currently supports only Firefox
# useful links: # useful links:
# http://kb.mozillazine.org/Profiles.ini_file # <http://kb.mozillazine.org/Profiles.ini_file>
# https://stackoverflow.com/a/740183/12005228 # <https://stackoverflow.com/a/740183/12005228>
# https://wiki.mozilla.org/Places:BookmarksComments # <https://wiki.mozilla.org/Places:BookmarksComments>
import sys import sys
import os import os
@ -14,16 +14,16 @@ from configparser import ConfigParser
import tempfile import tempfile
import shutil import shutil
import sqlite3 import sqlite3
import collections from typing import Optional, Tuple, Generator
sys.path.insert(1, os.path.join(os.path.dirname(__file__), "..", "script-resources")) sys.path.insert(1, os.path.join(os.path.dirname(__file__), "..", "script-resources"))
import common_script_utils import common_script_utils
if sys.platform == "darwin": if sys.platform == "darwin":
firefox_home = Path.home() / "Library" / "Application Support" / "Firefox" firefox_home: Path = Path.home() / "Library" / "Application Support" / "Firefox"
elif os.name == "posix": elif os.name == "posix":
firefox_home = Path.home() / ".mozilla" / "firefox" firefox_home: Path = Path.home() / ".mozilla" / "firefox"
else: else:
common_script_utils.platform_not_supported_error() common_script_utils.platform_not_supported_error()
@ -31,15 +31,17 @@ else:
profiles_config = ConfigParser(interpolation=None) profiles_config = ConfigParser(interpolation=None)
profiles_config.read(firefox_home / "profiles.ini") profiles_config.read(firefox_home / "profiles.ini")
installs_sections = [s for s in profiles_config.sections() if s.startswith("Install")] installs_sections: list[str] = [
s for s in profiles_config.sections() if s.startswith("Install")
]
if not installs_sections: if not installs_sections:
raise Exception("no Firefox installations detected!") raise Exception("no Firefox installations detected!")
if len(installs_sections) > 1: if len(installs_sections) > 1:
raise Exception("multiple Firefox installations are not supported!") raise Exception("multiple Firefox installations are not supported!")
profile_dir = firefox_home / profiles_config.get(installs_sections[0], "Default") profile_dir: Path = firefox_home / profiles_config.get(installs_sections[0], "Default")
# should places.sqlite be used instead? # should places.sqlite be used instead?
db_path = profile_dir / "weave" / "bookmarks.sqlite" db_path: Path = profile_dir / "weave" / "bookmarks.sqlite"
if not db_path.is_file(): if not db_path.is_file():
raise Exception("'{}' is not a file".format(db_path)) raise Exception("'{}' is not a file".format(db_path))
@ -50,17 +52,22 @@ if not db_path.is_file():
db_copy_fd, db_copy_path = tempfile.mkstemp(prefix="bookmarks.", suffix=".sqlite") db_copy_fd, db_copy_path = tempfile.mkstemp(prefix="bookmarks.", suffix=".sqlite")
os.close(db_copy_fd) os.close(db_copy_fd)
chooser_entries = [] chooser_entries: list[Tuple[str, str, Optional[str]]] = []
try: try:
shutil.copyfile(db_path, db_copy_path) shutil.copyfile(db_path, db_copy_path)
db = sqlite3.connect(db_copy_path) db = sqlite3.connect(db_copy_path)
urls = {} urls: dict[int, str] = {}
url_id: int
url: str
for url_id, url in db.execute("SELECT id, url FROM urls"): for url_id, url in db.execute("SELECT id, url FROM urls"):
urls[url_id] = url urls[url_id] = url
folders = {} folders: dict[str, Tuple[Optional[str], str]] = {}
folder_id: str
parent_folder_id: str
folder_title: str
for folder_id, parent_folder_id, folder_title in db.execute( for folder_id, parent_folder_id, folder_title in db.execute(
"SELECT guid, parentGuid, title FROM items WHERE kind = 3 AND validity AND NOT isDeleted" "SELECT guid, parentGuid, title FROM items WHERE kind = 3 AND validity AND NOT isDeleted"
): ):
@ -69,24 +76,29 @@ try:
folder_title, folder_title,
) )
url_title: str
url_id: int
url_keyword: str
parent_folder_id: str
for url_title, url_id, url_keyword, parent_folder_id in db.execute( for url_title, url_id, url_keyword, parent_folder_id in db.execute(
"SELECT title, urlId, keyword, parentGuid FROM items WHERE kind = 1 AND validity AND NOT isDeleted" "SELECT title, urlId, keyword, parentGuid FROM items WHERE kind = 1 AND validity AND NOT isDeleted"
): ):
url = urls[url_id] url = urls[url_id]
folder_path = collections.deque() folder_path = list[str]()
while parent_folder_id is not None: parent_folder_id_2: Optional[str] = parent_folder_id
folder = folders.get(parent_folder_id, None) while parent_folder_id_2 is not None:
folder = folders.get(parent_folder_id_2, None)
if folder is None: if folder is None:
# broken folder structure? # broken folder structure?
folder_path.clear() folder_path.clear()
break break
parent_folder_id, folder_title = folder parent_folder_id_2, folder_title = folder
if folder_title is not None: if folder_title is not None:
folder_path.appendleft(folder_title) folder_path.append(folder_title)
folder_path_str = ( folder_path_str = (
("/" + "/".join(folder_path)) if len(folder_path) > 0 else None ("/" + "/".join(reversed(folder_path))) if len(folder_path) > 0 else None
) )
chooser_entries.append((url_title, url, folder_path_str)) chooser_entries.append((url_title, url, folder_path_str))
@ -97,7 +109,7 @@ finally:
os.remove(db_copy_path) os.remove(db_copy_path)
def chooser_entries_iter(): def chooser_entries_iter() -> Generator[str, None, None]:
for title, url, folder_path_str in chooser_entries: for title, url, folder_path_str in chooser_entries:
entry_items = [title, url] entry_items = [title, url]
if folder_path_str is not None: if folder_path_str is not None:

View file

@ -3,7 +3,7 @@
import random import random
def randbyte(): def randbyte() -> int:
return random.randrange(0, 256) return random.randrange(0, 256)