Skip to content

update httpx requirement from >=0.25,<0.28 to >=0.25,<0.29#120

Open
vlouvet wants to merge 31 commits into
jacobsvante:mainfrom
vlouvet:main
Open

update httpx requirement from >=0.25,<0.28 to >=0.25,<0.29#120
vlouvet wants to merge 31 commits into
jacobsvante:mainfrom
vlouvet:main

Conversation

@vlouvet

@vlouvet vlouvet commented Sep 18, 2025

Copy link
Copy Markdown
Contributor

This PR would address the httpx package pinning to allow using the latest minor version.

Also update the settings file to make testing easier.

Finally, update the pyodbc package name to resolve a failure of 'poetry check'

add REST API example in Programmatic use section
* updated settings to add test specific parameters

* updated httpx package pinning to allow newer versions

* fixed pyodbc package name to pass poetry check
@codecov

codecov Bot commented Sep 18, 2025

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 37.02%. Comparing base (38bc66c) to head (0f63e9f).
⚠️ Report is 30 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #120      +/-   ##
==========================================
+ Coverage   35.76%   37.02%   +1.26%     
==========================================
  Files          25       25              
  Lines        1071     1083      +12     
==========================================
+ Hits          383      401      +18     
+ Misses        688      682       -6     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

* move wsdl version to a config property
@vlouvet

vlouvet commented Oct 15, 2025

Copy link
Copy Markdown
Contributor Author

@iloveitaly please review

Comment thread .vscode/settings.json Outdated
Comment thread netsuite/soap_api/client.py
Comment thread netsuite/config.py Outdated
Comment thread pyproject.toml Outdated
@vlouvet vlouvet marked this pull request as draft January 23, 2026 04:20
@vlouvet vlouvet marked this pull request as ready for review January 23, 2026 04:24
@vlouvet

vlouvet commented Jan 23, 2026

Copy link
Copy Markdown
Contributor Author

@iloveitaly The recommended / commented changes have been resolved at this time. please let me know if there is anything else you have questions on.

@vlouvet vlouvet requested a review from iloveitaly January 23, 2026 04:27
vlouvet and others added 10 commits January 22, 2026 21:55
…DL version. change unscoped for upgrade to httpx
…vante#45) (#4)

`WebServiceCall` checked `isinstance(response, zeep.xsd.ComplexType)`,
but `ComplexType` is the schema-definition class — runtime SOAP responses
are `zeep.xsd.CompoundValue` instances. The check was always False, so
status validation and `extract` were silently skipped on every call.

The decorator was also a synchronous wrapper around async SOAP methods,
so even with the right isinstance check it would have processed the
coroutine object rather than the awaited response.

Switch the isinstance check to `CompoundValue`, and detect coroutine
functions at decoration time so async methods are awaited inside the
wrapper. Add regression tests covering the success/error/extract/default
paths plus the sync code path.

Closes jacobsvante#45

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…svante#72) (#5)

Adds substantive unit tests for `NetSuiteSoapApi`:

- WSDL URL construction for sandbox vs production accounts, custom
  versions, and explicit `wsdl_url` overrides
- `AsyncNetSuiteTransport._fix_address` munges the default WSDL host to
  the account-specific subdomain
- Cache injection (default `SqliteCache` vs custom `InMemoryCache`)
- `TokenPassport` signature determinism and message format, and that
  `passport.make` raises for username/password auth
- Argument shaping for every public SOAP method (`get`, `getList`,
  `getAll`, `add`, `update`, `upsert`, `search`, `searchMoreWithId`,
  `getItemAvailability`) by mocking `request` and the type factories
- `request` merges `generate_passport()` output with `additionalHeaders`
- `to_builtin` delegates to `zeep.helpers.serialize_object`
- `with_timeout` proxies to `transport.settings(timeout=...)`
- `_ensure_required_dependencies` raises a `RuntimeError` mentioning the
  `soap_api` extra when zeep is missing

Closes jacobsvante#72

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…acobsvante#42) (#6)

NetSuite's SuiteQL REST API caps a single response at limit=1000 and a
single query at 100,000 rows. Users had to wire up the `next` link
themselves — the issue was that `suiteql()` always re-encoded `limit`
and `offset` in `params`, which would clash with the offset embedded in
the absolute `next` URL.

Add `NetSuiteRestApi.suiteql_paginated`: an async generator that runs
the initial query, then follows `links[rel=next]` (POSTing the same
body to the absolute URL) until `hasMore` is False or no next link is
present.

The original `suiteql` docstring now explains both the ORDER-BY zero-row
quirk (jacobsvante#29) and the >100k partitioning workaround. README/docs also gain
a pagination section.

Closes jacobsvante#42

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e#29) (#7)

NetSuite's SuiteQL REST endpoint has a known quirk where a query with
`ORDER BY` and a small `limit` (the default 10) silently returns zero
rows. Users hit this and assume the library is broken — see
jacobsvante#29 where `SELECT id, name FROM subsidiary ORDER BY id`
returns no items.

We can't fix the NetSuite-side bug, but we can stop users from losing
hours debugging it: log a warning whenever `ORDER BY` is detected in the
query string and `limit < 1000`, pointing them at the workaround
(raise the limit, or sort client-side).

Detection uses a word-boundary regex so substrings like `order_by_id`
don't trigger false positives. Documented in the `suiteql` docstring.

Closes jacobsvante#29

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oder (#8)

Adds 100 new tests across 8 files. Coverage gains by module:

  config.py             59% -> 100%
  exceptions.py         57% -> 100%
  json.py               46% ->  94%
  rest_api_base.py      46% -> 100%
  restlet.py            70% -> 100%
  soap_api/decorators   88% -> 100%
  soap_api/passport     77% -> 100%
  soap_api/transports   88% -> 100%
  cli/helpers            0% ->  92%
  cli/main               0% ->  55%
  cli/misc               0% -> 100%
  cli/rest_api           0% ->  51%
  cli/restlet            0% ->  63%
  cli/soap_api           0% ->  69%

  TOTAL                 46% ->  75%

Also fixes a 5-year-old bug in `netsuite.json._orjson_default`: it called
`_get_encoder(obj)` then invoked the result, but `_get_encoder` already
returns the *encoded value*, not the encoder. Any payload containing a
Decimal, bytes, set, frozenset, Path, or timedelta would crash with
`'str' object is not callable` when orjson is installed. The fix removes
the redundant call. Surfaced while writing tests for `nsjson.dumps`.

CLI tests gate on `pytest.importorskip("pkg_resources")` so they skip
cleanly on Python 3.14 / setuptools 81+ (which dropped `pkg_resources`).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(ci): trim workflows for unofficial-fork posture

Drop the publishing/distribution machinery the upstream uses to ship to
PyPI and gh-pages — this fork is a friendly fork that does not publish a
package. What remains is the dev-loop automation: tests, style, types,
coverage.

- Delete `.github/workflows/cd.yml` (release-please + `poetry publish`).
- Delete `.github/workflows/docs.yml` (mkdocs -> gh-pages deploy). The
  in-repo `docs/` markdown still renders on github.com.
- `ci.yml` and `codecov.yml`: collapse upstream's 75-cell matrix
  (5 Pythons x 5 extras x 3 OSes) to a single combination — Python 3.12
  on ubuntu-latest with `--extras all`. Forks don't need the full
  compatibility surface; one combination is enough to catch regressions
  on every PR.
- Drop the `.tool-versions` / asdf-parse-action plumbing since the matrix
  no longer reads versions from there.

Also reformat the test files added in #8 to satisfy the `style` job
(black + isort). Production code is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(deps): add pytest-asyncio dev-dep so async tests can run

The async test files added in #5/#6/#7/#8 (tests/test_rest_api.py,
test_soap_api.py, test_rest_api_base.py, test_restlet.py,
test_soap_api_decorators*.py, test_soap_api_transports.py) all use
`@pytest.mark.asyncio`, but the dev-dependency was never declared in
pyproject.toml. The local development environment had pytest-asyncio
installed transitively, so `pytest -q` showed 149 passing — masking
the gap until CI was actually exercised.

This PR (chore/fork-ci-config) is the first one to enable a working CI
matrix on the fork, so it's where the failure surfaces. CI runs
`poetry install --extras all` from a clean state and then `pytest`,
which collects the async tests but has no plugin to drive them, fails
collection on all 51 with:

  Failed: async def functions are not natively supported.
  You need to install a suitable plugin for your async framework, for
  example: anyio, pytest-asyncio, ...

Add `pytest-asyncio = "~0.23"` to `[tool.poetry.dev-dependencies]`
between pytest and pytest-cov.

Also pin `asyncio_mode = "strict"` in `[tool.pytest.ini_options]` to
match how the existing tests are written (each is explicitly marked
with `@pytest.mark.asyncio`) and to silence pytest-asyncio 0.23+'s
deprecation warning about leaving the mode unset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This is an unofficial fork that does not publish to PyPI, so the
existing "pip install netsuite" instructions never produce this fork's
code. Switch every install snippet to the git+https URL form.

- README.md: replace upstream's PyPI/CI/codecov badges with vlouvet
  equivalents, add an "unofficial fork" disclaimer, and rewrite the
  install section to use `pip install git+https://...`. Drop the
  upstream Slack/SuiteSync links since they belong to the original
  maintainer's project, not this fork.
- docs/index.md: same install rewrite. Note that the SOAP install line
  also now flags NetSuite's 2027.1 SOAP deprecation, so users picking
  the soap_api extra are at least warned at install time.
- mkdocs.yml: point repo_name/repo_url at vlouvet/netsuite so the
  docs theme's edit/source links resolve correctly.
- pyproject.toml: keep the original authors as the credit they're owed,
  add Vicente Louvet III, and point homepage/repository at the fork.
  Drop the now-incorrect `documentation` URL (no auto-deployed site
  for this fork; in-repo docs/ is canonical per #9).

No production code changes. No tests added — the install instructions
aren't exercised by the test suite.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ation warning (#11)

* feat: add OAuth 2.0 auth (Client Credentials JWT + Auth Code) and SOAP deprecation warning

NetSuite has announced SOAP Web Services will be removed in the 2027.1
release. This PR introduces the migration path: OAuth 2.0 against the
REST API, plus a runtime DeprecationWarning to surface the sunset.

Two flows, both centered on a shared `OAuth2BearerAuth` httpx auth
handler that the REST API and Restlet clients use transparently in
place of OAuth 1.0a TBA:

- **Client Credentials with JWT Bearer assertion** (M2M, RFC 7523).
  `OAuth2ClientCredentialsAuth` takes a `client_id`, `certificate_id`
  (the `kid` NetSuite assigns when you upload your public key), and a
  PEM-encoded private key. The handler signs short-lived JWTs and
  exchanges them at NetSuite's token endpoint, caching the resulting
  access token with a 60-second safety margin before refreshing.
  Default algorithm is PS256; PS/RS/ES at 256/384/512 are accepted.
- **Authorization Code Grant** is supported via two helper functions —
  `build_authorization_url()` and `exchange_authorization_code()` —
  that callers wire into their own redirect handlers. The resulting
  token is dropped into `OAuth2AccessTokenAuth` (bring-your-own).
  We don't refresh BYO tokens automatically; the calling app's auth
  layer is responsible for that.

`Config.auth` accepts the four auth types (TokenAuth,
OAuth2ClientCredentialsAuth, OAuth2AccessTokenAuth,
UsernamePasswordAuth). `RestApiBase._make_auth` dispatches on type.
The OAuth2 handler is cached on the API instance so token re-use works
across back-to-back requests.

`NetSuiteSoapApi.__init__` now emits a `DeprecationWarning` mentioning
the 2027.1 sunset and pointing users at `OAuth2ClientCredentialsAuth`.
The pyproject `filterwarnings` filter suppresses it inside the existing
test suite (which instantiates the SOAP client 60+ times) so it doesn't
clutter the pytest summary; a dedicated test in
`tests/test_soap_api_deprecation.py` re-enables warnings explicitly to
assert the behavior.

35 new tests across `tests/test_oauth2.py`,
`tests/test_oauth2_integration.py`, and
`tests/test_soap_api_deprecation.py`. Coverage of the new
`netsuite/oauth2.py` is 97%. Overall: 149 tests -> 184 tests, 75% ->
77% coverage.

`docs/index.md` gains a new "Authentication" section with worked
examples for all three flows (M2M JWT, Authorization Code, legacy TBA)
and an explicit callout for the 2027.1 SOAP sunset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(deps): declare joserfc as a direct dependency

netsuite/oauth2.py imports `joserfc` directly for JWT signing. Newer
authlib (1.7+) pulls joserfc in as a transitive, which is why local
test runs work, but the older end of our `authlib = ">=1,<3"` range
does not. CI's clean resolver picks one of those older authlibs and
fails at import time with:

  ModuleNotFoundError: No module named 'joserfc'

Direct usage means direct dependency. Pin loosely (`>=1,<2`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vlouvet and others added 12 commits May 8, 2026 09:38
For private repos, and for forks where Codecov tokenless uploads can be
flaky, the token has to be passed explicitly to `codecov/codecov-action@v5`.
The repo's `CODECOV_TOKEN` secret is read at run time and forwarded.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Imports the dependabot bump from upstream PR jacobsvante#109.
Black 25 introduces the 2025 stable style (PEP 639 license metadata,
trailing-comma normalization on typed function parameters, generic
function definition wrapping, etc.) but none of those rules trigger on
this codebase — `black --check .` is a no-op after the version bump,
so no reformat hunks are bundled into this commit.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Imports the dependabot bump from upstream PR jacobsvante#108.
pytest-cov 6 dropped Python 3.8 support; this fork already requires
Python >=3.9 in pyproject.toml so the floor is unchanged.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Imports the change from upstream PR jacobsvante#99 (merit-finns).

NetSuite explicitly informed integrators that SOAP web service WSDL
versions 2017.2 through 2021.1 are unsupported and that integrations on
those versions need to upgrade. Until our 2027.1 SOAP sunset arrives,
the lib's default WSDL needs to be one NetSuite still serves.

The `NetSuiteSoapApi(version=...)` kwarg still lets users pin to a
specific version when their account hasn't migrated yet.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…^80.9.0 (#13)

Imports the dependabot bump from upstream PR jacobsvante#117.
types-setuptools is type stubs only — no runtime impact.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ns (#20)

The deprecation warning and OAuth2 module docstring asserted SOAP Web
Services 'will be removed in the 2027.1 release.' The NetSuite 2026.1
SuiteTalk release notes make no such hard-date claim: removal is gradual,
2025.2 is the last *planned* SOAP endpoint, and older endpoints degrade to
unsupported over subsequent releases (later endpoints only released as
needed).

Soften the message and docstring to match the official wording and point
at the SOAP Removal Plans FAQ. The 'NetSuite has announced that SOAP Web
Services' prefix is preserved so the pyproject filterwarnings entry still
matches. Update test_soap_api_deprecation to assert 2025.2 instead of the
removed 2027.1 date.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The specs/ folder holds local reference material (NetSuite release-note
PDFs, scratch notes) that should not be tracked.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add convenience wrappers on NetSuiteRestApi for the four REST operations
introduced in the NetSuite 2026.1 release, mirroring the existing
suiteql/jsonschema/openapi helpers. All were already reachable via the
generic get/post/patch methods; these absorb the operation-specific URL
shapes and custom media-type headers:

- attach() / detach(): POST /record/v1/{t}/{id}/!attach|!detach/{tt}/{tid};
  optional contact "role" body; returns None on the 204 response. Internal
  and eid: external IDs both work.
- create_form(): POST .../!transform/{tt} with the
  "Accept: application/vnd.oracle.resource+json; type=create-form" header,
  returning a record prepopulated from a related record.
- select_options(): POST (new instance) or PATCH (existing, via record_id)
  with the type=select-options Accept header and a comma-joined "fields"
  param; dependent field values ride in the body.
- batch(): async homogeneous add/update/upsert (max 100) with the
  "Prefer: respond-async" + type=collection headers. Talks to
  _request_impl directly to surface the async job's Location header, which
  the JSON-only _request helper discards.

Covered by unit tests asserting method, URL, headers, params and body for
each operation.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The parametrized encoder test hardcoded Path("/tmp/x") -> "/tmp/x". The
encoder serializes Path via str(), which on Windows yields "\tmp\x", so the
test failed on windows-latest CI. Compare against str(Path(...)) so the
expected value matches per-OS (POSIX "/tmp/x" vs Windows "\\tmp\\x").

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…) works (#24)

build_client_assertion called jwt.encode(header, claims, key) without an
algorithms argument. joserfc's default registry only permits its
"recommended" algorithms (RS256/ES256/...) and raises
UnsupportedAlgorithmError for PS256 -- which is this module's
DEFAULT_ALGORITHM and NetSuite's recommended assertion algorithm. So the
default machine-to-machine flow failed at signing time; only callers
explicitly passing algorithm="RS256" worked (ES384/ES512 were affected too).

Whitelist the chosen algorithm in jwt.encode so every entry in
SUPPORTED_ALGORITHMS actually signs. Confirmed against a live NetSuite
sandbox: a PS256 assertion now signs and the M2M token endpoint accepts it.

Every existing assertion test pinned algorithm="RS256", so the default
PS256 path was never exercised. Add a regression test that builds an
assertion with no algorithm= and asserts it signs as PS256.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NetSuite answers a REST create with 204 No Content and a Location header
pointing at the new record, so the JSON-only _request helper (which returns
None on 204) gives callers no way to learn the assigned ID.

Add create_record(record_type, record_data): it POSTs to
/record/v1/{record_type} via _request_impl directly (the same pattern
batch() uses to reach the Location header), then returns the ID parsed from
that header -- an int for numeric IDs, the raw string for external IDs
(eid:...), or None when no Location is present.

This is a scoped reimplementation of the idea in PR jacobsvante#121. Unlike that PR it
does not mutate the generic _request to special-case every 204 POST (which
would silently change the return type of all POSTs), and the ID parser
ignores query strings/fragments and tolerates a trailing slash rather than
using a bare /([^/]+)$ regex that would capture a querystring.

Tested: collection-endpoint POST + body, numeric id -> int, external id ->
str, query/trailing-slash handling, missing-header -> None, error status
raises.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#26)

* test: raise coverage to 91% (CLI handlers, client facade, rest_api methods)

The suite sat at 78%, with the largest gaps in code that had no direct
tests: the CLI command handlers (cli/rest_api 51%, cli/restlet 63%,
cli/main 55%), the NetSuite facade (client.py 68%), and several plain
NetSuiteRestApi methods (the HTTP verbs, jsonschema, token_info, openapi).

Add focused unit tests that drive each surface without the network:

- test_cli_rest_api.py: builds the real argparse parser and invokes each
  subcommand's func(config, args) with a mocked NetSuite -- covers param
  assembly, payload/query-file reading, header parsing (incl. repeated and
  invalid headers), JSON encoding, the openapi-serve path (http.server.test
  mocked), and the RuntimeError->parser.error path. cli/rest_api 51%->99%.
- test_cli_restlet.py: same approach for the restlet handlers. 63%->100%.
- test_cli_main.py: drives main() for the per-section help shortcuts and a
  full async-subcommand run via both --config-environment and an ini file.
  55%->95%.
- test_client.py: the NetSuite cached_property accessors and option passing.
  68%->100%.
- test_rest_api.py: direct tests for request/get/post/put/patch/delete,
  jsonschema, token_info, openapi (with/without record types), batch body
  parsing, and the _make_url/_make_default_headers helpers. 81%->100%.

Net: 78% -> 91% (above the 90% target), 194 -> 235 tests, all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli): drop pkg_resources so the CLI imports on modern setuptools

netsuite/cli/misc.py did `import pkg_resources` at module top, only to read
the package version. setuptools 81+ removed pkg_resources, so on any modern
environment `import netsuite.cli.<anything>` raised ModuleNotFoundError
(cli/__init__ -> main -> misc) -- breaking the whole CLI, not just `version`.

Switch to the stdlib `importlib.metadata.version`, which has been the
recommended replacement since Python 3.8.

This also unblocks CI coverage: the CLI tests were guarded with
`pytest.importorskip("pkg_resources")` and silently skipped wherever
pkg_resources was absent (i.e. CI), and the new test_cli_rest_api /
test_cli_restlet modules errored at collection. With misc importable, drop
those guards so every CLI test actually runs. Verified with pkg_resources
uninstalled: 241 passed, 0 skipped, 0 collection errors, coverage 91%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants