Repository navigation
Reuse HTTPS connections, negotiate HTTP/2, add new response fields - #35
Merged
Merged
Conversation
Replace the per-instance requests.Session with one httpx.Client so that every IPData instance keeps a persistent connection pool (20 connections, 60 s keep-alive) for its whole life and multiplexes requests over HTTP/2 when api.ipdata.co negotiates it, falling back to HTTP/1.1 keep-alive otherwise. A first request to the API costs a TCP + TLS 1.3 handshake (roughly 660 to 980 ms); a request on a reused connection costs about a third of that, so clients should be created once and reused. - IPData.__init__ keeps its signature and gains http2=True and transport=None keyword arguments; the latter injects any httpx.BaseTransport (httpx.MockTransport in the new offline tests). When no transport is injected httpx builds its default one, which honours HTTPS_PROXY/NO_PROXY and SSL_CERT_FILE like requests did and applies the pool limits; an explicit HTTPTransport would have ignored both, so the connection-level `retries` knob is not used and connect failures are retried by the retry loop below instead. - retry_limit and retry_backoff_factor were dead: the retrying adapter was mounted on the prefix "http" which requests never selects. They now work as the docstring always described: GET and POST retry on 500, 502, 503 and 504 and on httpx.TransportError (reset, EOF or timeout on a stale pooled connection) after sleeping retry_backoff_factor * 2 ** (attempt - 2) seconds, i.e. 0.5, 1, 2, 4, 8, 16 and 32 s by default. 429 is retried only when the response has a Retry-After header (sleep min(Retry-After, 60 s)); otherwise it is returned at once so an exhausted quota fails fast. - bulk() now uses the client timeout; it sent its POST without one. - Add close(), __enter__ and __exit__ to release the pool. - The User-Agent becomes ipdata-python/<version> using importlib.metadata (fallback "unknown") and requests send Accept: application/json. - geofeeds.py downloads with httpx.get(..., follow_redirects=True), as requests followed redirects by default. - Dependency: requests -> httpx[http2]>=0.28,<1 in setup.cfg and requirements.txt. Version 4.0.9 -> 4.1.0. - CI: the workflow gains a pull_request trigger and a Python 3.9-3.13 matrix running `python -m pytest -m "not api"`; the existing job (full suite with the API key secret, build, tag-gated publish) keeps running on pushes to master and tags only. - Tests: offline tests through httpx.MockTransport cover lookup, bulk (timeout present), the retry schedule with a patched sleep, 429 fast-fail and Retry-After, close/context manager, user-agent and api-key parameters, transport reuse across calls and the module-level default client. The four live tests stay marked `api`. - README: new "Connection reuse and HTTP/2" section and offline test instructions. Claude-Session: https://claude.ai/code/session_01VQiRp4zEJKUXHrKkFaeUke Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The API now returns three additions: the top-level booleans is_satellite and is_anycast (siblings of is_eu, included only when true and omitted otherwise) and, on each threat.blocklists entry, the optional first_seen and last_seen UTC calendar dates (YYYY-MM-DD strings) and the days_seen integer. Some feeds, such as ipdata's own vpn feed, omit all three. The library is untyped and passes the JSON through unchanged, so the code change is to accept the two new names in IPData.valid_fields, which validates the `fields` and `select_field` parameters and feeds the CLI's -f / -e options. The dates stay strings and are not parsed. Tests built from the specification samples cover a satellite response, an anycast response with dated blocklist entries, a VPN exit whose third blocklist entry has no dates, the same shapes inside a bulk response, the None-when-absent behaviour of DotDict for the two flags, pass-through of unknown keys and field-name validation for both names. The README documents the five fields and the present-only-when-true rule. Claude-Session: https://claude.ai/code/session_01VQiRp4zEJKUXHrKkFaeUke Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The first run of the new pull_request matrix failed for Python 3.9
before any test ran: ubuntu-latest now resolves to the Ubuntu 26.04
image, and the actions/python-versions manifest has no 3.9 build for
it ("The version '3.9' with architecture 'x64' was not found for
Ubuntu 26.04"). 3.9 builds exist for 22.04 and 24.04 only. Python
3.10 to 3.13 passed on 26.04.
Pin the test matrix to ubuntu-24.04, where all five versions have
builds. The build-n-publish job pins Python 3.10.4, which also has no
26.04 build (only 20.04, 22.04 and 24.04), so it would fail at setup
on the next push to master or tag; pin its runner to ubuntu-24.04 too,
leaving its steps and the tag gate untouched.
Claude-Session: https://claude.ai/code/session_01VQiRp4zEJKUXHrKkFaeUke
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Every
IPDatainstance now owns one persistent HTTPS connection pool (anhttpx.Client; 20 connections, 60 s keep-alive) and multiplexes its requests over HTTP/2 when api.ipdata.co negotiates it, falling back to HTTP/1.1 keep-alive otherwise, so the library no longer pays a TCP + TLS handshake per call.requestsis replaced byhttpx[http2]>=0.28,<1. When notransportis injected, the client lets httpx build its default transport, so the pool limits,http2, the proxy environment variables andSSL_CERT_FILEall apply; the httpcore-levelretriesknob is not used, and connection failures (httpx.ConnectError,ConnectTimeout) areTransportErrors handled by the library's own retry loop with the documented schedule, which avoids two retry layers compounding into 8 x 8 connection attempts for an unreachable host. Neither TCP keepalive nor HTTP/2 PING frames are configured; the idle policy is the 60 s pool expiry. The new offline tests live insrc/ipdata/test_client.pyandsrc/ipdata/test_response_fields.py;src/ipdata/test_ipdata.pykeeps the four live tests markedapiunchanged.Commits:
7c24765Reuse HTTPS connections and negotiate HTTP/2 per IPData instance: onehttpx.Clientper instance,http2=Trueandtransport=Noneconstructor keywords, working retries,close()/ context manager,bulk()timeout,ipdata-python/<version>User-Agent plusAccept: application/json,httpx.get(..., follow_redirects=True)in geofeeds, dependency and version (4.1.0) changes, CI matrix with apull_requesttrigger, offline tests, README section "Connection reuse and HTTP/2".0932516Add is_satellite, is_anycast and blocklist date fields: both names joinIPData.valid_fields, fixtures built from the API's sample responses, README fields documentation.b968cc2Run CI on ubuntu-24.04 so Python 3.9 and the pinned 3.10.4 resolve: follow-up after the first PR run failed for 3.9 atactions/setup-python.ubuntu-latestnow resolves to the Ubuntu 26.04 image, and the actions/python-versions manifest has no Python 3.9 build for it (3.9.20 to 3.9.25 exist for 22.04 and 24.04 only) and no build of the3.10.4the publish job pins (20.04, 22.04 and 24.04 only), so both jobs are pinned toruns-on: ubuntu-24.04. The runner image is the only change to the publish job; its Python pin, steps and tag gate are untouched.CI: besides the new matrix job, the existing
build-n-publishjob gainedif: github.event_name == 'push'so that pull requests do not run the live tests or the build; it still runs the full suite with the secret on pushes tomasterand tags, and publishing stays gated onstartsWith(github.ref, 'refs/tags').What changes for users
IPDatainstance keeps its connections alive and reuses them. Create one client per process (or per API key or endpoint), reuse it for every lookup, and callclose()or usewith IPData(...) as client:at shutdown. Instances are thread-safe (the CLIbatchcommand already shares one across its thread pool and now multiplexes over one HTTP/2 connection). Create clients after forking; never share one acrossos.fork()/multiprocessingboundaries. The module-levelipdata.lookup()/ipdata.bulk()helpers keep their process-wideipdata.default_client, created on first use and living until the process exits.http2=True(setFalsefor HTTP/1.1 keep-alive only) andtransport=None(inject anyhttpx.BaseTransport, for examplehttpx.MockTransportin tests). All existing positional and keyword arguments keep working. When a transport is injected, httpx 0.28 applies neither the pool limits nor theHTTPS_PROXY/NO_PROXYenvironment variables to it, so configure those on the transport itself.retry_limitandretry_backoff_factornow do what the docstring promised. They were dead: the retrying adapter was mounted on the prefix"http", whichrequestsnever selects (it matches on"http://"/"https://"). GET and POST requests (includingPOST /bulk) now retry on HTTP 500, 502, 503, 504 and on transport errors (reset, EOF or timeout on a stale pooled connection, including a transport error while the body of a non-streaming response is being read) with sleeps ofretry_backoff_factor * 2 ** (attempt - 2)seconds: 0.5, 1, 2, 4, 8, 16, 32 s for the defaults.httpx.UnsupportedProtocolis not retried, since a misconfigured endpoint scheme can never succeed. A 429 is retried only when the response carriesRetry-After(sleepingmin(Retry-After, 60 s)); otherwise it is returned immediately withstatus == 429, exactly as the library has always returned non-200 responses, so an exhausted quota fails fast and no new exception type is introduced (ipdata initkeeps printingresponse.messageon failure). After the retries are exhausted the last response is returned with itsstatus, or the transport error is raised asIPDataExceptionas before.bulk()now uses the client timeout (default 60 s); it previously sent its POST with no timeout at all.ipdata-python/<installed version>(ipdata-python/unknownfrom an uninstalled source checkout) and requests sendAccept: application/json.Geofeeddownloads follow redirects, as they did withrequests.requestsis gone,httpx[http2]>=0.28,<1(withh2) is required. Behavioural differences inherited from httpx: a custom CA bundle must be supplied throughSSL_CERT_FILE/SSL_CERT_DIR(httpx does not readREQUESTS_CA_BUNDLE);HTTPS_PROXY/NO_PROXYkeep working;timeoutmust be a number,None, or anhttpx.Timeout(a(connect, read)tuple, whichrequestsaccepted, now raises at construction).is_satelliteandis_anycast(top-level booleans, present only when true, otherwise absent and thereforeNonethrough dot notation) and the optionalfirst_seen,last_seen(YYYY-MM-DDstrings, UTC) anddays_seen(integer) attributes ofthreat.blocklistsentries. Both flags are accepted byfields=/select_field=and by the CLI-f/-eoptions.DotDict,IPDataException, the validation helpers, the CLI commands and the module-level API are untouched. Internally, the privaterequests-based attribute is replaced by the private_client(anhttpx.Client).Release notes
Version
4.1.0(setup.cfg). Tag to push after merge:4.1.0(the tag push triggers the existing PyPI publish job).Verification
Local,
python -m pytest -m "not api"with the package installed editable andrequestsuninstalled (the 4 deselected tests are the live tests markedapi, which need an API key):master:50 passed, 4 deselected7c24765:79 passed, 4 deselected0932516, and again before the CI follow-up push:87 passed, 4 deselectedon Python 3.13.18ipdata-python/unknownUser-Agent fallback is exercised):87 passed, 4 deselectedon eachast.parse(source, feature_version=(3, 9))on every changed module: OKA request to api.ipdata.co from an
IPDataclient negotiated HTTP/2 and further requests on the same client reused the connection.CI, workflow "Test and Publish ipdata to PyPI" (
pull_requestevent added by this PR):0932516, https://github.com/ipdata/python/actions/runs/41620641859: failure.Test (Python 3.10),3.11,3.12and3.13succeeded;Test (Python 3.9)failed inactions/setup-pythonbefore any test ran: "The version '3.9' with architecture 'x64' was not found for Ubuntu 26.04" (ubuntu-latestis now the Ubuntu 26.04 image, and the python-versions manifest has no 3.9 build for it).Build and publish to PyPIskipped, as intended on pull requests.b968cc2, https://github.com/ipdata/python/actions/runs/41621069854: success.Test (Python 3.9),3.10,3.11,3.12and3.13all passed (python -m pytest -m "not api");Build and publish to PyPIskipped on the pull request as intended.Noticed, not changed
bulk()accepts asetof resources in its validation butjson=cannot serialise a set, so a set has always ended inIPDataException("Error when looking up ... Object of type set is not JSON serializable"); same with httpx.list(resources)would fix it.DotDict.__getattr__wraps every list element inDotDict, so dot access to a list of scalars (data.xwhenx == [1, 2, 3]) raisesTypeError; API lists hold objects so this does not surface today.batch --exclude:fields = set(ipdata.valid_fields)...runs beforeipdata = IPData(...)is assigned in the same function, soipdata batch -e <field>raisesUnboundLocalError.batchCSV output: thethreatsub-field list omitsis_vpn(and now the blocklist dates), sothreat_is_vpnis dropped from CSV rows.--output-format CSV; the option is--format.test_bulk_v4/test_bulk_v6comparepair[0]with itself, so they never check the returned IPs; all four live tests run 100 hypothesis examples each against the live API on every push tomaster.3.10.4(a 2022 patch release) and installsbuildwith--user; it now runs onubuntu-24.04, where that version still exists, but the pin should eventually move to"3.10"or newer.actions/checkout@v4andactions/setup-python@v5are Node 20 actions, which GitHub flags as deprecated on its runners.timeoutis still passed through the constructor only; there is no per-call timeout, as before.🤖 Generated with Claude Code