keycall 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. keycall-0.1.0/.github/workflows/ci.yml +79 -0
  2. keycall-0.1.0/.github/workflows/release.yml +130 -0
  3. keycall-0.1.0/.gitignore +52 -0
  4. keycall-0.1.0/CHANGELOG.md +124 -0
  5. keycall-0.1.0/LICENSE +661 -0
  6. keycall-0.1.0/PKG-INFO +141 -0
  7. keycall-0.1.0/README.md +104 -0
  8. keycall-0.1.0/USAGE.md +278 -0
  9. keycall-0.1.0/keycall-test-keys.example.toml +47 -0
  10. keycall-0.1.0/keycall-test-keys.example.txt +21 -0
  11. keycall-0.1.0/pyproject.toml +67 -0
  12. keycall-0.1.0/src/keycall/__init__.py +63 -0
  13. keycall-0.1.0/src/keycall/_cache.py +65 -0
  14. keycall-0.1.0/src/keycall/_capabilities.py +32 -0
  15. keycall-0.1.0/src/keycall/_catalog/catalog.json +82 -0
  16. keycall-0.1.0/src/keycall/_classify.py +59 -0
  17. keycall-0.1.0/src/keycall/_cli.py +197 -0
  18. keycall-0.1.0/src/keycall/_client.py +496 -0
  19. keycall-0.1.0/src/keycall/_credential.py +65 -0
  20. keycall-0.1.0/src/keycall/_dnsguard.py +135 -0
  21. keycall-0.1.0/src/keycall/_enums.py +36 -0
  22. keycall-0.1.0/src/keycall/_errors.py +68 -0
  23. keycall-0.1.0/src/keycall/_registry.py +207 -0
  24. keycall-0.1.0/src/keycall/_sanitize.py +46 -0
  25. keycall-0.1.0/src/keycall/_sources.py +235 -0
  26. keycall-0.1.0/src/keycall/_tracing.py +124 -0
  27. keycall-0.1.0/src/keycall/_transport.py +392 -0
  28. keycall-0.1.0/src/keycall/_types.py +279 -0
  29. keycall-0.1.0/src/keycall/adapters/__init__.py +33 -0
  30. keycall-0.1.0/src/keycall/adapters/_anthropic.py +169 -0
  31. keycall-0.1.0/src/keycall/adapters/_base.py +123 -0
  32. keycall-0.1.0/src/keycall/adapters/_gemini.py +240 -0
  33. keycall-0.1.0/src/keycall/adapters/_openai.py +133 -0
  34. keycall-0.1.0/src/keycall/adapters/_openai_compat.py +127 -0
  35. keycall-0.1.0/src/keycall/adapters/_perplexity.py +78 -0
  36. keycall-0.1.0/src/keycall/py.typed +0 -0
  37. keycall-0.1.0/tests/conftest.py +19 -0
  38. keycall-0.1.0/tests/test_adapters.py +272 -0
  39. keycall-0.1.0/tests/test_classify.py +37 -0
  40. keycall-0.1.0/tests/test_cli.py +94 -0
  41. keycall-0.1.0/tests/test_client.py +207 -0
  42. keycall-0.1.0/tests/test_credential.py +62 -0
  43. keycall-0.1.0/tests/test_hardening.py +414 -0
  44. keycall-0.1.0/tests/test_registry.py +80 -0
  45. keycall-0.1.0/tests/test_sources.py +145 -0
  46. keycall-0.1.0/tests/test_tracing.py +114 -0
  47. keycall-0.1.0/tests/test_transport.py +136 -0
  48. keycall-0.1.0/tests/test_types.py +96 -0
@@ -0,0 +1,79 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+
26
+ - name: Install
27
+ run: pip install -e ".[dev]"
28
+
29
+ - name: Lint
30
+ run: ruff check src tests
31
+
32
+ - name: Test
33
+ # No provider secrets: the suite runs against recorded fixtures only.
34
+ run: pytest -q
35
+
36
+ build:
37
+ runs-on: ubuntu-latest
38
+ steps:
39
+ - uses: actions/checkout@v4
40
+
41
+ - uses: actions/setup-python@v5
42
+ with:
43
+ python-version: "3.12"
44
+
45
+ - name: Build
46
+ run: |
47
+ pip install build twine
48
+ python -m build
49
+
50
+ - name: Check metadata
51
+ run: twine check dist/*
52
+
53
+ - name: Verify package data ships
54
+ # catalog.json and py.typed are easy to lose to a packaging change
55
+ # and only fail at runtime, so assert them here.
56
+ run: |
57
+ python - <<'EOF'
58
+ import glob, zipfile, sys
59
+ wheel = sorted(glob.glob("dist/*.whl"))[-1]
60
+ names = zipfile.ZipFile(wheel).namelist()
61
+ missing = [
62
+ f for f in ("keycall/_catalog/catalog.json", "keycall/py.typed")
63
+ if f not in names
64
+ ]
65
+ if missing:
66
+ sys.exit(f"missing from wheel: {missing}")
67
+ print(f"{wheel}: package data present")
68
+ EOF
69
+
70
+ - name: Install from wheel and smoke test
71
+ run: |
72
+ python -m venv /tmp/smoke
73
+ /tmp/smoke/bin/pip install dist/*.whl
74
+ /tmp/smoke/bin/python -c "
75
+ import keycall
76
+ from keycall._registry import resolve_provider, catalog_version
77
+ assert resolve_provider('anthropic').base_url.startswith('https://')
78
+ print('keycall', keycall.__version__, 'catalog', catalog_version())
79
+ "
@@ -0,0 +1,130 @@
1
+ name: Release
2
+
3
+ # Publishing is tag-driven: push a v* tag, and this builds, verifies,
4
+ # publishes to PyPI, and cuts the GitHub release. Nothing publishes from a
5
+ # developer machine.
6
+ on:
7
+ push:
8
+ tags: ["v*"]
9
+ workflow_dispatch:
10
+ inputs:
11
+ test_pypi:
12
+ description: "Publish to TestPyPI instead of PyPI"
13
+ type: boolean
14
+ default: true
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ jobs:
20
+ build:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - uses: actions/setup-python@v5
26
+ with:
27
+ python-version: "3.12"
28
+
29
+ - name: Install tooling
30
+ run: pip install build twine
31
+
32
+ - name: Verify tag matches package version
33
+ # A tag that disagrees with pyproject would publish the wrong version
34
+ # under the right name, which cannot be undone on PyPI.
35
+ if: startsWith(github.ref, 'refs/tags/v')
36
+ run: |
37
+ python - <<'EOF'
38
+ import os, sys, tomllib
39
+ with open("pyproject.toml", "rb") as f:
40
+ version = tomllib.load(f)["project"]["version"]
41
+ tag = os.environ["GITHUB_REF_NAME"].removeprefix("v")
42
+ if tag != version:
43
+ sys.exit(f"tag {tag!r} != pyproject version {version!r}")
44
+ print(f"version {version} matches tag")
45
+ EOF
46
+
47
+ - name: Run tests
48
+ run: |
49
+ pip install -e ".[dev]"
50
+ pytest -q
51
+
52
+ - name: Build
53
+ run: python -m build
54
+
55
+ - name: Check metadata
56
+ run: twine check dist/*
57
+
58
+ - name: Verify package data ships
59
+ run: |
60
+ python - <<'EOF'
61
+ import glob, zipfile, sys
62
+ wheel = sorted(glob.glob("dist/*.whl"))[-1]
63
+ names = zipfile.ZipFile(wheel).namelist()
64
+ missing = [
65
+ f for f in ("keycall/_catalog/catalog.json", "keycall/py.typed")
66
+ if f not in names
67
+ ]
68
+ if missing:
69
+ sys.exit(f"missing from wheel: {missing}")
70
+ EOF
71
+
72
+ - uses: actions/upload-artifact@v4
73
+ with:
74
+ name: dist
75
+ path: dist/
76
+
77
+ publish-testpypi:
78
+ needs: build
79
+ if: github.event_name == 'workflow_dispatch' && inputs.test_pypi
80
+ runs-on: ubuntu-latest
81
+ environment: testpypi
82
+ permissions:
83
+ id-token: write # OIDC for trusted publishing; no API token stored
84
+ steps:
85
+ - uses: actions/download-artifact@v4
86
+ with:
87
+ name: dist
88
+ path: dist/
89
+
90
+ - uses: pypa/gh-action-pypi-publish@release/v1
91
+ with:
92
+ repository-url: https://test.pypi.org/legacy/
93
+
94
+ publish-pypi:
95
+ needs: build
96
+ if: startsWith(github.ref, 'refs/tags/v')
97
+ runs-on: ubuntu-latest
98
+ environment: pypi
99
+ permissions:
100
+ id-token: write # OIDC for trusted publishing; no API token stored
101
+ steps:
102
+ - uses: actions/download-artifact@v4
103
+ with:
104
+ name: dist
105
+ path: dist/
106
+
107
+ - uses: pypa/gh-action-pypi-publish@release/v1
108
+
109
+ github-release:
110
+ needs: publish-pypi
111
+ if: startsWith(github.ref, 'refs/tags/v')
112
+ runs-on: ubuntu-latest
113
+ permissions:
114
+ contents: write # needed to create the release
115
+ steps:
116
+ - uses: actions/checkout@v4
117
+
118
+ - uses: actions/download-artifact@v4
119
+ with:
120
+ name: dist
121
+ path: dist/
122
+
123
+ - name: Create GitHub release
124
+ env:
125
+ GH_TOKEN: ${{ github.token }}
126
+ run: |
127
+ gh release create "$GITHUB_REF_NAME" \
128
+ --title "$GITHUB_REF_NAME" \
129
+ --notes "See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md)." \
130
+ dist/*
@@ -0,0 +1,52 @@
1
+ # Internal design docs (PRD, naming decisions, research) — not part of the public package.
2
+ /internal/
3
+
4
+ # macOS and system files.
5
+ .DS_Store
6
+ .AppleDouble
7
+ .LSOverride
8
+ ._*
9
+ __MACOSX/
10
+ Thumbs.db
11
+ Desktop.ini
12
+
13
+ # Local environment and tool state.
14
+ .env
15
+ .env.*
16
+ .claude/
17
+ .codex/
18
+
19
+ # Live-test credential files (the .example templates stay tracked).
20
+ keycall-test-keys.*
21
+ !keycall-test-keys.example.*
22
+
23
+ # Internal documentation (top-level project docs stay tracked).
24
+ *.md
25
+ !README.md
26
+ !USAGE.md
27
+ !CHANGELOG.md
28
+ !CONTRIBUTING.md
29
+
30
+ # Python build/runtime artifacts.
31
+ __pycache__/
32
+ *.py[cod]
33
+ *.egg-info/
34
+ dist/
35
+ build/
36
+ .venv/
37
+ .pytest_cache/
38
+ .mypy_cache/
39
+ .ruff_cache/
40
+ .coverage
41
+ htmlcov/
42
+
43
+ # Build/package artifacts and logs.
44
+ *.zip
45
+ *.tar
46
+ *.tar.gz
47
+ *.tgz
48
+ *.log
49
+
50
+ # Editor/project metadata.
51
+ .idea/
52
+ .vscode/
@@ -0,0 +1,124 @@
1
+ # Changelog
2
+
3
+ All notable changes to KeyCall are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ Nothing yet.
11
+
12
+ ## [0.1.0] — 2026-08-05
13
+
14
+ First release. Key validation, model discovery and filtering, and text
15
+ generation, live-verified against every supported provider.
16
+
17
+ ### Added
18
+
19
+ **Clients**
20
+
21
+ - `KeyCall` and `AsyncKeyCall` with identical surfaces; only awaiting differs.
22
+ - Provider and credential bound once at construction as immutable client
23
+ identity: no setters, no per-call override, no public `api_key` property.
24
+ - Context-manager support; `close()` releases the credential and HTTP client.
25
+ - Configurable `connect_timeout`, `read_timeout`, `max_response_bytes`,
26
+ `trust_env`, `allow_insecure_localhost`, and `allow_private_network`.
27
+
28
+ **Model discovery**
29
+
30
+ - `list_models(categories=..., refresh=...)` returning a `ModelDiscovery`
31
+ envelope with `models`, `fetched_at`, `from_cache`, `catalog_version`, and
32
+ `warnings`.
33
+ - Eight-member `ModelCategory` taxonomy; text generation is the default filter.
34
+ - Conservative classification: explicit provider metadata first, then
35
+ maintained identifier rules. Unclassifiable models resolve to `UNKNOWN` and
36
+ never enter the default text picker.
37
+ - Process-local availability cache with a 5-minute TTL, keyed by an HMAC
38
+ fingerprint of the credential rather than the credential itself.
39
+
40
+ **Text generation**
41
+
42
+ - `invoke(TextGenerationRequest)` as the low-level primitive and
43
+ `generate_text(...)` as the convenience path.
44
+ - Normalized `InvocationResult` with typed output parts, `usage`,
45
+ `round_trip_duration_ms`, `provider_request_id`, `finish_reason`, and
46
+ `warnings`.
47
+ - `temperature` and `top_p`, validated at construction and omitted from the
48
+ wire body when unset. Models with maintained evidence that they reject
49
+ sampling parameters (OpenAI o-series and gpt-5; Anthropic Opus 4.7+,
50
+ Opus 5+, Sonnet 5+) fail with `MODEL_NOT_SUITABLE` before any network call.
51
+ - Usage fields distinguish "provider reported zero" from "provider did not
52
+ report", which stays `None` and is never fabricated.
53
+
54
+ **Providers**
55
+
56
+ - OpenAI (Responses API), Anthropic (Messages), Google Gemini
57
+ (`generateContent`), DeepSeek, Perplexity (Sonar), Moonshot/Kimi.
58
+ - Explicit OpenAI-compatible custom targets via `protocol` plus `base_url`.
59
+ - Provider identity and wire protocol kept as separate concepts; named
60
+ providers may override their protocol's default adapter.
61
+
62
+ **Errors**
63
+
64
+ - Single `KeyCallError` with a typed `ErrorCode` discriminator, plus
65
+ `retryable`, `status_code`, `provider_request_id`, and `retry_after`.
66
+ - Twelve normalized codes spanning credential, model, provider, transport, and
67
+ setup failures.
68
+
69
+ **Security**
70
+
71
+ - Credentials wrapped in a redacting type at the single public entry point;
72
+ excluded from reprs, formatting, exceptions, logs, traces, copies, and
73
+ pickles, with canary tests asserting absence.
74
+ - `Credential.reveal()` called from exactly one place, the transport layer's
75
+ header builder.
76
+ - Provider-originated error text scrubbed for credential values, credential
77
+ patterns, and control characters, then length-bounded, before reaching any
78
+ result, exception, log, or trace.
79
+ - Redirects refused rather than followed while carrying a credential.
80
+ - Response bodies read incrementally against a 10 MB cap.
81
+ - SSRF guard rejecting literal private, loopback, link-local, and reserved IP
82
+ targets unless explicitly opted in.
83
+ - DNS-rebinding guard for custom endpoints: resolves once, rejects if any
84
+ resolved address is private, then connects to the validated address while
85
+ preserving the original hostname for TLS SNI and `Host`.
86
+ - HTTPS required for custom endpoints; plain HTTP only for localhost behind an
87
+ explicit flag.
88
+
89
+ **Reliability**
90
+
91
+ - Operation-aware retries: bounded retries with backoff and `Retry-After`
92
+ support for model listing; none for generation, since no supported provider
93
+ documents generation idempotency.
94
+ - Explicit connect and read timeouts on every request.
95
+
96
+ **CLI**
97
+
98
+ - `keycall verify` for live credential verification from TXT, JSON, or TOML
99
+ files, an explicit `env:VAR_NAME` reference, or an interactive prompt.
100
+ - `--generate` makes one bounded call per target, walking filtered models in
101
+ provider order and reporting every attempt (`--attempts`, default 8) so
102
+ provider drift stays visible.
103
+ - `--strict-credentials` promotes credential-file warnings to errors.
104
+ - Credential files are never modified or deleted; keys never appear in output.
105
+
106
+ **Observability**
107
+
108
+ - Optional TraceAct integration emitting `keycall.list_models` and
109
+ `keycall.text_generation` spans with safe fields only. Silent when TraceAct
110
+ is absent or the host has not configured it; disabled with one warning on an
111
+ incompatible version.
112
+
113
+ ### Known limitations
114
+
115
+ - Streaming, tool calling, structured output, and non-text modalities are not
116
+ implemented.
117
+ - Gemini's list endpoint advertises models an account cannot invoke and exposes
118
+ no lifecycle field, so they cannot be pre-filtered.
119
+ - Perplexity's Sonar models are not API-discoverable and are maintained in the
120
+ bundled catalog.
121
+ - The provider catalog ships inside the package and updates only on release.
122
+
123
+ [Unreleased]: https://github.com/shehuphd/keycall/compare/v0.1.0...HEAD
124
+ [0.1.0]: https://github.com/shehuphd/keycall/releases/tag/v0.1.0