Skip to content

Instantly share code, notes, and snippets.

@fooker
Last active May 9, 2025 09:12
Show Gist options
  • Save fooker/db6948cb3c3551e5516dd6c875f6de05 to your computer and use it in GitHub Desktop.
Save fooker/db6948cb3c3551e5516dd6c875f6de05 to your computer and use it in GitHub Desktop.
A genuine interface to the telefonliste
#! /usr/bin/env nix-shell
#! nix-shell -i python3
#! nix-shell -p python3Packages.requests
#! nix-shell -p python3Packages.pandas
#! nix-shell -p python3Packages.openpyxl
#! nix-shell -p python3Packages.click
#! nix-shell -p python3Packages.unidecode
#! nix-shell -p python3Packages.thefuzz
import os
import json
import requests
import pandas as pd
import click
import unidecode
from thefuzz import fuzz
from email.utils import formatdate
URL = "https://intranet.hs-fulda.de/fileadmin/RZ/Dokumente/Telefonliste.xlsx"
XDG_CACHE = os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache"))
CACHE_DIR = os.path.join(XDG_CACHE, "phonebook-cli")
XLSX_FILE = os.path.join(CACHE_DIR, "phonebook.xlsx")
PARSED_FILE = os.path.join(CACHE_DIR, "phonebook.json")
os.makedirs(CACHE_DIR, exist_ok=True)
def download(force=False):
headers = {}
if not force:
try:
mtime = os.path.getmtime(XLSX_FILE)
except FileNotFoundError:
pass
else:
headers["If-Modified-Since"] = formatdate(mtime, usegmt=True)
response = requests.get(URL, headers=headers, stream=True)
if response.status_code == 304:
return force # Not modified – if forced, we update anyways
response.raise_for_status()
with open(XLSX_FILE, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
return True
def parse_and_cache():
records = pd.read_excel(XLSX_FILE, engine="openpyxl")
records = records.to_dict(orient="records")
with open(PARSED_FILE, "w") as f:
json.dump(records, f)
return records
def load_cached():
try:
with open(PARSED_FILE, "r") as f:
return json.load(f)
except FileNotFoundError:
return parse_and_cache()
def load(force):
if download(force):
return parse_and_cache()
else:
return load_cached()
def normalize(text):
return unidecode.unidecode(str(text).lower())
def search_name(name_query, force_update=False):
name_query_norm = normalize(name_query)
records = load(force_update)
matches = []
for rec in records:
full_name = f"{rec.get("Vorname", "")} {rec.get("Name", "")}"
full_name = normalize(full_name)
score = fuzz.partial_ratio(name_query_norm, full_name)
if score >= 80:
matches.append(rec)
return matches
@click.command()
@click.argument("name")
@click.option("--update/--no-update",
default=False,
help="Force update before searching")
def main(name, update):
"""Search for a NAME in the phonebook"""
results = search_name(name, force_update=update)
if results:
for rec in results:
gn = rec.get("Vorname", "")
sn = rec.get("Name", "")
tel = rec.get("Telefon")
if tel:
tel = float(tel)
if tel.is_integer():
tel = str(int(tel))
else:
tel = ""
click.echo(f"{gn} {sn} - {tel}")
else:
click.echo("No results found.")
if __name__ == "__main__":
main()
@fooker
Copy link
Author

fooker commented Apr 9, 2025

As a derivation:

let
  script = pkgs.fetchurl {
    url = "https://gist.githubusercontent.com/fooker/db6948cb3c3551e5516dd6c875f6de05/raw/6dd568aa3494712c94f6f7a2976fc1248541fc8e/telefonliste.py";
  };
in
pkgs.writers.writePython3Bin "telefonliste" {
  flakeIgnore = [ "E265" ];
  libraries = with pkgs.python3Packages; [
    requests
    pandas
    openpyxl
    click
    unidecode
    thefuzz
  ];
} script;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment