NestAPI Python Client
A small, fully dynamic sync and async Python client for the NestAPI web services.
The library contains no service names: any method you call becomes a route. New services work the moment they go live, and removed ones need no code change.
Features
- Dynamic routing:
api.ip_info()callsip-infowith no per-service classes or models - Sync and async:
NestApiandAsyncNestApishare the same routing, parameters and errors - Keyword or dict parameters, with validation left to the server
- Immutable version switching through
version() - Read-only responses with dot-path access and
res["key"]lookups - Typed exceptions mapped from the API's
error.code - Fully typed, and built on
httpxwith your own client as an option
Requirements
- Python 3.10 or higher
- httpx, installed automatically
Installation
pip install nestapi-py
The package is published as nestapi-py and imported as nestapi.
Quick start
from nestapi import NestApi
with NestApi() as api:
result = api.bmi(age=30, weight=70, height=175)
print(result["bmi"]) # 22.86
print(result.get("input.age")) # 30
Async
AsyncNestApi works exactly like NestApi: routing, parameters, versions, responses and errors are the same. Await every service call, as well as call(), request() and close(). version() is a plain method.
import asyncio
from nestapi import AsyncNestApi
async def main() -> None:
async with AsyncNestApi() as api:
result = await api.bmi(age=30, weight=70, height=175)
print(result["bmi"]) # 22.86
# Run several calls at the same time
first, second = await asyncio.gather(
api.bmi(age=30, weight=70, height=175),
api.version(2).call("ip-info", {"ip": "8.8.8.8"}),
)
asyncio.run(main())
Use one AsyncNestApi instance inside one event loop, because its connections are tied to that loop. To inject a client, pass an httpx.AsyncClient.
The examples below use NestApi. With AsyncNestApi, add await to each call.
Routing
The route is built from the method name. Every _ becomes -, every uppercase letter except the first starts a new - segment, and the result is lowercase.
| Method | Route |
|---|---|
ip_info |
ip-info |
search_shazam |
search-shazam |
chatAi, ChatAi |
chat-ai |
chatAI |
chat-a-i |
Names starting with _ are not services and raise AttributeError.
Pass parameters as keyword arguments or as a single dict. The dict form is needed for names that are Python keywords, such as from or in.
api.bmi(age=30, weight=70, height=175)
api.bmi({"age": 30, "weight": 70, "height": 175})
api.convert({"from": "USD", "in": "EUR"})
api.bmi(30, 70) # TypeError: arguments must be named
api.bmi({"age": 30}, weight=70) # TypeError: use one form, not both
api.bmi({1: "a"}) # TypeError: names must be strings
Explicit calls
# GET with the exact route name, no conversion
api.call("ip-info", {"ip": "8.8.8.8"})
# Any other HTTP method; parameters still go in the query string
api.request("POST", "some-service", {"a": 1})
Parameter values
| Python value | Sent as | Notes |
|---|---|---|
True / False |
x=true / x=false |
The server rejects 1, 0, True and TRUE |
None |
Not sent | Also skipped inside lists and dicts |
list / tuple |
x=1&x=2 |
Repeated keys. Not yet verified against the live API, as no documented service takes an array input |
dict |
x[key]=1 |
Same format as the PHP client |
Spaces are encoded as %20, and non-ASCII text is sent as UTF-8.
Versions
The default version is 1, and the apis- prefix is added for you. version() returns a copy and leaves the original instance unchanged. A version below 1 raises ValueError, and anything other than an int, including bool, raises TypeError.
api = NestApi(version=2)
v3 = api.version(3)
There is no automatic "latest version" mode. The source.version field in responses is unrelated to the version in the URL.
Responses
A successful call returns a read-only nestapi.Response.
result = api.bmi(age=30, weight=70, height=175)
result.data # Dict, list, string or None
result.meta # Optional, service-defined
result.source # {"name": "NestAPI", ...}
result.get("input.age") # 30
result.get("input.nope", 0) # 0
result.get("items.0.title") # Paths can index into lists
result["bmi"] # 22.86
"bmi" in result # True
result["nope"] # KeyError
result.data = {} # AttributeError
result["bmi"] = 1 # TypeError
del result["bmi"] # TypeError
A key that exists with a null value gives None, not the default. If data is not a dict or list, get() returns the default value.
Error handling
Branch on error_code, not on the message, because message text can change.
| Exception | Raised for |
|---|---|
ServiceNotFoundError |
NOT_FOUND: unknown service or API version |
ValidationError |
PARAM_NOT_FOUND, VALIDATION_ERROR |
RateLimitError |
RATE_LIMITED, RATE_LIMIT_EXCEEDED |
ServerError |
Any other code with HTTP status 500 or above |
ApiError |
Base class of the above, and any other code such as INVALID_IP |
TransportError |
Network failure, non-JSON body, or a body outside the API format |
TransportError is separate from ApiError, so catch it as well if you want to handle every failure.
from nestapi import ApiError, NestApi, TransportError, ValidationError
try:
api.bmi(age=500, weight=70, height=175)
except ValidationError as e:
e.error_code # "VALIDATION_ERROR"
e.status # 422
e.details # {"query.age": "Must be less than or equal to 120"}
except ApiError as e:
... # Any other error reported by the API
except TransportError as e:
... # No usable response arrived; the cause is in e.__cause__
details is a list for PARAM_NOT_FOUND and a dict for VALIDATION_ERROR.
Configuration
NestApi() works without any setup. Every option can be set through the constructor:
import httpx
api = NestApi(
version=1,
base_url="https://open.nestapi.ir",
timeout=30, # Seconds, default client only
client=httpx.Client(), # Optional; httpx.AsyncClient for AsyncNestApi
)
SSL verification is always on in the default client, and there is no option to turn it off.
Closing connections
Call close() or use with NestApi() as api: to release the connection. With the async client, use await api.close() or async with AsyncNestApi() as api:.
- A client you inject stays yours:
close()leaves it open. - Copies from
version()share the client of the original. Closing a copy does nothing, so the original keeps working. - Closing the original closes the shared client, and its copies then raise
TransportError.
Testing
pip install -e ".[dev]"
pytest
Releasing
- Bump
__version__insrc/nestapi/__init__.pyand add an entry toCHANGELOG.md. - Commit, then create a GitHub release with a tag such as
v1.0.1. - The
Publish to PyPIworkflow builds the package and uploads it through PyPI trusted publishing.
License
Released under the MIT License.
Metadata
Release files for nestapi-py 1.0.1
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 | Actions |
|---|---|---|---|---|
| nestapi_py-1.0.1.tar.gz | 16.9 kB | With provenance | Details for nestapi_py-1.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| nestapi_py-1.0.1-py3-none-any.whl | Python 3 | none | any | Details for nestapi_py-1.0.1-py3-none-any.whl |
Total release size: 28.8 kB
Release files / nestapi_py-1.0.1.tar.gz
| Download URL | nestapi_py-1.0.1.tar.gz |
|---|---|
| Size | 16.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ea8edcb939bbfd98ba4c90f8a78e829818631f102b3ee19a883c68c6cb05f60f
|
|
BLAKE2b-256 checksum How to use checksums |
c5f4d45fb59cfce4a15ef4b124f4a37002fcf1ec721eadd7986ca5efed65f2a2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.
Transparency logRelease files / nestapi_py-1.0.1-py3-none-any.whl
| Download URL | nestapi_py-1.0.1-py3-none-any.whl |
|---|---|
| Size | 11.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0662059d6a826000aaece2abe90288f8df2f31e9f6f3a000d13dcfd998daa506
|
|
BLAKE2b-256 checksum How to use checksums |
6f9883ee71bb3d723f2118101807cc6f4504474abc8d078092d3a8537ff4cc29
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.
Transparency logSecurity for nestapi-py 1.0.1
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.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for nestapi_py-1.0.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.
Transparency logA 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.
To learn more about protecting yourself from malicious actors, visit our package security documentation.