Builds the proxy username a gateway expects, and refuses the input it would silently drop.
Quickstart · Parameters · Sticky sessions · Errors · Account API · Other gateways · Documentation
Proxy opens no socket. It builds the username a proxy gateway expects, refuses
the input that gateway would mishandle, and hands the result to whatever HTTP
client you already use.
Two calls do reach the network, both by name and neither on import:
proxy.check() opens one CONNECT and tells you what the gateway said about it,
and Client talks to the dashboard API.
Works with requests · httpx · aiohttp · Playwright · Patchright · Puppeteer · curl - and anything else that takes a proxy URL, because that is all it hands back.
pip install nodemaven
Nothing else is required. The only dependency is tomli, and only on Python
3.10 and older, where the standard library has no TOML parser.
login and password are the Proxy Username and Proxy Password assigned under
Proxy Setup in the dashboard - a separate pair from
the account you sign in with. The other option there is IP whitelisting, which needs no
credentials in the username at all; both are described in
authentication methods.
from nodemaven import Proxy
proxy = Proxy(login="your-login", password="your-password",
country="us", filter="medium")
print(proxy.check())200 Connection established via gate.nodemaven.com:8080 in 0.42s, exit 203.0.113.7
That is one CONNECT and no traffic through the tunnel. A refusal is a return value, not an exception, because the status code is the thing you came for and raising would bury it in a traceback:
407 Proxy Authentication Required via gate.nodemaven.com:8080 in 0.19s
usually NOT your credentials, despite what the status says. A value the gateway
will not take on `country`, `filter`, `ttl`, `type` or `speed` answers 407, and
so does a wrong password. Check the values before the password - and check the
case of `ttl`, which is the one value that is case-sensitive: `10M` is refused
where `10m` is accepted.
That second paragraph is data in the gateway definition, not a string in this library, because what a status code means is per-gateway.
check() tunnels to api.ipify.org:443 by default and whatever you name will
see a TCP connection from your exit address, so the target is a parameter rather
than a constant. The timeout defaults to 15 seconds, because one of this
gateway's measured reactions is no reply for about 20 seconds.
proxy.check(target="example.com:443", timeout=15.0)The example below uses requests, which this package does not install - it
has no HTTP client of its own and does not want one. Install it alongside:
pip install nodemaven[requests]
import requests
from nodemaven import Proxy
proxy = Proxy(login="your-login", password="your-password",
country="us", filter="medium")
r = requests.get("https://api.ipify.org", proxies=proxy.requests())
print(r.text)203.0.113.42
If that is not your own address, it worked. If it is your own, the request never went through the proxy.
Credentials can come from the environment instead, so nothing is in your source:
# NODEMAVEN_LOGIN and NODEMAVEN_PASSWORD
proxy = Proxy(country="us", filter="medium")proxy.url() # http://user:pass@gate.nodemaven.com:8080 - httpx, aiohttp, curl
proxy.requests() # {"http": ..., "https": ...}
proxy.httpx() # {"http://": ..., "https://": ...}
proxy.playwright() # {"server": ..., "username": ..., "password": ...}
proxy.username # the username on its own
proxy.server # host:port, no credentialsWith Playwright, Patchright or Puppeteer:
pip install playwright
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context(proxy=proxy.playwright())Because the gateway does not tell you when you get it wrong. A misspelt parameter is not refused - the tunnel opens, the setting is dropped, and the traffic you are paying to route through a medium-quality US pool goes out wherever the gateway felt like:
# by hand: a typo the gateway answers 200 to, and never mentions again
"http://user-country-us-filtr-medium:pass@gate.nodemaven.com:8080"
# with this library: refused before anything is sent
Proxy(login="user", password="pass", country="us", filtr="medium")ParamError: NodeMaven does not know the parameter 'filtr': it is answered with
200 and dropped, so the connection would succeed and your setting would NOT be
applied. Known: ['city', 'country', 'filter', 'ipv4', 'isp', 'region', 'sid',
'speed', 'ttl', 'type']
That is the whole reason the package exists. The gateway answers a wrong value five different ways and names the parameter in none of them; the table of what it does instead, and the probes behind it, are in docs/validation.md.
What the shipped NodeMaven definition accepts. Every name here was confirmed against the gateway rather than transcribed:
| parameter | what it selects | values measured to work |
|---|---|---|
country |
country code, or any |
us, de, ... |
region |
area inside the country | a name |
city |
city inside the country | a name, with a region beside it |
isp |
the exit's ISP | a name |
type |
mobile or residential exits | mobile, residential |
sid |
the sticky session - see below | any string with no - |
ttl |
how long that session is held | 1m, 10m, 10h, 24h |
filter |
IP quality | low, medium, high |
speed |
claims a connection speed class | fast, slow |
ipv4 |
claims to force IPv4 | True |
type picks a different pool rather than a filter over one pool. Five requests
per arm with a fresh sid and country=us: type=mobile drew T-Mobile and
Verizon Wireless ASNs, while type=residential and leaving it unset drew
Comcast, Charter, Windstream and other wireline carriers, with no mobile ASN
among them.
ipv4 and speed are confirmed names whose effects are unmeasured, and the
table says claims to for that reason.
Names are validated. Values, on this gateway, are not. A name outside the table raises before anything is sent, because the gateway answers an unknown name with 200 and drops the setting. Values are passed through, because what is known is which ones have been observed to work, and that is not the same as the set the gateway accepts - refusing on a guessed list would block a setting that would have worked.
country, region, city, isp and type are folded before they are sent -
trimmed, lowered, and each remaining space turned into _ - so country="US"
and region="District of Columbia" are not your problem:
Proxy(login="u", password="p", region="District of Columbia").username
# u-region-district_of_columbiasid, filter, ttl and speed keep their case, and for ttl that matters:
ttl-10m opens the tunnel and ttl-10M is answered 407. Every value that is
not folded is refused if it contains whitespace.
Credentials come from NODEMAVEN_LOGIN and NODEMAVEN_PASSWORD when not passed
in, and the gateway address from NODEMAVEN_HOST and NODEMAVEN_PORT.
Which names fold, why the values are not checked against a list, and what each wrong value is answered with, are in docs/validation.md.
One Proxy is one identity. Pin it to a sticky session:
held = proxy.session("order4417")A session id cannot contain the character the gateway separates parameters
with, which for this one is -, and passing one raises rather than
connecting. The gateway cuts the value at the separator, so without the refusal
every order id beginning order would quietly share one session and one exit.
The session key is the whole parameter set, not the session id. Adding or removing any parameter moves you to a different exit address, which is why parameters change through a method that returns a new object rather than by assignment - the move is a different identity, and the code should say so:
germany = proxy.replace(country="de") # a new identity, a new exit
plain = proxy.replace(filter=None) # also a new identityFor a worker pool, one identity per worker:
for identity in proxy.sessions(50):
queue.put(identity) # each one a different exitEach id is 2 * length hexadecimal characters from secrets, length=6 by
default, and the ids are distinct within one call. The measurements behind
the separator rule, and why the ids are hex rather than uuid4(), are in
docs/validation.md.
Everything inherits from NodeMavenError, so one except catches everything
this library raises.
| exception | raised when |
|---|---|
ParamError |
a parameter name is unknown, a value is empty, a value contains whitespace or the gateway's separator, or a value is outside a list the definition declares |
CredentialsError |
no login, no password, no gateway address, or no API key, from arguments or environment |
ProviderError |
a gateway definition is missing, unreadable, or internally inconsistent |
CheckError |
check() got no answer at all - DNS, a refused connection, a timeout, or something that is not a proxy on that port |
ApiError |
the account API refused a call, or answered a shape this library cannot read. Carries .status and .body |
AuthError |
the API key was rejected - ApiError with a 401 or 403 |
NotFoundError |
the row is gone - ApiError with a 404 |
RateLimitError |
too many calls - ApiError with a 429, and a .retry_after |
NodeMavenError |
the base, never raised on its own |
ParamError also covers the two structural cases: session() on a definition
that declares no session parameter, and a definition whose parameter names
collide with login, password, host, port or provider.
The first three are found before a socket exists - they are failures in what
you asked for, not in what happened. The next five have been to the network and
back. That split is why they are separate classes: retrying a ParamError can
only produce the same ParamError.
Two credentials, two exceptions, and they are not interchangeable.
CredentialsError from Proxy is the proxy password, refused before
anything is sent; AuthError from Client is the dashboard API key, and it
has been to the server. A library that reported both as one would tell you to fix
the key when the password is wrong.
Quota, usage, sub-users and the location catalogue. Separately credentialled, because the API key and the proxy password are different secrets from different places.
from nodemaven import Client
client = Client() # NODEMAVEN_APIKEY from the environment
me = client.me()
print(me["data"]) # traffic leftme() returns the server's own object with its own field names, unrenamed and
unmodelled - data is the traffic left and there is no traffic_left, and
is_traffic_frozen is a string. It also returns your proxy password in
clear text, on every call, and so does every row of sub_users(). Do not
print, log or paste either.
client.countries() # the catalogue, paginated
client.regions(country__code="us") # Django's field lookup, the server's spelling
client.cities(country__code="us", region__code="dc")
client.isps(country__code="us") # a different envelope, see the docs
client.zip_codes(country__code="us")
# Which regions and cities the ISP and zip-code catalogues actually cover.
# Separate endpoints, not filters on the two above.
client.isp_regions(country__code="us")
client.isp_cities(country__code="us", region__code="dc")
client.zip_code_regions(country__code="us")
client.zip_code_cities(country__code="us", region__code="dc")
# Statistics are per proxy username, and the username is required.
# The range is `start` and `end`, not `start_date` and `end_date`, and the
# dates are `dd-mm-yyyy`. ISO is answered 400.
client.statistics_data("acct-1", start="01-09-2026", end="07-09-2026")
client.statistics_requests("acct-1", start="01-09-2026")
client.domain_statistics("acct-1", period="hours24")
client.sub_users(page=1)
client.create_sub_user("worker-1", "a-password", traffic_limit=1024)
client.update_sub_user(id, traffic_limit=2048)
client.delete_sub_user(id)
client.reset_sub_user_usage([id]) # a list, even for one
client.whitelist_ips(page=1)
client.whitelist_ip(id)
client.upsert_whitelist_ip("203.0.113.7", 10, name="the office")
client.delete_whitelist_ip(id)A list endpoint returns a Page, which iterates one page and not the
collection. iterate() gets the rest:
page = client.countries() # one page, 1000 rows by default
for country in client.iterate(page): # all of them
...page.count is None everywhere, paging differs by endpoint, and one page can
be half a collection with nothing in the reply saying so. That is the API's
shape rather than a gap here, and it is measured in
docs/observed-behavior.md.
The gateway answers a country it does not have with 407, which reads as a
credentials problem. The catalogue knows better, so it can be asked:
problems = client.validate(proxy)
if problems:
raise SystemExit("\n".join(problems))No account here? Any proxy you already have works. Parameters are data, not hardcoded keywords. A gateway is its prefix, separators, session parameter and the set of parameter names it actually recognises - and a definition written by you goes through the same builder and the same validation as the one shipped here.
Build it in place:
from nodemaven import Provider, Proxy
mine = Provider(id="mine", label="My proxy", known_params=frozenset())
proxy = Proxy(provider=mine, login="u", password="p",
host="proxy.example.com", port=8000)Or keep the definition in a TOML file:
# my-gateway.toml
label = "My proxy"
known_params = ["country", "session"]
session_param = "session"
host = "proxy.example.com"
port = 8000from nodemaven import Proxy, load_file
mine = load_file("my-gateway.toml")
proxy = Proxy(provider=mine, login="u", password="p", country="us")
proxy.session("order4417") # u-country-us-session-order4417known_params is the whole point of the file: name a parameter that is not in
it and the call raises instead of connecting. Leave the list empty and every
parameter is refused, which is the correct thing to say about a gateway whose
dialect nobody has established.
Credentials fall back to the environment under the definition's id in upper case,
so this one reads MY_GATEWAY_LOGIN and MY_GATEWAY_PASSWORD and never
NODEMAVEN_*. One process can hold several gateways without their credentials
reaching each other.
The id comes from the filename, not from the variable you assign it to.
load_file("my-gateway.toml") is my-gateway however it is named in your code,
and - becomes _ in the variable names. Pass provider_id= to say it
outright. The error raised when a credential is missing prints the exact pair it
looked for, so this is one guess you never have to make.
Every definition carries a status. measured means traffic has gone through
that gateway and the dialect was read off the wire. documented means it was
transcribed from documentation and never exercised. available() lists the ids
shipped here, and only nodemaven is one of them - it is measured.
It does not retry. Retrying a refused request is the thing that most reliably makes the next one worse: measured over 1464 attempts, the chance the next attempt succeeds falls from 75% with no failures behind it to 1.6% after six, and 294 attempts spent past six failures returned three pages. A library that shipped automatic retry as a default would be spending that on your behalf without telling you. The table is in docs/observed-behavior.md.
It also does not own an HTTP client, a connection pool or a browser. Those are yours, and they are better than anything a vendor SDK would bundle.
- API reference - every public name, what it takes and what it raises
- Why the validation is the point - what the gateway answers to a wrong value, and why names are refused and values are not
- Observed behaviour - the gateway and dashboard findings this package is built on, each with its run
- CHANGELOG.md - entries carry the probe and the date behind any change to what the gateway is believed to accept
- NodeMaven docs - the product documentation
Python 3.9 or newer. No dependencies on 3.11 and newer; tomli on older ones.
MIT.