It all started with @click

and, well. It went on from there.

I'm going to post some code now.

from collections.abc import Generator
from contextlib import contextmanager, suppress
from dataclasses import dataclass

import click
import requests
from beartype.typing import Any, Sequence, TypedDict, Unpack

from src.auth import fetch_auth_token
from src.settings import SETTINGS


class CliOptions(TypedDict):
    auth_type: str
    chat_type: str
    topic: str | None
    participant: str | None


@dataclass(eq=False)
class Options:
    auth_type: str
    chat_type: str
    topic: str | None
    participant: str | None


@contextmanager
def find_in_instance(
    cli_options, data
) -> Generator[list[dict[str, None | str | dict[str, str]]], Any, None]:
    match cli_options:
        case Options(participant=None, topic=None, chat_type=("oneOnOne" | "group" | "meeting")):
            yield [item for item in data if cli_options.chat_type == item["chatType"]]
        case Options(participant=None, chat_type=("oneOnOne" | "group" | "meeting")):
            yield [
                item
                for item in data
                if cli_options.chat_type == item["chatType"] and cli_options.topic in item["topic".lower()]
            ]
        case Options(topic=None, chat_type=("oneOnOne" | "group" | "meeting")):
            yield [
                item
                for item in data
                if cli_options.chat_type == item["chatType"]
                for member in item["members"]
                if cli_options.participant in member["displayName"].lower()
            ]
        case Options(chat_type=("oneOnOne" | "group" | "meeting")):
            yield [
                item
                for item in data
                if cli_options.chat_type == item["chatType"] and cli_options.topic in item["topic"]
                for member in item["members"]
                if cli_options.participant in member["displayName"]
            ]
        case __:
            yield [{"default": None}]


def lowercase(ctx, param, value):
    if value is not None:
        return value.lower()


@click.command("cli")
@click.option(
    "--auth-type",
    type=click.Choice(
        ["interactive", "code"],
        case_sensitive=False,
    ),
    default="interactive",
    help="Default is 'interactive'. If running headless use 'code'",
)
@click.option(
    "--chat-type",
    type=click.Choice(["oneOnOne", "group", "meeting"], case_sensitive=False),
    default="oneOnOne",
    help="Chat is either oneOnOne [default],  group or meeting.",
)
@click.option(
    "--topic",
    callback=lowercase,
    help="Filter on chat name or topic when it contains the <string>",
)
@click.option(
    "--participant",
    callback=lowercase,
    help="Filter on chat participants name when it contains the <string>",
)
def cli(**kwargs: Unpack[CliOptions]):
    def recursive_fetch(
        url: str,
        headers: dict[str, str],
        cli_options: Options,
        index: int = 0,
    ) -> int:
        ...do some processing
    ..do auth and some pre prossesing
    cli_options = Options(**kwargs)
    recursive_fetch(url, headers, cli_options)

So, what are we looking at here? We are looking at a client that fetches chats from https://graph.microsoft.com/v1.0/me/chats?$top=50&$expand=members, recursively.

It started when ruff complained I had too many parameters in my method call. So, naturally, I turned to **kwargs I usually never turn to **kwargs but with TypedDict I feel I can have a clear and concise understanding of what hides inside the dict. So that's one thing. I haven't seen this particular pattern used with click in the wild yet. I might be the only one with this set of needs... or I'm just very poor at designing command-line applications. That might be it.

Anyways. One thing led to another. This is the first time I've used the match statement with any success. I've tried it before but never had a use case that actually resulted in fewer lines and clearer code. Now I think I have.

I will post the auth and the Settings too - when I get around to doing it. For the time being, this post is somewhat of a placeholder so that I don't forget..

0
Subscribe to my newsletter

Read articles from Rune Hansén Steinnes-Westum directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rune Hansén Steinnes-Westum
Rune Hansén Steinnes-Westum