Builds the proxy username a gateway expects, and refuses the input it would silently drop.
Quickstart · Sticky sessions · Why the validation is the point · What it does not do · Other gateways · Docs
Build and validate proxy connection strings.
This library 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.
pip install nodemaven
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.
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)No account? Any proxy you already have works. A gateway is a data description, not a code path, so one that ships no definition here goes through the same builder and the same validation:
import requests
from nodemaven import Proxy, Provider
# An empty known_params is not a stub. It says nobody has established what this
# gateway recognises, so every parameter is refused rather than sent to be
# silently dropped - see "Why the validation is the point" below.
mine = Provider(id="mine", label="My proxy", known_params=frozenset())
proxy = Proxy(provider=mine, login="your-login", password="your-password",
host="proxy.example.com", port=8000)
r = requests.get("https://api.ipify.org", proxies=proxy.requests())
print(r.text)Describe the parameters it does take and it validates those too - see Other gateways. Everything from here to the end of that section builds strings offline and opens no socket at all.
Credentials can come from the environment instead, so nothing is in your source:
# NODEMAVEN_LOGIN and NODEMAVEN_PASSWORD
proxy = Proxy(country="us", filter="medium")The same identity, for other clients:
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:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context(proxy=proxy.playwright())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 |
isp |
the exit's ISP | a name |
sid |
the sticky session - see below | any string with no - |
ttl |
how long that session is held | 10m, 1m |
filter |
IP quality | low, medium, high (2026-08-13) |
speed |
connection speed class | fast, slow (2026-08-12) |
ipv4 |
force IPv4 | True / False |
Names are validated. Values, on this gateway, are not. Passing a name that is not in this 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, which is the worse mistake of the two. The schema does carry a per-parameter list of legal values and refuses anything outside it; the shipped definition leaves that list empty for every parameter, deliberately, and a definition you write yourself gets the check as soon as you fill it in.
Credentials come from NODEMAVEN_LOGIN and NODEMAVEN_PASSWORD when not passed
in, and the gateway address from NODEMAVEN_HOST and NODEMAVEN_PORT.
All four inherit 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 the gateway's separator, or a value is outside a list the definition declares |
CredentialsError |
no login, no password, or no gateway address, from arguments or environment |
ProviderError |
a gateway definition is missing, unreadable, or internally inconsistent |
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.
Nothing here is raised from a response, because nothing here sends one. Every failure this library reports is a failure it found before a socket existed.
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.
That is measured and not a precaution: on 2026-08-20 a probe opened tunnels with
sid-order8e3bf9-4417 and with sid-order8e3bf9, four rounds each, interleaved,
and both landed on one exit address while a third arm spelled
sid-order8e3bf94417 held a different one throughout. The gateway cuts the value
at the separator and reads the rest as something else, so 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.
country=us, sid=A and country=us, sid=A, filter=medium are two different
sessions on the gateway, so adding or removing any parameter moves you to a
different exit address. That 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 identityA gateway is bad at telling you that you got the username wrong. Measured against this one on 2026-08-10, seven kinds of bad input produce seven different reactions and not one of them names the cause:
| you sent | the gateway answers |
|---|---|
| bad country | 406 Not Acceptable |
| bad region | 406 Not Acceptable |
| bad city | 500 Internal Server Error |
bad filter value |
407 Proxy Authentication Required |
bad ttl value |
407 Proxy Authentication Required |
| empty value | nothing, the connection hangs about 20 s |
| unknown parameter name | 200, and the parameter is ignored |
The two 407 replies send you to check credentials that are correct. The last
row is worse than any of them: the request succeeds, your code carries on, and
the setting you asked for was never applied. Nothing that comes back over the
wire can tell you.
So this library checks before anything is sent:
>>> Proxy(login="u", password="p", contry="us")
ParamError: NodeMaven does not know the parameter 'contry': 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']It does not retry. That is deliberate, and it is the one design decision here taken against a measurement rather than a preference.
Retrying a refused request is the thing that most reliably makes the next one worse: each retry confirms automation to the target and burns the exit range for everyone else sharing the pool. Measured over 1464 attempts, the chance that the next attempt succeeds, by how many failures came immediately before it:
| failures before | P(next attempt succeeds) |
|---|---|
| 0 | 75% |
| 1 | 21% |
| 3 | 5.9% |
| 5 | 5.8% |
| 6 | 1.6% |
| 7-9 | 0.5% |
294 attempts were spent past six consecutive failures and returned 3 pages - 98 attempts per delivered page, against 1.7 in a healthy session. A library that shipped automatic retry as a default would be spending that on your behalf without telling you.
Those 1464 attempts, and the cells they came from, are in nodemaven/proxy-benchmark - the harness that measured them, open source, so the table above can be re-run rather than believed.
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.
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. Either build it in place, as in the quickstart, or keep it 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. Only nodemaven is shipped
here, and it is measured.
Python 3.9 or newer. No dependencies on 3.11 and newer; tomli on older ones.
CHANGELOG.md. Entries carry the probe and the date behind any change to what the gateway is believed to accept.
MIT.