Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3a7455e
chore: clarify Sphinx doc build paths in gitignore
da1910 May 8, 2026
fd340fb
feat(common): vendor CaseInsensitiveDict and narrow types to httpx
da1910 May 8, 2026
233d824
feat(common): add RetryingHTTPTransport and httpx session utilities
da1910 May 8, 2026
b2d87d7
feat(common): migrate ApiClient, factory, and OIDC to httpx
da1910 May 8, 2026
bc3b3fa
test: migrate suite to pytest-httpx and httpx clients
da1910 May 8, 2026
37238ac
build: drop requests runtime deps and tune mypy for optional auth
da1910 May 8, 2026
4ad0cd9
docs: add httpx migration planning note
da1910 May 8, 2026
fedbc68
Tidy up docs (draft)
da1910 May 8, 2026
7fb7e11
docs: clarify IdP session header handling in OIDC and add test for fa…
da1910 May 8, 2026
85cf7c0
Remove _preload_content from ApiClient call_api path
da1910 May 8, 2026
bb58284
Replace SessionConfiguration proxies dict with proxy_url
da1910 May 8, 2026
7c03d33
docs: satisfy Vale and pydocstyle tooling
da1910 May 8, 2026
ab0957b
docs: drop httpx intersphinx; fix ApiClient numpydoc order; fix ruff …
da1910 May 8, 2026
154321f
Remove module link from README
da1910 May 8, 2026
c4a8933
chore: adding changelog file 1046.added.md [dependabot-skip]
pyansys-ci-bot May 8, 2026
e5d027c
feat(httpx): add AsyncApiClient and extend retry transport
da1910 May 8, 2026
864e3d2
test(integration): shared FastAPI fixtures and HTTP verb coverage
da1910 May 8, 2026
6637380
test(api): enhance tests for AsyncApiClient and OIDCSessionFactory
da1910 May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ _old/
build/
dist/

# autogenerated docs
# Documentation (Sphinx)
# Full build tree from doc/Makefile / doc/make.bat (html, latex, .doctrees, etc.)
doc/_build/

# autogenerated API stubs (autosummary), under doc/source when present
_autosummary


Expand Down
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ repos:
- id: pydocstyle
additional_dependencies: [tomli]
files: "^(src/)"
exclude: "_case_insensitive_dict\\.py$"
- id: pydocstyle
name: pydocstyle (vendored CaseInsensitiveDict)
additional_dependencies: [tomli]
files: "^src/ansys/openapi/common/_case_insensitive_dict\\.py$"
args: ["--convention=numpy", "--add-ignore=D105,D102"]

- repo: https://github.com/ansys/pre-commit-hooks
rev: v0.7.2
Expand Down
3 changes: 3 additions & 0 deletions .valeignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Generated paths — not hand-edited documentation prose.
doc/_build/
doc/source/api/_autosummary/
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ APIs, this Python library provides a common client to consume
HTTP APIs, minimizing overhead and reducing code duplication.

OpenAPI-Common supports authentication with Basic, Negotiate, NTLM,
and OpenID Connect. Most features of the underlying requests session
and OpenID Connect. Most features of the underlying ``httpx`` client
are exposed for use. Some basic configuration is also provided by default.

Dependencies
Expand Down
1 change: 1 addition & 0 deletions doc/changelog.d/1046.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DRAFT - Use httpx as the transport
2 changes: 1 addition & 1 deletion doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
# sphinx.ext.intersphinx
intersphinx_mapping = {
"python": ("https://docs.python.org/3.11", None),
"requests": ("https://requests.readthedocs.io/en/latest", None),
# httpx docs are MkDocs-based; they do not publish a Sphinx objects.inv.
"sphinx": ("https://www.sphinx-doc.org/en/master/", None),
}

Expand Down
101 changes: 51 additions & 50 deletions doc/source/user_guide/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
User guide
##########


Basic usage
-----------

Expand Down Expand Up @@ -77,119 +78,119 @@ Currently only the Authorization Code authentication flow is supported.

Session configuration
---------------------
You can set all options that are available in Python library *requests* through
the client with the :class:`~.SessionConfiguration` object. This enables you to
configure custom SSL certificate validation, send client certificates if your API
server requires them, and configure many other options.
The :class:`~.SessionConfiguration` class holds TLS settings, an optional outbound ``proxy_url``, headers, redirects, and
timeouts. The :class:`.ApiClientFactory` turns it into a synchronous HTTP client using ``httpx`` (retries and
timeouts are applied in OpenAPI-Common's transport layer), which backs each :class:`.ApiClient`.

Use it to configure custom certificate validation, send client certificates if your API
server requires them, and adjust other transport options.

For example, to send a client certificate with every request:

.. code:: python

>>> from ansys.openapi.common import SessionConfiguration
>>> from ansys.openapi.common import ApiClientFactory, SessionConfiguration
>>> configuration = SessionConfiguration(
... client_cert_path='./my-client-cert.pem',
... client_cert_key='secret-key'
... client_cert_key='secret-key',
... )
>>> client.configuration = configuration
>>> client = ApiClientFactory(
... 'https://my-api.com/v1.svc',
... session_configuration=configuration,
... ).with_anonymous().connect()
<ApiClient url: https://my-api.com/v1.svc>


HTTPS certificates
------------------

It is common to use a private CA in an organization to generate TLS certificates for internal resources. The
``requests`` library uses the ``certifi`` package which contains public CA certificates only, which means ``requests``
cannot verify private TLS certificates in its default configuration. The following error message is typically displayed
if a private TLS certificate is validated against the ``certifi`` public CAs:
It is common to use a private CA in an organization to generate TLS certificates for internal resources. By
default, **httpx** verifies server certificates using the same **certifi** CA bundle that many Python HTTP
stacks use: it contains public roots only, so a server certificate issued by a private CA is not trusted unless
you add trust material (private CA file, merged bundle, or system-store integration).

If verification fails, Python typically surfaces ``ssl.SSLCertVerificationError``, sometimes wrapped by
**httpx** in a ``httpx.ConnectError`` when opening the TLS connection. For example:

.. code:: text

requests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443): Max retries exceeded with url: /
(Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable
to get local issuer certificate (_ssl.c:1028)')))``
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed:
unable to get local issuer certificate (_ssl.c:1000)

If you encounter this error message, you should provide ``requests`` with the CA used to generate your private TLS
certificate. There are three recommended approaches to doing this, listed below in the order of simplicity.
If you see this, point OpenAPI-Common at the CA that signed the server certificate (or a bundle that includes
it), using one of the options below.


1. `pip-system-certs`_
~~~~~~~~~~~~~~~~~~~~~~

The ``pip-system-certs`` library patches the certificate loading mechanism for ``requests`` to use the system
certificate store instead of the ``certifi`` store. Assuming the system certificate store includes the private CA, no
further action is required beyond installing ``pip-system-certs`` in the same virtual environment as this package.
The ``pip-system-certs`` package patches **certifi** so the default CA bundle reflects your operating system’s
certificate store instead of the bundled Mozilla list alone. Because **httpx** uses that default bundle for
verification unless you override it, installing ``pip-system-certs`` in the same environment as this library
often resolves trust for corporate CAs that are already in the system store.

.. warning::

The change to ``requests`` affects every package in your environment, including pip. You **must** use a virtual
environment when using ``pip-system-certs`` to avoid unexpected side-effects in other Python scripts.

This is recommended approach for Windows and Linux users. However, there are some situations in which
``pip-system-certs`` cannot be used:

* Your platform is not supported by ``pip-system-certs``.
* The private CA certificate has not been added to the system certificate store.
* The OpenSSL deployment used by Python is not configured to use the system certificate store (common when using
conda-provided Python).
Changing **certifi**’s behaviour affects other libraries in that environment that rely on the same process-wide
patching, including package installers. Use a **virtual environment** when enabling ``pip-system-certs`` to
avoid unintended side effects outside your project.

In these cases, the ``SSLCertVerificationError`` is still raised. Instead, provide the appropriate CA certificate to
``requests`` directly.
This is the recommended approach for Windows and Linux when ``pip-system-certs`` is supported. It does **not** help
when the private CA is not in the system store, or when your Python build does not load the system store for
OpenSSL (common with some conda layouts). In those cases, pass a CA file or bundle explicitly (sections 2 and 3).


2. System CA certificate bundle (Linux only)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The :class:`~.SessionConfiguration` object allows you to provide a path to a file containing one or more CA
certificates. The custom CA certificate file is used instead of the ``certifi`` package to verify the service's TLS
certificate.
certificates. The file is used for TLS verification instead of the default **certifi** bundle.

If you need to authenticate both internally- and publicly signed TLS certificates within the same environment, you must
use a CA bundle which contains both the internal and public CAs used to sign the TLS certificates.
If you need to validate both internal and public TLS endpoints in the same process, use a single bundle that
concatenates the internal CA(s) and the public roots you care about.

.. note::

OIDC authentication often requires validating internally- and publicly signed TLS certificates, since both internal
and public resources are used to authenticate the resource.
OIDC flows often involve both internal and public endpoints, so a merged bundle may be required.

CA bundles are often provided by Linux environments which include all trusted public CAs and any internal CAs added to
the system certificate store. These are available in the following locations:
CA bundles are often available on Linux machines that combine public and locally trusted anchors, for example:

* Ubuntu: ``/etc/ssl/certs/ca-certificates.crt``
* SLES: ``/var/lib/ca-certificates/ca-bundle.pem``
* RHEL/Rocky Linux: ``/etc/pki/tls/cert.pem``

For example, to use the system CA bundle in Ubuntu, use the following:
For example, on Ubuntu:

.. code:: python

from ansys.openapi.common import SessionConfiguration

config = SessionConfiguration(cert_store_path="/etc/ssl/certs/ca-certificates.crt")

This allows ``requests`` to correctly validate both internally and publicly signed TLS certificates, as long as the
internal CA certificate has been added to the system certificate store. If the internal CA certificate has not been
added to the system certificate store, then a ``SSLCertVerificationError`` is still raised, and you should proceed to
the next section.
This lets the **httpx** client validate chains signed by CAs present in that bundle, provided the issuing CA for
your service is included. If your internal CA is not in the system bundle, continue to section 3.


3. Single CA certificate
~~~~~~~~~~~~~~~~~~~~~~~~

If you only need to authenticate internal TLS certificates, you can provide a path to the specific internal CA
certificate to be used for verification:
If you only need to trust a dedicated internal issuing CA, pass its certificate (PEM) as ``cert_store_path``:

.. code:: python

from ansys.openapi.common import SessionConfiguration

config = SessionConfiguration(cert_store_path=/home/username/my_private_ca_certificate.pem)
config = SessionConfiguration(
cert_store_path="/home/username/my_private_ca_certificate.pem"
)

Where ``/home/username/my_private_ca_certificate.pem`` is the path to the CA certificate file.
where ``/home/username/my_private_ca_certificate.pem`` is the path to the PEM file.

.. note::

The ``cert_store_path`` argument overrides the ``certifi`` CA certificates. Providing a single private CA certificate
causes ``requests`` to fail to validate publicly signed TLS certificates.
When ``cert_store_path`` is set, that file **replaces** the default **certifi** bundle for verification. A PEM
that contains only your private CA **does** not validate publicly issued sites unless those roots are also
included in the same file or you use one of the other strategies described earlier in this section.


.. _pip-system-certs: https://gitlab.com/alelec/pip-system-certs
4 changes: 4 additions & 0 deletions doc/styles/config/vocabularies/ANSYS/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ pip\-system\-certs
HTTPS
CA
CAs
certifi
conda
httpx
OpenSSL
65 changes: 44 additions & 21 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ classifiers = [
"Programming Language :: Python :: 3.14",
]
dependencies = [
"requests>=2.26",
"requests-negotiate-sspi>=0.5.2,<0.6; sys_platform == 'win32'",
"requests-ntlm>=1.1,<2.0",
"pyparsing>=3.2,<3.4",
"python-dateutil>=2.9",
"httpx>=0.27",
"httpx-ntlm>=1.4.0",
"httpx-negotiate-sspi>=0.28,<0.29; sys_platform == 'win32'",
]

[dependency-groups]
Expand All @@ -51,13 +51,12 @@ dev = [
"uvicorn",
"fastapi",
"pydantic",
"requests-mock",
"pytest-httpx>=0.35",
"pytest-mock",
"covertable",
"mypy>=1.8.0",
"types-requests",
"types-python-dateutil",
"requests_auth",
"httpx-auth>=0.22",
"keyring",
"sphinx-design>=0.6.0"
]
Expand All @@ -75,11 +74,14 @@ dev-linux = [

[project.optional-dependencies]
oidc = [
"requests_auth>=8.0.0,<9.0.0",
"keyring>=22,<26"
"httpx-auth>=0.22",
"keyring>=22,<26",
]
# GSSAPI/Kerberos via python-gssapi is Linux-oriented. Windows Negotiate uses SSPI:
# ``httpx-negotiate-sspi``; do not add ``httpx-gssapi`` for win32.
# Validation: optional Negotiate integration tests (Linux) and manual SSPI checks (Windows).
linux-kerberos = [
"requests-kerberos>=0.13,<0.16; sys_platform == 'linux'"
"httpx-gssapi>=0.6,<0.7; sys_platform == 'linux'"
]

[tool.hatch.build.targets.wheel]
Expand Down Expand Up @@ -127,6 +129,21 @@ explicit_package_bases = true
mypy_path = "$MYPY_CONFIG_FILE_DIR/src"
namespace_packages = true

# Third-party auth: optional Linux extras and untyped packages. Suppressing only
# ``import-untyped`` in the modules that import them keeps Windows CI (and
# ``warn_unused_ignores``) stable without ``# type: ignore`` on each import line.
[[tool.mypy.overrides]]
module = "httpx_gssapi"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "ansys.openapi.common._session"
disable_error_code = ["import-untyped"]

[[tool.mypy.overrides]]
module = "ansys.openapi.common._oidc"
disable_error_code = ["import-untyped"]

[tool.towncrier]
package = "ansys.openapi.common"
directory = "doc/changelog.d"
Expand All @@ -137,13 +154,13 @@ title_format = "`{version} <https://github.com/ansys/openapi-common/releases/tag
issue_format = "`#{issue} <https://github.com/ansys/openapi-common/pull/{issue}>`_"

[[tool.towncrier.type]]
directory = "added"
name = "Added"
directory = "breaking"
name = "Breaking"
showcontent = true

[[tool.towncrier.type]]
directory = "changed"
name = "Changed"
directory = "added"
name = "Added"
showcontent = true

[[tool.towncrier.type]]
Expand All @@ -152,26 +169,32 @@ name = "Fixed"
showcontent = true

[[tool.towncrier.type]]
directory = "dependencies"
name = "Dependencies"
directory = "documentation"
name = "Documentation"
showcontent = true

[[tool.towncrier.type]]
directory = "miscellaneous"
name = "Miscellaneous"
directory = "dependencies"
name = "Dependencies"
showcontent = true

[[tool.towncrier.type]]
directory = "documentation"
name = "Documentation"
directory = "maintenance"
name = "Maintenance"
showcontent = true

[[tool.towncrier.type]]
directory = "maintenance"
name = "Maintenance"
directory = "miscellaneous"
name = "Miscellaneous"
showcontent = true

[[tool.towncrier.type]]
directory = "test"
name = "Test"
showcontent = true

[[tool.towncrier.type]]
directory = "changed"
name = "Changed"
showcontent = true

Loading
Loading