{"id":21611702,"url":"https://github.com/ktsstudio/extapi","last_synced_at":"2026-03-15T16:40:48.928Z","repository":{"id":263250033,"uuid":"870641032","full_name":"ktsstudio/extapi","owner":"ktsstudio","description":"Python/asyncio library for external HTTP requests","archived":false,"fork":false,"pushed_at":"2024-10-11T17:18:15.000Z","size":109,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-07-12T15:43:53.241Z","etag":null,"topics":["aiohttp","asyncio","http","httpx","python","requests"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ktsstudio.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","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}},"created_at":"2024-10-10T12:08:14.000Z","updated_at":"2025-03-18T14:38:38.000Z","dependencies_parsed_at":"2024-11-17T10:20:32.350Z","dependency_job_id":"be5c8d69-e3a0-4f20-8300-77caf88ea3d8","html_url":"https://github.com/ktsstudio/extapi","commit_stats":null,"previous_names":["ktsstudio/extapi"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/ktsstudio/extapi","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktsstudio%2Fextapi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktsstudio%2Fextapi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktsstudio%2Fextapi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktsstudio%2Fextapi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ktsstudio","download_url":"https://codeload.github.com/ktsstudio/extapi/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktsstudio%2Fextapi/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":271420155,"owners_count":24756491,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-21T02:00:08.990Z","response_time":74,"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":["aiohttp","asyncio","http","httpx","python","requests"],"created_at":"2024-11-24T21:13:36.901Z","updated_at":"2026-03-15T16:40:48.879Z","avatar_url":"https://github.com/ktsstudio.png","language":"Python","readme":"# extapi\n\n[![Build](https://github.com/ktsstudio/extapi/actions/workflows/actions.yaml/badge.svg?branch=main)](https://github.com/ktsstudio/extapi/actions)\n[![PyPI](https://img.shields.io/pypi/v/extapi.svg)](https://pypi.python.org/pypi/extapi)\n\nLibrary for performing HTTP calls to external systems. Made to be modular, extensible and easy to use.\n\n## Installation\n\nTo use with aiohttp backend:\n```bash\npip install 'extapi[aiohttp]'\n```\n\nTo use with httpx backend:\n```bash\npip install 'extapi[httpx]'\n```\n\n## Quick example\n\nUsing aiohttp:\n\n```python\nimport asyncio\nfrom extapi.http.backends.aiohttp import AiohttpExecutor\n\n\nasync def main():\n    async with AiohttpExecutor() as executor:\n        async with await executor.get('https://httpbin.org/get') as response:\n            print(response.status)\n            print(await response.read())\n\n\nasyncio.run(main())\n```\n\nUsing httpx:\n\n```python\nimport asyncio\nfrom extapi.http.backends.httpx import HttpxExecutor\n\n\nasync def main():\n    async with HttpxExecutor() as executor:\n        async with await executor.get('https://httpbin.org/get') as response:\n            print(response.status)\n            print(await response.read())\n\n\nasyncio.run(main())\n```\n\n## Features\n\n### Retryable executor\n\nYou can use the `RetryableExecutor` class to retry requests in case of failure. It will retry the request until the maximum number of retries is reached or the request is successful.\n\nThere is also a set of additional `Addon`s to a RetyableExecutor. Usually you would probably use `RetryableExecutor` in your code base.\n\n```python\nimport asyncio\nfrom extapi.http.backends.aiohttp import AiohttpExecutor\nfrom extapi.http.executors.retry import RetryableExecutor\n\n\nasync def main():\n    async with AiohttpExecutor() as backend:\n        executor = RetryableExecutor(backend, max_retries=3)\n\n        async with await executor.get('https://httpbin.org/get') as response:\n            print(response.status)\n            print(await response.read())\n\n\nasyncio.run(main())\n```\n\nAs you can see nothing changes in terms of usage, but now we have retries in case of failure (it may be 500 errors, 401 with custom authentication and token reacquiring for example, TimeoutError, any other Exceptions). There is a default set of addons that are being added to a `RetryableExecutor`:\n\n```python\ndefault_addons = [\n    Retry5xxAddon(),\n    Retry429Addon(),\n    LoggingAddon(),\n]\n```\n\nFirst 2 retry 5xx and 429 status codes. The last one logs the request and response.\n\n### Addons\n\nAddons are a way to extend the functionality of an executor. They can be used to add additional functionality to the executor, like logging, retrying requests, headers passing, authentication, etc.\n\n#### LoggingAddon\n\nThis one is simple ad just logs the fact of a request being sent and a received response.\n\n\n#### VerboseLoggingAddon\n\nThis one is more verbose and logs the request and response headers and body.\n\n#### Retry5xxAddon\n\nThis one retries the request in case of 5xx status code.\n\n#### Retry429Addon\n\nThis one retries the request in case of 429 status code. It also waits for the `Retry-After` header if it is present in the response and waits the specified time.\n\n#### StatusValidationAddon\n\nThis one validates the status code of the response. If the status code is not in the list of allowed status codes, it raises an exception.\n\n#### AddHeadersAddon\n\nThis one adds headers to the request. It expects a callable or an awaitable that accepts headers in order to modify them.\n\nIn the following example we add an `X-Api-Key` header on each request (in case of errors this would be 3 requests).\n\n_Note: the function `headers_patch` is called before each request in case of retries, so you can leverage that to add some keys rotating logic in this case, for example._\n\n```python\nimport asyncio\n\nfrom multidict import CIMultiDict\n\nfrom extapi.http.addons.headers import AddHeadersAddon\nfrom extapi.http.backends.aiohttp import AiohttpExecutor\nfrom extapi.http.executors.retry import RetryableExecutor\n\n\nasync def headers_patch(headers: CIMultiDict):\n    headers[\"X-Api-Key\"] = \"some-api-token\"\n\n\nasync def main():\n    async with AiohttpExecutor() as backend:\n        executor = RetryableExecutor(backend, max_retries=3, addons=[\n            AddHeadersAddon(headers_patch)\n        ])\n\n        async with await executor.get('https://httpbin.org/get') as response:\n            print(await response.read())\n\n\nasyncio.run(main())\n```\n\n#### BearerAuthAddon:\n\nThis one adds a `Authorization` header with a `Bearer` token. As in the previous example, you may execute some complex logic in the callable - like issuing new token in case of 401 error.\n\n```python\nimport asyncio\n\nfrom extapi.http.addons.auth import BearerAuthAddon\nfrom extapi.http.backends.aiohttp import AiohttpExecutor\nfrom extapi.http.executors.retry import RetryableExecutor\n\n\nasync def token_getter() -\u003e str:\n    return \"some-api-token\"\n\n\nasync def main():\n    async with AiohttpExecutor() as backend:\n        executor = RetryableExecutor(backend, max_retries=3, addons=[\n            BearerAuthAddon(token_getter)\n        ])\n\n        async with await executor.get('https://httpbin.org/get') as response:\n            print(await response.read())\n\n\nasyncio.run(main())\n```\n\n##### Custom\n\nYou can also extend any existing addons or create your own. Just inherit from `Addon` and implement the `execute` method.\n\nThis is the `Addon` protocol and your custom addon has to satisfy it.\n```python\n@runtime_checkable\nclass Addon(Protocol[T]):\n    async def before_request(self, request: RequestData) -\u003e None:\n        return None\n\n    async def process_response(\n        self, request: RequestData, response: Response[T]\n    ) -\u003e Response[T]:\n        return response\n\n    async def process_error(self, request: RequestData, error: Exception) -\u003e None:\n        return None\n```\n\nIf you want to execute some custom logic in case of retry you would also need to satisfy a `Retryable` protocol. The `need_retry` function must return a tuple (bool, float | None) where the first element is a flag if the request should be retried and the second is a delay in seconds before the next retry if any.\n\n```python\n@runtime_checkable\nclass Retryable(Protocol[T_contr]):\n    async def need_retry(\n        self, response: Response[T_contr]\n    ) -\u003e tuple[bool, float | None]: ...\n```\n\nAnd as an example Retry5xxAddon:\n\n```python\nclass Retry5xxAddon(Retryable[T], Generic[T]):\n    async def need_retry(self, response: Response[T]) -\u003e tuple[bool, float | None]:\n        if response.status \u003e= 500:\n            return True, 1.0\n\n        return False, None\n```\n\n### Other executors\n\nYou can create your own executor by inheriting from the `Executor` class and implementing the `execute` method. There are a couple more extra executors that modify behaviour of the initial request:\n\n* `OpenTelemetryExecutor` — adds OpenTelemetry tracing to the request.\n* `PrometheusMetricsExecutor` — tracks Prometheus metrics from the request/response.\n* `ConcurrencyLimitedExecutor` - limits amount of concurrent requests that can happen simultaneously.\n* `RateLimitedExecutor` — limits the amount of requests per second/minute. You can choose the window.\n\nLet's see at the full-featured example:\n\n```python\nimport asyncio\n\nfrom multidict import CIMultiDict\n\nfrom extapi.http.addons.auth import BearerAuthAddon\nfrom extapi.http.addons.headers import AddHeadersAddon\nfrom extapi.http.addons.status import StatusValidationAddon\nfrom extapi.http.backends.aiohttp import AiohttpExecutor\nfrom extapi.http.executors.limiters import (\n    ConcurrencyLimitedExecutor,\n    RateLimitedExecutor,\n)\nfrom extapi.http.executors.metrics import PrometheusMetricsExecutor\nfrom extapi.http.executors.retry import RetryableExecutor\nfrom extapi.http.executors.trace import OpenTelemetryExecutor\nfrom extapi.http.metrics.container import MetricsContainer\nfrom extapi.limiters.concurrency.local import LocalConcurrencyLimiter\nfrom extapi.limiters.rps.local import LocalRateLimiter\n\n\nclass TestHeaders:\n    async def __call__(self, headers: CIMultiDict) -\u003e None:\n        headers.add(\"X-Test-Header\", \"test\")\n\n\nclass FooTokenGetter:\n    def __call__(self) -\u003e str:\n        return \"foo-bar\"\n\n\nasync def main():\n    async with AiohttpExecutor() as base:\n        executor = base.generalize()\n        executor = OpenTelemetryExecutor(executor)\n        executor = PrometheusMetricsExecutor(\n            executor, metrics_container=MetricsContainer(metrics_prefix=\"demo\")\n        )\n        executor = RateLimitedExecutor(\n            executor,\n            rate_limiter=LocalRateLimiter(rate_limit=50, rate_limit_window_seconds=1),\n        )\n        executor = ConcurrencyLimitedExecutor(\n            executor,\n            concurrency_limiter=LocalConcurrencyLimiter(max_concurrency=100),\n        )\n\n        executor = RetryableExecutor(\n            executor,\n            addons=[\n                BearerAuthAddon(FooTokenGetter()),\n                AddHeadersAddon(TestHeaders()),\n                StatusValidationAddon((200,)),\n            ],\n        )\n\n        async with await executor.get('https://httpbin.org/get') as response:\n            print(await response.read())\n\nasyncio.run(main())\n```\n\nIn this example we leverage opentelemetry, metrics, rate limiting, retrying mechanics. We also add a custom header and a bearer token to the request. We also validate the status code of the response to be 200.\n\n\n### What to depend on?\n\nIf you need to accept somewhere an executor in your code you may reference a `AbstractExecutor` as the most abstract class that all executors inherit from.\n\n```python\nfrom typing import Any, TypeVar\n\nfrom extapi.http.abc import AbstractExecutor\n\nT = TypeVar(\"T\")\n\nasync def httpbin_get(executor: AbstractExecutor[T]) -\u003e Any:\n    async with await executor.get('https://httpbin.org/get') as response:\n        return await response.json()\n\n```\n\nThis way you may add any executor to your code, and it will work with it.\n\n\n### Get an underlying backend executor\n\nIn some cases you may need to get an underlying backend executor to be sure that you may send a request in a specific format for this particular executor. You can do so like the following:\n\n```python\nimport asyncio\n\nfrom extapi.http.backends.aiohttp import AiohttpExecutor\nfrom extapi.http.executors.retry import RetryableExecutor\nfrom extapi.http.executors.wrapped import unwrap_executor\n\n\nasync def main():\n    async with AiohttpExecutor() as backend:\n        executor = RetryableExecutor(backend)\n\n        assert isinstance(unwrap_executor(executor), AiohttpExecutor)\n\n        # ... the rest\n\nasyncio.run(main())\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fktsstudio%2Fextapi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fktsstudio%2Fextapi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fktsstudio%2Fextapi/lists"}