apipool-ng
apipool-ng is a next-generation Multiple API Key Manager.
It allows developers to manage multiple API keys simultaneously. For example,
if a single API key has 1000/day quota, you can register 10 API keys and let
apipool-ng automatically rotate them.
Features
-
Automatically rotate API keys across multiple credentials.
-
Built-in usage statistics, searchable by time, status, and apikey.
Stats collector can be deployed on any relational database (SQLite by default).
-
Clean API, minimal code required to implement complex features.
-
Async support —
adummyclientprovides full async chain-proxy withawait. -
Server SDK mode — connect to an
apipool-serverinstance and proxy all APIcalls through it, with transparent key rotation and zero local key material.
-
Dynamic scaling —
DynamicKeyManagerauto-refreshes keys from server,expanding or shrinking the pool in real time.
-
Lightweight dependencies —
sqlalchemy+httpx.
Installation
pip install apipool-ng
Quick Start
Library Mode (Local Keys)
Implement an ApiKey subclass, then create an ApiKeyManager:
from apipool import ApiKey, ApiKeyManager
class MyApiKey(ApiKey):
def __init__(self, key):
self.key = key
def get_primary_key(self):
return self.key
def create_client(self):
return MyApiClient(api_key=self.key)
def test_usability(self, client):
return client.test_connection()
apikeys = [MyApiKey(k) for k in ["key1", "key2", "key3"]]
manager = ApiKeyManager(apikey_list=apikeys)
manager.check_usable()
# Synchronous calls — keys auto-rotate
result = manager.dummyclient.some_api_method(arg)
Server SDK Mode (Remote Proxy)
Connect to an apipool-server instance — no API keys stored locally:
import os
from apipool import connect, login
# 1. Authenticate
tokens = login(
service_url="http://localhost:8000",
username="alice",
password="password",
)
# 2. Connect to a pool
manager = connect(
service_url="http://localhost:8000",
pool_identifier="google-geocoding",
auth_token=tokens["access_token"],
)
# 3. Use exactly like library mode
result = manager.dummyclient.geocode("1600 Amphitheatre Parkway")
Fetch Raw Keys from Server
Retrieve decrypted keys and build a local ApiKeyManager:
from apipool import login, get_keys, ApiKeyManager
tokens = login("http://localhost:8000", "alice", "password")
raw_keys = get_keys(
service_url="http://localhost:8000",
pool_identifier="coingecko",
auth_token=tokens["access_token"],
)
# raw_keys = ["CG-xxx", "CG-yyy", ...]
DummyClient & AsyncDummyClient
Use manager.dummyclient (sync) or manager.adummyclient (async) just like
your original API client. Under the hood, they automatically select a usable
key, record usage events, and rotate keys on rate-limit errors.
# Sync — use dummyclient
result = manager.dummyclient.some_method()
# Async — use adummyclient
result = await manager.adummyclient.some_method()
# Multi-level attribute chains are supported natively
result = await manager.adummyclient.coins.simple.price.get(ids="bitcoin")
Rate Limit Handling
Specify the exception type that indicates rate-limit exhaustion:
from apipool import ApiKeyManager
manager = ApiKeyManager(
apikey_list=apikeys,
reach_limit_exc=RateLimitError, # auto-rotate on this exception
)
When a call raises reach_limit_exc, the key is automatically removed from
the pool and the exception is re-raised. When all keys are exhausted,
PoolExhaustedError is raised on the next call.
from apipool import ApiKeyManager, PoolExhaustedError
try:
result = manager.dummyclient.some_method()
except PoolExhaustedError:
print("All API keys exhausted")
Server SDK Mode (Full API)
| Function | Sync | Async |
|---|---|---|
| Authenticate | login(service_url, username, password) | await alogin(...) |
| Connect to pool | connect(service_url, pool_identifier, auth_token) | await async_connect(...) |
| Fetch raw keys | get_keys(service_url, pool_identifier, auth_token) | await aget_keys(...) |
Async example:
from apipool import async_connect, alogin
tokens = await alogin("http://localhost:8000", "alice", "password")
manager = await async_connect(
service_url="http://localhost:8000",
pool_identifier="coingecko",
auth_token=tokens["access_token"],
)
result = await manager.adummyclient.coins.simple.price.get(ids="bitcoin")
Dynamic Key Manager (Auto-Refresh)
DynamicKeyManager extends ApiKeyManager with a background thread that
periodically fetches the latest key list from apipool-server and reconciles
the local pool — adding new keys and removing deleted ones automatically.
Sync: DynamicKeyManager
from apipool import DynamicKeyManager, get_keys, login
tokens = login("http://localhost:8000", "alice", "password")
manager = DynamicKeyManager(
key_fetcher=lambda: get_keys(
service_url="http://localhost:8000",
pool_identifier="coingecko",
auth_token=tokens["access_token"],
),
api_key_factory=lambda raw_key: CoinGeckoApiKey(raw_key),
refresh_interval=120, # seconds
on_keys_added=lambda keys: print(f"Added: {keys}"),
on_keys_removed=lambda keys: print(f"Removed: {keys}"),
)
# Use like a normal ApiKeyManager
result = manager.dummyclient.ping()
print(f"Pool size: {manager.pool_size}")
# Graceful shutdown
manager.shutdown()
Async: AsyncDynamicKeyManager
from apipool import AsyncDynamicKeyManager, aget_keys, alogin
tokens = await alogin("http://localhost:8000", "alice", "password")
manager = AsyncDynamicKeyManager(
key_fetcher=lambda: aget_keys(
service_url="http://localhost:8000",
pool_identifier="coingecko",
auth_token=tokens["access_token"],
),
api_key_factory=lambda raw_key: AsyncCoinGeckoKey(raw_key),
refresh_interval=120,
)
await manager.astart() # initial fetch + auto-refresh task
result = await manager.adummyclient.coins.simple.price.get(ids="bitcoin")
print(f"Pool size: {manager.pool_size}")
await manager.ashutdown()
How It Works
| Step | Description |
|---|---|
| 1. Fetch | Call key_fetcher() to get the latest list[str] from the server |
| 2. Diff | Compare server keys vs. local active + archived keys |
| 3. Add | New keys → create via api_key_factory → add to pool |
| 4. Restore | Archived keys that reappear on server → restore to active pool |
| 5. Remove | Keys gone from server → remove from active pool to archive |
| 6. Callback | on_keys_added / on_keys_removed fired if keys changed |
Parameters
| Parameter | Type | Description |
|---|---|---|
| key_fetcher | Callable[[], list[str]] | Returns current raw keys from server |
| api_key_factory | Callable[[str], ApiKey] | Converts raw key to ApiKey instance |
| refresh_interval | float | Seconds between refreshes (default 60) |
| on_keys_added | Callable[[list[str]], None] | Callback after keys are added |
| on_keys_removed | Callable[[list[str]], None] | Callback after keys are removed |
| config_fetcher | Callable[[], PoolConfig] | Returns pool config from server (optional) |
Configuration Sync
DynamicKeyManager can automatically sync configuration from the server.
Pass a config_fetcher (typically get_config bound with your credentials)
to enable automatic config synchronization on each refresh cycle.
PoolConfig
Server-side pool configuration is stored as pool_config JSON and synced to
the client as a PoolConfig dataclass:
| Field | Type | Default | Description |
|---|---|---|---|
| concurrency | int | 0 | Max concurrent calls (0 = unlimited) |
| timeout | float | 30.0 | Per-request timeout in seconds |
| rate_limit | int | 0 | Max requests per key per interval |
| rate_limit_interval | int | 60 | Interval for rate limit counting |
| retry_on_failure | bool | False | Retry failed calls on another key |
| max_retries | int | 0 | Maximum retry attempts |
| custom | dict | {} | Arbitrary key-value settings |
| batch_retry_on_failure | bool \| None | None | Batch retry (falls back to retry_on_failure) |
| batch_max_retries | int \| None | None | Batch retries (falls back to max_retries) |
| ban_threshold | int | 3 | Consecutive failures before key ban |
| ban_duration | float | 300.0 | Key ban duration in seconds |
| reach_limit_exception | str | None | Dotted path to exception class |
| rotation_strategy | str | "random" | Key rotation strategy |
Sync Example
from apipool import DynamicKeyManager, get_keys, get_config, login
tokens = login("http://localhost:8000", "alice", "password")
manager = DynamicKeyManager(
key_fetcher=lambda: get_keys(
service_url="http://localhost:8000",
pool_identifier="coingecko",
auth_token=tokens["access_token"],
),
api_key_factory=lambda raw_key: CoinGeckoApiKey(raw_key),
config_fetcher=lambda: get_config(
service_url="http://localhost:8000",
pool_identifier="my-pool",
auth_token=tokens["access_token"],
),
refresh_interval=120,
)
# Config is auto-synced. Access it anytime:
print(f"Concurrency: {manager.config.concurrency}")
print(f"Timeout: {manager.config.timeout}")
Manual Config Fetch
from apipool import get_config, aget_config
# Sync
config = get_config("http://localhost:8000", "my-pool", tokens["access_token"])
manager.apply_config(config)
# Async
config = await aget_config("http://localhost:8000", "my-pool", tokens["access_token"])
Concurrent Execution
Execute the same method across multiple argument sets with bounded concurrency:
Sync: call_concurrent
# Execute 100 API calls with max 10 concurrent
results = manager.call_concurrent(
method_name="some_api_method",
args_list=[(arg1,), (arg2,), (arg3,)],
kwargs_list=[{"key": "a"}, {"key": "b"}, {"key": "c"}],
max_concurrency=10, # overrides config.concurrency
timeout=15.0, # overrides config.timeout
)
Async: acall_concurrent
results = await manager.acall_concurrent(
method_name="coins.simple.price.get",
args_list=[(), (), ()],
kwargs_list=[
{"ids": "bitcoin"},
{"ids": "ethereum"},
{"ids": "solana"},
],
max_concurrency=5,
timeout=10.0,
)
Concurrency and timeout default to manager.config values when not specified.
Batch Execution
batch_exec and abatch_exec are designed for high-volume workloads
such as fetching 10 000 token prices from CoinGecko. Key features:
-
Deduplication — each
item_idis guaranteed to execute at most once. -
Retry with rotation — when an API call fails, the item is retried on a
different key, honouring the key rotation strategy.
-
Temporary banning — keys that accumulate
ban_thresholdconsecutivefailures are temporarily excluded from the batch group for
ban_durationseconds, then automatically re-admitted.
-
Server-configurable — all tuning parameters can be set centrally in
pool_configon the server and synced to clients viaPoolConfig.
Sync: batch_exec
from apipool import ApiKeyManager, BatchResult
manager = ApiKeyManager(apikey_list=apikeys)
# Each item is (item_id, args_tuple, kwargs_dict)
items = [
("bitcoin", (), {"ids": "bitcoin", "vs_currencies": "usd"}),
("ethereum", (), {"ids": "ethereum", "vs_currencies": "usd"}),
# ... up to 10 000 items
]
result: BatchResult = manager.batch_exec(
method_name="coins.simple.price.get",
items=items,
max_concurrency=20, # 20 parallel calls
timeout=10.0, # per-call timeout
retry_on_failure=True, # retry on another key
max_retries=3, # up to 3 retries per item
ban_threshold=3, # ban key after 3 consecutive failures
ban_duration=300.0, # ban for 5 minutes
)
print(f"Success: {result.succeeded}/{result.total} ({result.success_rate:.1%})")
print(f"Failed items: {list(result.errors.keys())}")
print(f"Banned keys: {list(result.banned_keys.keys())}")
# Access individual results
btc_price = result.results["bitcoin"]
Async: abatch_exec
result: BatchResult = await manager.abatch_exec(
method_name="coins.simple.price.get",
items=items,
max_concurrency=50,
retry_on_failure=True,
max_retries=3,
)
BatchResult
| Field | Type | Description |
|---|---|---|
| total | int | Total items submitted |
| succeeded | int | Items that completed successfully |
| failed | int | Items that failed after all retries |
| results | dict | item_id → result for successful items |
| errors | dict | item_id → Exception for failed items |
| banned_keys | dict | primary_key → ban_expiry_timestamp |
| elapsed | float | Wall-clock seconds for the batch |
| success_rate | float | Property: succeeded / total |
How It Works
| Step | Description |
|---|---|
| 1. Deduplicate | Each item_id is unique — no item is executed twice |
| 2. Dispatch | Items are dispatched to ThreadPoolExecutor (sync) or asyncio.Semaphore (async) |
| 3. Execute | Each call goes through the normal key selection → ChainProxy → stats pipeline |
| 4. Fail → Retry | On failure, the item is retried on a different key (up to max_retries) |
| 5. Ban | Keys hitting ban_threshold consecutive failures are banned for ban_duration |
| 6. Collect | Successful results and final errors are collected into BatchResult |
Batch Config Fields
These PoolConfig fields control batch behaviour from the server:
| Field | Type | Default | Description |
|---|---|---|---|
| batch_retry_on_failure | bool \| None | None (→ retry_on_failure) | Retry failed items on another key |
| batch_max_retries | int \| None | None (→ max_retries) | Max retries per item in batch |
| ban_threshold | int | 3 | Consecutive failures before banning a key |
| ban_duration | float | 300.0 | Seconds a banned key is excluded |
ApiKey Abstract Class
Subclass ApiKey and implement three methods:
| Method | Description |
|---|---|
| get_primary_key() | Return a unique identifier for this key |
| create_client() | Create and return the SDK client instance |
| test_usability(client) | Test if the key is usable; return bool |
Optional async methods on ApiKey:
| Method | Description |
|---|---|
| aconnect_client() | Async version of connect_client() |
| ais_usable() | Async version of is_usable() |
StatsCollector
Query usage statistics through manager.stats:
from apipool import StatusCollection
# Usage count per key in last hour
manager.stats.usage_count_stats_in_recent_n_seconds(3600)
# {"key1": 3, "key2": 5, "key3": 2}
# Count specific events
count = manager.stats.usage_count_in_recent_n_seconds(
n_seconds=3600,
status_id=StatusCollection.c9_ReachLimit.id,
)
API Reference
Exported Symbols
from apipool import (
# Core
ApiKey,
ApiKeyManager,
PoolExhaustedError,
BatchResult,
# Async chain proxy
AsyncDummyClient,
AsyncChainProxy,
AsyncApiCaller,
# Dynamic scaling
DynamicKeyManager,
AsyncDynamicKeyManager,
# Stats
StatusCollection,
StatsCollector,
# Configuration
PoolConfig,
# Server SDK mode — sync
connect,
login,
get_keys,
get_config,
# Server SDK mode — async
async_connect,
alogin,
aget_keys,
aget_config,
)
License
MIT License
Metadata
Release files for apipool-ng 1.0.9
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| apipool_ng-1.0.9.tar.gz | 135.8 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| apipool_ng-1.0.9-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 274.7 kB
Release files / apipool_ng-1.0.9.tar.gz
| Download URL | apipool_ng-1.0.9.tar.gz |
|---|---|
| Size | 135.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
291f9fded3cb3c8a7eb631cf99b38eb7c1f645a658200f66d5d067ca07287863
|
|
BLAKE2b-256 checksum How to use checksums |
0c7191777fd6f31732c7c5cab03592c3fcd1ab5f88079817fd966e1e17f7042c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.0
|
Release files / apipool_ng-1.0.9-py3-none-any.whl
| Download URL | apipool_ng-1.0.9-py3-none-any.whl |
|---|---|
| Size | 138.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d6ca6470de69ea058a9c29a2b39abb99839c17be345a50eb088fa46ac2012fb1
|
|
BLAKE2b-256 checksum How to use checksums |
d53340818b960cfdd503924f72a9019a79a5820a003453d520b40f1d550edc5c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.0
|
Security for apipool-ng 1.0.9
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.