{"id":47891076,"url":"https://github.com/osodevops/keito-python","last_synced_at":"2026-04-04T03:06:06.569Z","repository":{"id":342410007,"uuid":"1173867175","full_name":"osodevops/keito-python","owner":"osodevops","description":"Official Python SDK for the Keito API — track billable time, expenses, and invoices for humans and AI agents","archived":false,"fork":false,"pushed_at":"2026-03-05T21:09:00.000Z","size":44,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-06T00:46:35.638Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://keito.ai/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/osodevops.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-05T20:47:03.000Z","updated_at":"2026-03-05T21:09:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/osodevops/keito-python","commit_stats":null,"previous_names":["osodevops/keito-python"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/osodevops/keito-python","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/osodevops%2Fkeito-python","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/osodevops%2Fkeito-python/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/osodevops%2Fkeito-python/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/osodevops%2Fkeito-python/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/osodevops","download_url":"https://codeload.github.com/osodevops/keito-python/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/osodevops%2Fkeito-python/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31385942,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T01:22:39.193Z","status":"online","status_checked_at":"2026-04-04T02:00:07.569Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-04T03:06:06.092Z","updated_at":"2026-04-04T03:06:06.556Z","avatar_url":"https://github.com/osodevops.png","language":"Python","readme":"# Keito Python SDK\n\nOfficial Python SDK for the [Keito](https://keito.ai) API — track billable time, expenses, and invoices for humans and AI agents.\n\n[![CI](https://github.com/osodevops/keito-python/actions/workflows/ci.yml/badge.svg)](https://github.com/osodevops/keito-python/actions/workflows/ci.yml)\n[![PyPI](https://img.shields.io/pypi/v/keito)](https://pypi.org/project/keito/)\n[![Python](https://img.shields.io/pypi/pyversions/keito)](https://pypi.org/project/keito/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n\n## Features\n\n- **Dual sync + async clients** — `Keito` and `AsyncKeito` backed by `httpx`\n- **Typed models** — All responses are frozen Pydantic v2 models with full IDE autocomplete\n- **Auto-pagination** — Iterate through all pages with a simple `for` loop\n- **Automatic retries** — Exponential backoff on 408, 429, 5xx with jitter\n- **Typed error hierarchy** — Catch specific errors like `KeitoNotFoundError` or `KeitoRateLimitError`\n- **Agent helpers** — `AgentMetadata.build()` and `outcomes.log()` for AI agent billing workflows\n- **Raw response access** — `.with_raw_response` on every resource for headers, status codes, and rate limit info\n- **Env var fallback** — Reads `KEITO_API_KEY` and `KEITO_ACCOUNT_ID` from environment\n- **Context manager support** — Proper resource cleanup with `with` / `async with`\n\n## Installation\n\n```bash\npip install keito\n```\n\nRequires Python 3.9+.\n\n## Quick Start\n\n```python\nfrom keito import Keito\n\n# Reads KEITO_API_KEY and KEITO_ACCOUNT_ID from env\nclient = Keito()\n\n# Or pass explicitly\nclient = Keito(api_key=\"kto_...\", account_id=\"acc_...\")\n\n# Create a time entry\nentry = client.time_entries.create(\n    project_id=\"proj_123\",\n    task_id=\"task_456\",\n    spent_date=\"2026-03-05\",\n    hours=1.5,\n    notes=\"Code review for PR #842\",\n)\n\n# List all time entries (auto-paginates)\nfor entry in client.time_entries.list(project_id=\"proj_123\"):\n    print(f\"{entry.spent_date}: {entry.hours}h — {entry.notes}\")\n```\n\n## Async Usage\n\n```python\nfrom keito import AsyncKeito\n\nasync with AsyncKeito() as client:\n    entry = await client.time_entries.create(\n        project_id=\"proj_123\",\n        task_id=\"task_456\",\n        spent_date=\"2026-03-05\",\n        hours=2.0,\n    )\n\n    async for entry in client.time_entries.list():\n        print(entry.id)\n```\n\n---\n\n## Agent Workflows\n\nThe Keito SDK is purpose-built for AI agent billing. Agents can track their own time, log outcome-based events, record LLM costs as expenses, and generate invoices — all programmatically.\n\n### Setting Up an Agent Client\n\nEvery Keito agent has its own API key and account. Set these as environment variables in your agent runtime:\n\n```bash\nexport KEITO_API_KEY=\"kto_agent_...\"\nexport KEITO_ACCOUNT_ID=\"acc_...\"\n```\n\n```python\nfrom keito import Keito\nfrom keito.types import Source\n\nclient = Keito()\n\n# Check the agent's identity\nme = client.users.me()\nprint(me.user_type)  # UserType.AGENT\nprint(me.email)      # agent@yourcompany.com\n```\n\n### Tracking Agent Time\n\nUse `source=Source.AGENT` to mark entries as agent-generated. This lets managers filter and report on agent vs. human work.\n\n```python\nfrom keito import Keito, AgentMetadata\nfrom keito.types import Source\n\nclient = Keito()\n\nentry = client.time_entries.create(\n    project_id=\"proj_123\",\n    task_id=\"task_456\",\n    spent_date=\"2026-03-05\",\n    hours=1.5,\n    notes=\"Automated code review for PR #842\",\n    source=Source.AGENT,\n    billable=True,\n    metadata=AgentMetadata.build(\n        agent_id=\"code-reviewer-v2\",\n        framework=\"langchain\",\n        model_provider=\"anthropic\",\n        model_name=\"claude-4-sonnet\",\n        tokens_in=12500,\n        tokens_out=32700,\n        cost_usd=0.18,\n        run_id=\"run_abc123\",\n        confidence=0.94,\n    ),\n)\n```\n\n### Structured Agent Metadata\n\nThe `AgentMetadata.build()` helper produces a structured dict following the Keito metadata schema:\n\n```python\nfrom keito import AgentMetadata\n\nmetadata = AgentMetadata.build(\n    agent_id=\"support-agent-v3\",\n    framework=\"crewai\",\n    model_provider=\"anthropic\",\n    model_name=\"claude-4-sonnet\",\n    tokens_in=8000,\n    tokens_out=15000,\n    cost_usd=0.12,\n    run_id=\"run_xyz\",\n    parent_run_id=\"run_parent\",\n    trigger=\"webhook\",\n    confidence=0.97,\n    human_reviewed=False,\n)\n\n# Produces:\n# {\n#     \"agent\": {\"id\": \"support-agent-v3\", \"framework\": \"crewai\"},\n#     \"run\": {\"id\": \"run_xyz\", \"parent_id\": \"run_parent\", \"trigger\": \"webhook\"},\n#     \"model\": {\n#         \"provider\": \"anthropic\",\n#         \"name\": \"claude-4-sonnet\",\n#         \"tokens_in\": 8000,\n#         \"tokens_out\": 15000,\n#         \"cost_usd\": 0.12,\n#     },\n#     \"quality\": {\"confidence\": 0.97, \"human_reviewed\": False},\n# }\n```\n\nAll fields are optional — include only what's relevant to your agent.\n\n### Outcome-Based Billing\n\nNot all agent work is measured in hours. Use outcomes to bill for discrete events like tickets resolved, leads qualified, or deployments completed.\n\n```python\nfrom keito import Keito, OutcomeTypes\n\nclient = Keito()\n\n# Log a resolved ticket as a billable outcome\noutcome = client.outcomes.log(\n    project_id=\"proj_123\",\n    task_id=\"task_456\",\n    spent_date=\"2026-03-05\",\n    outcome_type=OutcomeTypes.TICKET_RESOLVED,\n    description=\"Resolved billing inquiry #4821\",\n    unit_price=0.99,\n    quantity=1,\n    success=True,\n    evidence={\"ticket_id\": \"TKT-4821\", \"resolution_time_seconds\": 45},\n)\n\n# outcome is a TimeEntry with hours=0, source=\"agent\",\n# and metadata containing the outcome details\n```\n\nAvailable outcome types:\n\n| Outcome Type | Value |\n|---|---|\n| `OutcomeTypes.TICKET_RESOLVED` | `ticket_resolved` |\n| `OutcomeTypes.LEAD_QUALIFIED` | `lead_qualified` |\n| `OutcomeTypes.CODE_REVIEW_COMPLETED` | `code_review_completed` |\n| `OutcomeTypes.PR_MERGED` | `pr_merged` |\n| `OutcomeTypes.DEPLOYMENT_COMPLETED` | `deployment_completed` |\n| `OutcomeTypes.TEST_SUITE_PASSED` | `test_suite_passed` |\n| `OutcomeTypes.DOCUMENT_GENERATED` | `document_generated` |\n| `OutcomeTypes.DATA_PIPELINE_RUN` | `data_pipeline_run` |\n| `OutcomeTypes.ALERT_TRIAGED` | `alert_triaged` |\n| `OutcomeTypes.CUSTOMER_REPLY_SENT` | `customer_reply_sent` |\n\nYou can also pass any custom string as `outcome_type` for types not in the enum.\n\n### Logging LLM Costs as Expenses\n\nTrack API costs (OpenAI, Anthropic, etc.) as billable expenses:\n\n```python\nfrom keito.types import Source\n\nexpense = client.expenses.create(\n    project_id=\"proj_123\",\n    expense_category_id=\"cat_llm_api\",\n    spent_date=\"2026-03-05\",\n    total_cost=0.18,\n    notes=\"Claude 4 Sonnet — PR review (12.5k in, 32.7k out)\",\n    billable=True,\n    source=Source.AGENT,\n    metadata={\n        \"model\": \"claude-4-sonnet\",\n        \"tokens_in\": 12500,\n        \"tokens_out\": 32700,\n        \"provider\": \"anthropic\",\n    },\n)\n```\n\n### Generating Invoices\n\nAgents can create and send invoices for their work:\n\n```python\n# Create an invoice with line items\ninvoice = client.invoices.create(\n    client_id=\"cli_789\",\n    subject=\"March 2026 — AI Agent Services\",\n    period_start=\"2026-03-01\",\n    period_end=\"2026-03-31\",\n    line_items=[\n        {\n            \"kind\": \"Service\",\n            \"description\": \"AI Code Review (42 hours x $150/hr)\",\n            \"quantity\": 42,\n            \"unit_price\": 150.00,\n        },\n        {\n            \"kind\": \"Service\",\n            \"description\": \"312 tickets resolved x $0.99\",\n            \"quantity\": 312,\n            \"unit_price\": 0.99,\n        },\n    ],\n)\n\n# Send the invoice via email\nmessage = client.invoices.messages.create(\n    invoice.id,\n    recipients=[{\"name\": \"Client CFO\", \"email\": \"cfo@client.com\"}],\n    subject=f\"Invoice #{invoice.number}\",\n    attach_pdf=True,\n)\n```\n\n### Full Agent Loop Example\n\nA complete agent billing loop — track time, log outcomes, record costs, generate invoice:\n\n```python\nfrom keito import Keito, AgentMetadata, OutcomeTypes\nfrom keito.types import Source\n\nclient = Keito()\n\nPROJECT = \"proj_support\"\nTASK = \"task_tickets\"\nTODAY = \"2026-03-05\"\n\n# 1. Track time spent on the run\nentry = client.time_entries.create(\n    project_id=PROJECT,\n    task_id=TASK,\n    spent_date=TODAY,\n    hours=0.5,\n    notes=\"Support ticket triage batch — 15 tickets processed\",\n    source=Source.AGENT,\n    metadata=AgentMetadata.build(\n        agent_id=\"support-triage-v2\",\n        model_provider=\"anthropic\",\n        model_name=\"claude-4-sonnet\",\n        run_id=\"run_batch_042\",\n    ),\n)\n\n# 2. Log each resolved ticket as an outcome\nfor ticket_id in [\"TKT-101\", \"TKT-102\", \"TKT-103\"]:\n    client.outcomes.log(\n        project_id=PROJECT,\n        task_id=TASK,\n        spent_date=TODAY,\n        outcome_type=OutcomeTypes.TICKET_RESOLVED,\n        description=f\"Resolved {ticket_id}\",\n        unit_price=0.99,\n        success=True,\n        evidence={\"ticket_id\": ticket_id},\n    )\n\n# 3. Record LLM API cost\nclient.expenses.create(\n    project_id=PROJECT,\n    expense_category_id=\"cat_llm\",\n    spent_date=TODAY,\n    total_cost=0.42,\n    notes=\"Anthropic API cost for ticket triage batch\",\n    source=Source.AGENT,\n    billable=True,\n)\n\n# 4. Query what the agent did today\nfor e in client.time_entries.list(source=Source.AGENT, from_date=TODAY, to_date=TODAY):\n    print(f\"  {e.hours}h — {e.notes}\")\n\n# 5. Check project and task info\nfor project in client.projects.list(is_active=True):\n    print(f\"Project: {project.name} (billable={project.is_billable})\")\n```\n\n### Filtering Agent Work in Reports\n\nManagers can pull reports filtered to agent activity:\n\n```python\n# Team time report — includes both humans and agents\nfor result in client.reports.team_time(from_date=\"20260301\", to_date=\"20260331\"):\n    print(f\"{result.user_name}: {result.billable_hours}h (${result.billable_amount})\")\n\n# List only agent time entries\nfor entry in client.time_entries.list(source=Source.AGENT, from_date=\"2026-03-01\"):\n    print(f\"{entry.user.name}: {entry.hours}h — {entry.notes}\")\n```\n\n### Inspecting Rate Limits and Headers\n\nAgents running high-throughput loops should monitor rate limits. Use `.with_raw_response` to inspect headers:\n\n```python\nresult = client.time_entries.with_raw_response.create(\n    project_id=PROJECT,\n    task_id=TASK,\n    spent_date=TODAY,\n    hours=0.5,\n    source=Source.AGENT,\n)\n\nentry = result.data\nremaining = result.headers.get(\"X-RateLimit-Remaining\")\nif remaining and int(remaining) \u003c 10:\n    print(f\"Warning: only {remaining} API calls remaining\")\n```\n\n---\n\n## API Reference\n\n### Client Configuration\n\n```python\nfrom keito import Keito, AsyncKeito\n\nclient = Keito(\n    api_key=\"kto_...\",             # or KEITO_API_KEY env var\n    account_id=\"acc_...\",          # or KEITO_ACCOUNT_ID env var\n    base_url=\"https://app.keito.io\",  # optional\n    timeout=60.0,                  # request timeout in seconds\n    max_retries=2,                 # retries on 408/429/5xx\n    httpx_client=None,             # bring your own httpx.Client\n)\n```\n\n### Resources\n\n| Resource | Methods |\n|---|---|\n| `client.time_entries` | `list()`, `create()`, `update(id)`, `delete(id)` |\n| `client.expenses` | `list()`, `create()` |\n| `client.projects` | `list()` |\n| `client.clients` | `list()`, `create()`, `get(id)`, `update(id)` |\n| `client.contacts` | `list()`, `create()` |\n| `client.tasks` | `list()` |\n| `client.users` | `me()` |\n| `client.invoices` | `list()`, `create()`, `get(id)`, `update(id)`, `delete(id)` |\n| `client.invoices.messages` | `list(invoice_id)`, `create(invoice_id)` |\n| `client.reports` | `team_time(from_date, to_date)` |\n| `client.outcomes` | `log()` |\n\nAll `list()` methods return auto-paginating iterators. Async variants use `AsyncKeito` and return async iterators.\n\nEvery resource also exposes `.with_raw_response` for raw `httpx.Response` access (see [Raw Response Access](#raw-response-access)).\n\n### Pagination\n\nList methods return iterators that automatically fetch pages:\n\n```python\n# Auto-paginate through all results\nfor entry in client.time_entries.list():\n    print(entry.id)\n\n# Access pagination metadata after first iteration\niterator = client.time_entries.list(per_page=50)\nfirst = next(iterator)\nprint(iterator.total_entries)  # total count across all pages\nprint(iterator.total_pages)\n\n# Async\nasync for entry in async_client.time_entries.list():\n    print(entry.id)\n```\n\n### Error Handling\n\nAll API errors are typed and catchable:\n\n```python\nfrom keito import (\n    KeitoApiError,         # Base class for all API errors\n    KeitoAuthError,        # 401 — invalid or missing credentials\n    KeitoForbiddenError,   # 403 — insufficient permissions\n    KeitoNotFoundError,    # 404 — resource not found\n    KeitoValidationError,  # 400 — invalid request data\n    KeitoConflictError,    # 409 — e.g. deleting an approved entry\n    KeitoRateLimitError,   # 429 — rate limited (has .retry_after)\n    KeitoServerError,      # 5xx — server error\n    KeitoTimeoutError,     # request timed out\n    KeitoConnectionError,  # network connection failure\n)\n\ntry:\n    client.time_entries.delete(\"entry_locked\")\nexcept KeitoConflictError as e:\n    print(e.status_code)  # 409\n    print(e.body)         # {\"error\": \"conflict\", \"error_description\": \"...\"}\nexcept KeitoApiError as e:\n    print(e.status_code, e.body)\n```\n\n### Retries\n\nAutomatic retries with exponential backoff and jitter:\n\n- **Retried status codes:** 408, 429, 500, 502, 503, 504\n- **Default retries:** 2 (configurable per-client or per-request)\n- **Backoff:** `min(2^(attempt-1) * 0.5s, 8s) + jitter`\n- **Retry-After:** Respected on 429 responses\n- **POST safety:** POST requests are not retried by default (not idempotent)\n\n```python\n# Override per-client\nclient = Keito(max_retries=5)\n\n# Override per-request\nentry = client.time_entries.create(\n    ...,\n    request_options={\"max_retries\": 0, \"timeout\": 120.0},\n)\n```\n\n### Per-Request Options\n\nEvery method accepts `request_options` for per-call overrides:\n\n```python\nentry = client.time_entries.create(\n    project_id=\"proj_123\",\n    task_id=\"task_456\",\n    spent_date=\"2026-03-05\",\n    hours=1.5,\n    request_options={\n        \"timeout\": 120.0,\n        \"max_retries\": 0,\n        \"additional_headers\": {\"X-Idempotency-Key\": \"unique-key-123\"},\n    },\n)\n```\n\n### Raw Response Access\n\nEvery resource has a `.with_raw_response` property that wraps method calls to return a `RawResponse` object with the parsed data alongside the raw `httpx.Response`. This is useful for inspecting status codes, headers, and rate limit info.\n\n```python\nfrom keito.core.raw_response import RawResponse\n\n# Access raw response alongside parsed data\nresult = client.time_entries.with_raw_response.create(\n    project_id=\"proj_123\",\n    task_id=\"task_456\",\n    spent_date=\"2026-03-05\",\n    hours=1.5,\n)\n\nprint(result.status_code)        # 201\nprint(result.headers)            # httpx.Headers\nprint(result.raw_response)       # full httpx.Response\n\nentry = result.data              # TimeEntry (parsed Pydantic model)\nprint(entry.id)\n\n# Works on any resource method (except list)\nraw = client.users.with_raw_response.me()\nprint(raw.data.email)\nprint(raw.status_code)\n\n# Async variant\nresult = await async_client.time_entries.with_raw_response.create(...)\n```\n\n### Context Managers\n\nBoth clients support proper resource cleanup:\n\n```python\n# Sync\nwith Keito() as client:\n    entries = list(client.time_entries.list())\n\n# Async\nasync with AsyncKeito() as client:\n    entries = [e async for e in client.time_entries.list()]\n```\n\n---\n\n## Types\n\nAll response models are frozen Pydantic v2 `BaseModel` subclasses. Import from `keito.types`:\n\n```python\nfrom keito.types import (\n    TimeEntry, TimeEntryCreate, TimeEntryUpdate,\n    Expense, ExpenseCreate,\n    Project,\n    ClientModel, ClientCreate,\n    Contact, ContactCreate,\n    Task,\n    User,\n    Invoice, InvoiceCreate, InvoiceUpdate, LineItem,\n    InvoiceMessage, InvoiceMessageCreate,\n    TeamTimeResult,\n    Source, UserType, InvoiceState, PaymentTerm, ApprovalStatus,\n    IdName,\n)\n```\n\n### Enums\n\n```python\nfrom keito.types import Source, UserType, InvoiceState, PaymentTerm, ApprovalStatus\n\nSource.WEB | Source.CLI | Source.API | Source.AGENT\nUserType.HUMAN | UserType.AGENT\nInvoiceState.DRAFT | InvoiceState.OPEN | InvoiceState.PAID | InvoiceState.CLOSED\nPaymentTerm.UPON_RECEIPT | PaymentTerm.NET_15 | PaymentTerm.NET_30 | ...\nApprovalStatus.UNSUBMITTED | ApprovalStatus.SUBMITTED | ApprovalStatus.APPROVED | ApprovalStatus.REJECTED\n```\n\n---\n\n## Development\n\n```bash\ngit clone https://github.com/osodevops/keito-python.git\ncd keito-python\npython3 -m venv .venv \u0026\u0026 source .venv/bin/activate\npip install -e \".[dev]\"\npytest\n```\n\n### Running Tests\n\n```bash\npytest                          # run all tests\npytest --cov=keito              # with coverage\npytest tests/test_retries.py    # single file\nruff check .                    # lint\n```\n\n---\n\n## License\n\nMIT\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fosodevops%2Fkeito-python","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fosodevops%2Fkeito-python","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fosodevops%2Fkeito-python/lists"}