Skip to main content

common-python-utils

Common Python utilities shared across Kaiano's projects.

Install name: miniapppolis-common-utils (PyPI)  ·  Import namespace: mini_app_polis

The distribution and the import namespace differ on purpose: the published name is prefixed because the registry is public, while the import namespace is the one every consumer already writes. Install miniapppolis-common-utils, import mini_app_polis (for example, from mini_app_polis.google import GoogleAPI).


Installation

Declare a version range in your pyproject.toml and let the lockfile carry the exact pin:

[project]
dependencies = [
  "miniapppolis-common-utils>=5.0,<6",
]

Then uv lock. To move to a newer release, uv lock --upgrade-package miniapppolis-common-utils — no edit to this file.

To use the LLM module, add the llm extra:

dependencies = [
  "miniapppolis-common-utils[llm]>=5.0,<6",
]

Modules

Module Import Description
api/ from mini_app_polis.api import KaianoApiClient HTTP client for internal FastAPI services
asana/ from mini_app_polis.asana import AsanaClient Asana task creation with external-id idempotency
config.py from mini_app_polis import config Env-var driven shared config (Spotify, Google, VDJ)
google/ from mini_app_polis.google import GoogleAPI Drive + Sheets facade
llm/ from mini_app_polis.llm import build_llm, LLMMessage OpenAI + Anthropic clients (optional extra)
mp3/ from mini_app_polis.mp3 import ... AcoustID identification, tagging, renaming
music/ from mini_app_polis.music import normalize_for_matching Music data normalization utilities
spotify/ from mini_app_polis.spotify import SpotifyAPI Spotipy wrapper
vdj/ from mini_app_polis.vdj.m3u import ParseFacade VirtualDJ M3U parsing

Usage

KaianoApiClient

from mini_app_polis.api import KaianoApiClient

# Set KAIANO_API_BASE_URL and this machine's own key
# (deejay-cog -> DEEJAY_COG_API_KEY) in the environment.
# The machine name is what selects the key variable, and the key is
# what names this caller to the API. There is no fallback: without it,
# every call returns 401.
client = KaianoApiClient.from_env("deejay-cog")
result = client.post("/sets", {"name": "My Set"})

AsanaClient

from datetime import date

from mini_app_polis.asana import AsanaClient, AsanaTaskInput, link, rich_text_body

# Set ASANA_ACCESS_TOKEN (personal access token) and, if you resolve tag
# names, ASANA_WORKSPACE_ID in the environment.
client = AsanaClient.from_env()

external_id = "voicenote.1AbCdEf"

# Idempotency is a single lookup, not a list-and-scan: Asana stores an
# app-scoped `external` object on each task and lets you address the task
# by it. Completed tasks are found too, so a triaged item is never
# recreated.
if client.find_task_by_external_id(external_id) is None:
    client.create_task(
        AsanaTaskInput(
            name="Send the report",
            html_notes=rich_text_body(
                "Because it is due Friday.",
                link("https://example.com/source", "Source"),
            ),
            project_gid="1218223550488548",
            section_gid="1218337761864701",
            assignee="me",
            due_on=date.today(),
            tag_gids=(client.find_or_create_tag("review"),),
            external_id=external_id,
        )
    )

Task bodies are Asana rich text, not markdown, and Asana returns 400 on malformed markup. Compose them with rich_text_body / link / escape_rich_text rather than by hand — transcripts and model output contain < and & often enough that escaping is a correctness concern.

LLM (requires llm extra)

from mini_app_polis.llm import build_llm, LLMMessage

llm = build_llm(provider="anthropic", model="claude-3-5-sonnet-20241022")
result = llm.generate_json(
    messages=[
        LLMMessage(role="system", content="Return JSON only."),
        LLMMessage(role="user", content="Extract the artist and title."),
    ],
    json_schema={
        "type": "object",
        "properties": {
            "artist": {"type": "string"},
            "title": {"type": "string"},
        },
        "required": ["artist", "title"],
    },
)
print(result.output_json)  # {"artist": "...", "title": "..."}

Logger

from mini_app_polis import logger

logger.info("Starting pipeline")
logger.error("Something went wrong: %s", err)

# Or get the logger instance directly
log = logger.get_logger()

Set LOGGING_LEVEL=INFO (or DEBUG/WARNING/ERROR) in your environment.


Development

Prerequisites

  • uv — Python package manager
curl -LsSf https://astral.sh/uv/install.sh | sh

First-time setup

Clone the repo and run these commands once in order:

# 1. Install all dependencies including dev and optional extras
uv sync --all-extras

# 2. Install pre-commit hooks into git
uv run pre-commit install

That's it. Pre-commit will now run automatically on every git commit.

Daily workflow

# Run tests
uv run pytest

# Run tests with coverage detail
uv run pytest --cov=mini_app_polis --cov-report=term-missing

# Lint (auto-fix where possible)
uv run ruff check src/ tests/ --fix

# Format
uv run ruff format src/ tests/

# Type check
uv run mypy src/

# Run all pre-commit hooks manually against all files
uv run pre-commit run --all-files

What pre-commit does

On every git commit, the following run automatically:

  • ruff — lints and auto-fixes what it can
  • ruff-format — formats code
  • python-check-mock-methods — catches incorrect mock usage
  • python-use-type-annotations — flags old-style type comments

If any hook fails, the commit is blocked. Ruff will auto-fix in place — just git add . and re-commit.

Environment variables

No .env file is required to run tests. For local development against real services, copy .env.example to .env and fill in values:

cp .env.example .env

Key variables:

Variable Used by Required for
KAIANO_API_BASE_URL KaianoApiClient Calling internal FastAPI services
<MACHINE_NAME>_API_KEY KaianoApiClient This machine's own named key, e.g. DEEJAY_COG_API_KEY. Derived from the machine name by machine_key_env_var()
KAIANO_API_KEY KaianoApiClient Unnamed fallback for a caller that declares no machine name. Authenticates, but its writes are unattributable
ASANA_ACCESS_TOKEN AsanaClient Asana personal access token. Read per request, so rotation needs no restart
ASANA_WORKSPACE_ID AsanaClient Workspace gid. Required only by find_or_create_tag(), since tags are workspace-scoped objects
LOGGING_LEVEL logger Log verbosity (INFO default). Importing logger calls basicConfig, so this reaches every library in the process. Loggers known to print credentials (websockets, httpx, httpcore, …) are held at INFO regardless — raising this to DEBUG will not print your secrets
GOOGLE_CREDENTIALS_JSON GoogleAPI Google Drive + Sheets access
SPOTIPY_CLIENT_ID SpotifyAPI Spotify operations
SPOTIPY_CLIENT_SECRET SpotifyAPI Spotify operations
SPOTIPY_REFRESH_TOKEN SpotifyAPI Spotify operations
ANTHROPIC_API_KEY llm extra Anthropic LLM calls
OPENAI_API_KEY llm extra OpenAI LLM calls

Releasing

Releases are automated via semantic-release on push to main.

Commit format Bump Example result
fix: ... patch v1.0.0 -> v1.0.1
feat: ... minor v1.0.0 -> v1.1.0
feat!: ... major v1.0.0 -> v2.0.0

Important: Semantic-release only recognizes Conventional Commits format. These will NOT trigger a release:

  • breaking change: ... - unrecognized type
  • feat: breaking change ... - the word "breaking" in the message does not count
  • Free-form messages with no type prefix

For major bumps, prefer the BREAKING CHANGE footer in the commit body as it is more reliably parsed than feat!:

feat: your message here

BREAKING CHANGE: description of what changed and why it breaks

The footer format (BREAKING CHANGE: in the body) is the more battle-tested path across different versions of semantic-release. feat! should work per spec but has been known to behave inconsistently depending on plugin versions. Document both and lean on the footer.


Import Namespace

Use mini_app_polis as the import namespace across all modules (for example, from mini_app_polis.api import KaianoApiClient).

Release files for miniapppolis-common-utils 5.10.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for miniapppolis-common-utils 5.10.1
File Size Uploaded
miniapppolis_common_utils-5.10.1.tar.gz 272.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for miniapppolis-common-utils 5.10.1
File Interpreter ABI Platform
miniapppolis_common_utils-5.10.1-py3-none-any.whl Python 3 none any Details

Total release size: 377.4 kB

Release files / miniapppolis_common_utils-5.10.1.tar.gz

Download URL miniapppolis_common_utils-5.10.1.tar.gz
Size 272.0 kB
Tags Source
SHA-256 checksum
How to use checksums
5a8b17f2c77b9650007c5c386b6e281ba6ee40d9168ccc2a24902adda10a985a
BLAKE2b-256 checksum
How to use checksums
34f6b6cd09ffcfe485fccff98481b5281ae1306b98386532489c0489fcbeef39
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / miniapppolis_common_utils-5.10.1-py3-none-any.whl

Download URL miniapppolis_common_utils-5.10.1-py3-none-any.whl
Size 105.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
739abc723989945a15ca4bfb8651c64fbca4725e1ad03db5c10f6e4f17dbff78
BLAKE2b-256 checksum
How to use checksums
8a4976a81af40f026cb3629f5f71a3b973c337b4bca07a00a10ee40e213f131f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

5.13.1

2 release files

5.13.0

2 release files

5.12.1

2 release files

5.12.0

2 release files

5.11.0

2 release files

5.10.2

2 release files

This release

5.10.1 This release

2 release files

5.10.0

2 release files

5.9.1

2 release files

5.9.0

2 release files

5.8.0

2 release files

5.7.1

2 release files

5.7.0

2 release files

5.6.0

2 release files

5.5.1

2 release files

5.5.0

2 release files

5.4.0

2 release files

5.3.2

2 release files

5.3.1

2 release files

5.3.0

2 release files

5.2.0

2 release files

5.1.3

2 release files

5.1.2

2 release files

5.1.1

2 release files

5.1.0

2 release files

5.0.2

2 release files

5.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page