narwhalapi 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.
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-10-09
4
+
5
+ - Initial typed synchronous and asynchronous clients for the stable API: economics,
6
+ FX, calendars, physical palm oil, spot prices (`commodities.spot_price`), events,
7
+ webhook destinations and calendar subscriptions.
8
+ - `nw.unstable` holds operations not yet in the live API; it is empty in this release.
9
+ - Exact decimals, typed errors, bounded retries, page iterators, response metadata,
10
+ and local webhook signature verification.
11
+ - `codes.PLAN_UPGRADE_REQUIRED` for the 403 returned when a request reaches before the
12
+ plan's history window.
13
+ - The release also covers prediction market reads, FX and commodity bar history, and physical commodity reference price series.
14
+ - Spot `bid` and `ask` are optional because the API may omit them.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Narwhal API Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.5
2
+ Name: narwhalapi
3
+ Version: 0.1.0
4
+ Summary: Typed clients for the Narwhal API
5
+ Project-URL: Homepage, https://narwhalapi.com
6
+ Author-email: Narwhal API Team <support@narwhalapi.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: api,decimal,economics,exchange-rates,fx,holidays,inflation,palm-oil,sdk
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Office/Business :: Financial
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: httpx<1,>=0.27
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Narwhal API for Python
27
+
28
+ Typed synchronous and asynchronous clients for economics, foreign exchange, calendars,
29
+ physical palm oil, physical commodity reference prices, spot gold and silver, prediction markets, FX and commodity bar history, and webhook delivery. Python 3.10 or newer. Install with `pip install narwhalapi`.
30
+
31
+ Set `NARWHAL_API_KEY` in the environment. Keep keys on the server. The default base URL is
32
+ `https://api.narwhalapi.com`; override it with `NARWHAL_BASE_URL` or `base_url=`.
33
+
34
+ ## Quickstart
35
+
36
+ ```pycon
37
+ >>> from decimal import Decimal
38
+ >>> from narwhalapi import Narwhal
39
+ >>> with Narwhal() as nw:
40
+ ... cpi = nw.economics.cpi("USA")
41
+ ... rates = nw.fx.rates("USD", currencies=["EUR", "JPY"])
42
+ ... days = list(nw.fx.rate_history_iter(
43
+ ... "USD", currencies=["EUR"], start_date="2026-08-20", end_date="2026-08-24"
44
+ ... ))
45
+ >>> isinstance(cpi.value, Decimal)
46
+ True
47
+ >>> isinstance(rates.rates[0].rate, Decimal)
48
+ True
49
+ >>> all(isinstance(rate.rate, Decimal) for day in days for rate in day.rates)
50
+ True
51
+
52
+ ```
53
+
54
+ Decimals remain exact `Decimal` values. Use `format(value, "f")` to display fixed-point
55
+ wire text, including very small values. Decimal inputs accept `Decimal` or `str`;
56
+ other numeric types raise `TypeError`. Responses accept only decimal strings matching
57
+ `^-?\d+(\.\d+)?$` with ASCII digits. Dates and timestamps remain strings.
58
+
59
+ ```python
60
+ from narwhalapi import AsyncNarwhal, errors
61
+
62
+ async def load_rates():
63
+ async with AsyncNarwhal() as nw:
64
+ try:
65
+ return await nw.fx.rates("USD", currencies=["IDR", "SGD"])
66
+ except errors.RateLimited as error:
67
+ if error.is_quota_exhausted:
68
+ return None
69
+ raise
70
+ ```
71
+
72
+ Share a synchronous client across threads. Use one async client per event loop.
73
+ Context managers close connections; explicit `close()` and `await aclose()` also work.
74
+
75
+ ## Operations and types
76
+
77
+ Namespaces are `economics`, `fx`, `calendars`, `commodities`, `events`, `webhooks`, and
78
+ `calendar_subscriptions`. Operations not yet in the live API appear under `nw.unstable`;
79
+ there are none today.
80
+ Models and known string constants are available through `narwhalapi.models`,
81
+ for example `from narwhalapi.models import Cpi`.
82
+ Unknown enum strings are retained. Union variants are frozen dataclasses with a `kind`;
83
+ its `kind` is the variant model name (for example `PalmoilSchedulePrice`).
84
+ An unknown variant has `kind="unknown"` and keeps its JSON in `raw`.
85
+ New response fields are ignored.
86
+
87
+ Required path arguments may be positional. Query and body arguments are keyword-only:
88
+
89
+ ```python
90
+ nw.fx.convert_currency(from_="USD", to="IDR", amount="100.00")
91
+ nw.fx.rate_history("USD", currencies=["IDR"], start_date="2026-01-01", end_date="2026-01-31")
92
+ spot = nw.commodities.spot_price("XAU", "USD")
93
+ nw.webhooks.update_destination(
94
+ "destination-id", url="https://example.test/webhook",
95
+ countries=["IDN"], families=["prices"], enabled=False,
96
+ )
97
+ feed_url = nw.calendar_subscriptions.feed_url("subscription-token")
98
+ ```
99
+
100
+ Updates require `enabled` explicitly. Creation sends only supplied fields; omitted fields
101
+ and explicit `None` remain distinct in request bodies. `feed_url()` makes no request.
102
+ Lists use the documented repeated or comma-joined encoding. Scalar strings, empty list
103
+ items and items containing commas are rejected for list query parameters.
104
+
105
+ Every paginated operation has an `_iter` method, yielding items lazily (use `async for`
106
+ with an async client). Filters persist across pages; `limit` is the page size. Cursor expiry
107
+ raises `errors.CursorExpired`, including after earlier items were yielded.
108
+
109
+ Top-level `meta` contains optional `request_id`, `quota`, and `short` values from response
110
+ headers. Each rate limit has optional `limit`, `remaining`, and `reset` fields.
111
+
112
+ ## Failures and retries
113
+
114
+ All client failures inherit `errors.NarwhalError`. `errors.ApiError` exposes `status`,
115
+ `code`, `title`, `detail`, `request_id`, `instance`, `retry_after`, and `raw_body` bytes.
116
+ Typed subclasses cover common statuses and cursor expiry. Known codes are exported as
117
+ uppercase constants in `narwhalapi.codes`; codes also accept future strings.
118
+ `TransportError.cause` preserves the underlying error. `DecodeError.raw_body` keeps an
119
+ invalid success response as the original bytes. Import exceptions directly, for example
120
+ `from narwhalapi.errors import RateLimited`. API errors display server fields as
121
+ `HTTP <status> <code>: <title> (request_id <id>)`. All SDK errors support pickling.
122
+ Client representations exclude credentials.
123
+
124
+ Defaults are a 30-second attempt timeout, a 10-second connect timeout, two retries, and a
125
+ 30-second maximum retry wait. Only short-window 429s retry on any method. A 503 retries
126
+ on GET; a transport failure retries on GET only before any response. Monthly quota
127
+ exhaustion never retries. Redirects are not followed.
128
+ Use `max_retries=0` to disable retries. Both clients honor standard proxy and certificate
129
+ environment variables, including `HTTPS_PROXY` and `SSL_CERT_FILE`; supply `proxy=`
130
+ to select an explicit proxy. `extra_headers` cannot replace Authorization.
131
+ The `transport`, `clock`, `sleep`, and `random_fraction` constructor options support tests;
132
+ async `sleep` must be awaitable.
133
+
134
+ ## Webhooks
135
+
136
+ `from narwhalapi.webhooks import verify` verifies **exact raw bytes** before decoding.
137
+ It accepts a nonempty secret string or a nonempty list of nonempty secrets during
138
+ rotation. Empty secrets raise `ValueError` before verification. Timestamp tolerance is
139
+ 300 seconds by default. Use the returned `event_id` as the idempotency key and store it
140
+ before applying the event. Future schema versions still decode.
141
+
142
+ FastAPI:
143
+
144
+ ```python
145
+ raw_body = await request.body()
146
+ event = verify(raw_body, request.headers, webhook_secret)
147
+ ```
148
+
149
+ Flask:
150
+
151
+ ```python
152
+ raw_body = request.get_data(cache=True)
153
+ event = verify(raw_body, request.headers, webhook_secret)
154
+ ```
155
+
156
+ Do not parse and re-serialize the body before verification. A verification failure raises
157
+ `errors.WebhookVerificationError` with a `reason`. Neither framework is a package dependency.
@@ -0,0 +1,132 @@
1
+ # Narwhal API for Python
2
+
3
+ Typed synchronous and asynchronous clients for economics, foreign exchange, calendars,
4
+ physical palm oil, physical commodity reference prices, spot gold and silver, prediction markets, FX and commodity bar history, and webhook delivery. Python 3.10 or newer. Install with `pip install narwhalapi`.
5
+
6
+ Set `NARWHAL_API_KEY` in the environment. Keep keys on the server. The default base URL is
7
+ `https://api.narwhalapi.com`; override it with `NARWHAL_BASE_URL` or `base_url=`.
8
+
9
+ ## Quickstart
10
+
11
+ ```pycon
12
+ >>> from decimal import Decimal
13
+ >>> from narwhalapi import Narwhal
14
+ >>> with Narwhal() as nw:
15
+ ... cpi = nw.economics.cpi("USA")
16
+ ... rates = nw.fx.rates("USD", currencies=["EUR", "JPY"])
17
+ ... days = list(nw.fx.rate_history_iter(
18
+ ... "USD", currencies=["EUR"], start_date="2026-08-20", end_date="2026-08-24"
19
+ ... ))
20
+ >>> isinstance(cpi.value, Decimal)
21
+ True
22
+ >>> isinstance(rates.rates[0].rate, Decimal)
23
+ True
24
+ >>> all(isinstance(rate.rate, Decimal) for day in days for rate in day.rates)
25
+ True
26
+
27
+ ```
28
+
29
+ Decimals remain exact `Decimal` values. Use `format(value, "f")` to display fixed-point
30
+ wire text, including very small values. Decimal inputs accept `Decimal` or `str`;
31
+ other numeric types raise `TypeError`. Responses accept only decimal strings matching
32
+ `^-?\d+(\.\d+)?$` with ASCII digits. Dates and timestamps remain strings.
33
+
34
+ ```python
35
+ from narwhalapi import AsyncNarwhal, errors
36
+
37
+ async def load_rates():
38
+ async with AsyncNarwhal() as nw:
39
+ try:
40
+ return await nw.fx.rates("USD", currencies=["IDR", "SGD"])
41
+ except errors.RateLimited as error:
42
+ if error.is_quota_exhausted:
43
+ return None
44
+ raise
45
+ ```
46
+
47
+ Share a synchronous client across threads. Use one async client per event loop.
48
+ Context managers close connections; explicit `close()` and `await aclose()` also work.
49
+
50
+ ## Operations and types
51
+
52
+ Namespaces are `economics`, `fx`, `calendars`, `commodities`, `events`, `webhooks`, and
53
+ `calendar_subscriptions`. Operations not yet in the live API appear under `nw.unstable`;
54
+ there are none today.
55
+ Models and known string constants are available through `narwhalapi.models`,
56
+ for example `from narwhalapi.models import Cpi`.
57
+ Unknown enum strings are retained. Union variants are frozen dataclasses with a `kind`;
58
+ its `kind` is the variant model name (for example `PalmoilSchedulePrice`).
59
+ An unknown variant has `kind="unknown"` and keeps its JSON in `raw`.
60
+ New response fields are ignored.
61
+
62
+ Required path arguments may be positional. Query and body arguments are keyword-only:
63
+
64
+ ```python
65
+ nw.fx.convert_currency(from_="USD", to="IDR", amount="100.00")
66
+ nw.fx.rate_history("USD", currencies=["IDR"], start_date="2026-01-01", end_date="2026-01-31")
67
+ spot = nw.commodities.spot_price("XAU", "USD")
68
+ nw.webhooks.update_destination(
69
+ "destination-id", url="https://example.test/webhook",
70
+ countries=["IDN"], families=["prices"], enabled=False,
71
+ )
72
+ feed_url = nw.calendar_subscriptions.feed_url("subscription-token")
73
+ ```
74
+
75
+ Updates require `enabled` explicitly. Creation sends only supplied fields; omitted fields
76
+ and explicit `None` remain distinct in request bodies. `feed_url()` makes no request.
77
+ Lists use the documented repeated or comma-joined encoding. Scalar strings, empty list
78
+ items and items containing commas are rejected for list query parameters.
79
+
80
+ Every paginated operation has an `_iter` method, yielding items lazily (use `async for`
81
+ with an async client). Filters persist across pages; `limit` is the page size. Cursor expiry
82
+ raises `errors.CursorExpired`, including after earlier items were yielded.
83
+
84
+ Top-level `meta` contains optional `request_id`, `quota`, and `short` values from response
85
+ headers. Each rate limit has optional `limit`, `remaining`, and `reset` fields.
86
+
87
+ ## Failures and retries
88
+
89
+ All client failures inherit `errors.NarwhalError`. `errors.ApiError` exposes `status`,
90
+ `code`, `title`, `detail`, `request_id`, `instance`, `retry_after`, and `raw_body` bytes.
91
+ Typed subclasses cover common statuses and cursor expiry. Known codes are exported as
92
+ uppercase constants in `narwhalapi.codes`; codes also accept future strings.
93
+ `TransportError.cause` preserves the underlying error. `DecodeError.raw_body` keeps an
94
+ invalid success response as the original bytes. Import exceptions directly, for example
95
+ `from narwhalapi.errors import RateLimited`. API errors display server fields as
96
+ `HTTP <status> <code>: <title> (request_id <id>)`. All SDK errors support pickling.
97
+ Client representations exclude credentials.
98
+
99
+ Defaults are a 30-second attempt timeout, a 10-second connect timeout, two retries, and a
100
+ 30-second maximum retry wait. Only short-window 429s retry on any method. A 503 retries
101
+ on GET; a transport failure retries on GET only before any response. Monthly quota
102
+ exhaustion never retries. Redirects are not followed.
103
+ Use `max_retries=0` to disable retries. Both clients honor standard proxy and certificate
104
+ environment variables, including `HTTPS_PROXY` and `SSL_CERT_FILE`; supply `proxy=`
105
+ to select an explicit proxy. `extra_headers` cannot replace Authorization.
106
+ The `transport`, `clock`, `sleep`, and `random_fraction` constructor options support tests;
107
+ async `sleep` must be awaitable.
108
+
109
+ ## Webhooks
110
+
111
+ `from narwhalapi.webhooks import verify` verifies **exact raw bytes** before decoding.
112
+ It accepts a nonempty secret string or a nonempty list of nonempty secrets during
113
+ rotation. Empty secrets raise `ValueError` before verification. Timestamp tolerance is
114
+ 300 seconds by default. Use the returned `event_id` as the idempotency key and store it
115
+ before applying the event. Future schema versions still decode.
116
+
117
+ FastAPI:
118
+
119
+ ```python
120
+ raw_body = await request.body()
121
+ event = verify(raw_body, request.headers, webhook_secret)
122
+ ```
123
+
124
+ Flask:
125
+
126
+ ```python
127
+ raw_body = request.get_data(cache=True)
128
+ event = verify(raw_body, request.headers, webhook_secret)
129
+ ```
130
+
131
+ Do not parse and re-serialize the body before verification. A verification failure raises
132
+ `errors.WebhookVerificationError` with a `reason`. Neither framework is a package dependency.
@@ -0,0 +1,14 @@
1
+ """Keep repository ignore files out of the source distribution."""
2
+
3
+ from pathlib import Path
4
+
5
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
6
+
7
+
8
+ class CustomBuildHook(BuildHookInterface):
9
+ def initialize(self, version, build_data):
10
+ if self.target_name == "sdist":
11
+ forced = build_data["force_include"]
12
+ for path in list(forced):
13
+ if Path(path).name in {".gitignore", ".hgignore"}:
14
+ del forced[path]
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "narwhalapi"
7
+ version = "0.1.0"
8
+ description = "Typed clients for the Narwhal API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [{ name = "Narwhal API Team", email = "support@narwhalapi.com" }]
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
14
+ dependencies = ["httpx>=0.27,<1"]
15
+ keywords = [
16
+ "api", "sdk", "economics", "fx", "exchange-rates", "inflation", "holidays",
17
+ "palm-oil", "decimal",
18
+ ]
19
+ classifiers = [
20
+ "Intended Audience :: Developers",
21
+ "Operating System :: OS Independent",
22
+ "Topic :: Office/Business :: Financial",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ "Typing :: Typed",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Programming Language :: Python :: 3.14",
31
+ "License :: OSI Approved :: MIT License",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://narwhalapi.com"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/narwhalapi"]
39
+
40
+ [tool.hatch.build.targets.sdist]
41
+ include = ["src/narwhalapi", "README.md", "CHANGELOG.md", "LICENSE", "pyproject.toml"]
42
+
43
+ [tool.hatch.build.hooks.custom]
@@ -0,0 +1,17 @@
1
+ """Typed clients for the Narwhal API."""
2
+
3
+ from . import codes, errors, models
4
+ from ._client import AsyncNarwhal, Narwhal
5
+ from ._meta import RateLimit, ResponseMeta
6
+ from ._version import __version__
7
+
8
+ __all__ = [
9
+ "AsyncNarwhal",
10
+ "Narwhal",
11
+ "RateLimit",
12
+ "ResponseMeta",
13
+ "__version__",
14
+ "codes",
15
+ "errors",
16
+ "models",
17
+ ]
@@ -0,0 +1,122 @@
1
+ """Connection-owning synchronous and asynchronous clients."""
2
+
3
+ import asyncio
4
+ import random
5
+ import time
6
+ from collections.abc import Awaitable, Callable, Mapping
7
+ from types import TracebackType
8
+ from typing import TypeVar
9
+
10
+ import httpx
11
+
12
+ from ._generated.operations import (
13
+ LIVE_SPEC_SHA,
14
+ AsyncStableOperations,
15
+ StableOperations,
16
+ )
17
+ from ._transport import AsyncTransport, Settings, SyncTransport
18
+
19
+ SyncClient = TypeVar("SyncClient", bound="Narwhal")
20
+ AsyncClient = TypeVar("AsyncClient", bound="AsyncNarwhal")
21
+
22
+
23
+ class Narwhal(StableOperations):
24
+ """Share one client across threads and close it when finished."""
25
+
26
+ def __init__(
27
+ self,
28
+ api_key: str | None = None,
29
+ *,
30
+ base_url: str | None = None,
31
+ proxy: str | None = None,
32
+ timeout: float = 30,
33
+ max_retries: int = 2,
34
+ max_retry_wait: float = 30,
35
+ extra_headers: Mapping[str, str] | None = None,
36
+ transport: httpx.BaseTransport | None = None,
37
+ sleep: Callable[[float], None] = time.sleep,
38
+ clock: Callable[[], float] = time.monotonic,
39
+ random_fraction: Callable[[], float] = random.random,
40
+ ) -> None:
41
+ settings = Settings(
42
+ api_key=api_key,
43
+ base_url=base_url,
44
+ proxy=proxy,
45
+ timeout=timeout,
46
+ max_retries=max_retries,
47
+ max_retry_wait=max_retry_wait,
48
+ extra_headers=extra_headers,
49
+ clock=clock,
50
+ random_fraction=random_fraction,
51
+ spec_sha=LIVE_SPEC_SHA,
52
+ )
53
+ self._transport = SyncTransport(settings, transport=transport, sleep=sleep)
54
+ super().__init__(self._transport)
55
+
56
+ def __repr__(self) -> str:
57
+ return "Narwhal(api_key=<redacted>)"
58
+
59
+ def close(self) -> None:
60
+ self._transport.http.close()
61
+
62
+ def __enter__(self: SyncClient) -> SyncClient:
63
+ return self
64
+
65
+ def __exit__(
66
+ self,
67
+ exc_type: type[BaseException] | None,
68
+ exc: BaseException | None,
69
+ tb: TracebackType | None,
70
+ ) -> None:
71
+ self.close()
72
+
73
+
74
+ class AsyncNarwhal(AsyncStableOperations):
75
+ """Use one client per event loop and close it with async with."""
76
+
77
+ def __init__(
78
+ self,
79
+ api_key: str | None = None,
80
+ *,
81
+ base_url: str | None = None,
82
+ proxy: str | None = None,
83
+ timeout: float = 30,
84
+ max_retries: int = 2,
85
+ max_retry_wait: float = 30,
86
+ extra_headers: Mapping[str, str] | None = None,
87
+ transport: httpx.AsyncBaseTransport | None = None,
88
+ sleep: Callable[[float], Awaitable[None]] = asyncio.sleep,
89
+ clock: Callable[[], float] = time.monotonic,
90
+ random_fraction: Callable[[], float] = random.random,
91
+ ) -> None:
92
+ settings = Settings(
93
+ api_key=api_key,
94
+ base_url=base_url,
95
+ proxy=proxy,
96
+ timeout=timeout,
97
+ max_retries=max_retries,
98
+ max_retry_wait=max_retry_wait,
99
+ extra_headers=extra_headers,
100
+ clock=clock,
101
+ random_fraction=random_fraction,
102
+ spec_sha=LIVE_SPEC_SHA,
103
+ )
104
+ self._transport = AsyncTransport(settings, transport=transport, sleep=sleep)
105
+ super().__init__(self._transport)
106
+
107
+ def __repr__(self) -> str:
108
+ return "AsyncNarwhal(api_key=<redacted>)"
109
+
110
+ async def aclose(self) -> None:
111
+ await self._transport.http.aclose()
112
+
113
+ async def __aenter__(self: AsyncClient) -> AsyncClient:
114
+ return self
115
+
116
+ async def __aexit__(
117
+ self,
118
+ exc_type: type[BaseException] | None,
119
+ exc: BaseException | None,
120
+ tb: TracebackType | None,
121
+ ) -> None:
122
+ await self.aclose()