https://github.com/techouse/httpx_qs
HTTPX transport leveraging qs-codec for advanced query string encoding and decoding
https://github.com/techouse/httpx_qs
httpx httpx-transport python qs qs-codec query-string
Last synced: 21 days ago
JSON representation
HTTPX transport leveraging qs-codec for advanced query string encoding and decoding
- Host: GitHub
- URL: https://github.com/techouse/httpx_qs
- Owner: techouse
- License: bsd-3-clause
- Created: 2025-09-27T10:43:57.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2026-03-31T11:09:26.000Z (28 days ago)
- Last Synced: 2026-03-31T12:27:21.700Z (28 days ago)
- Topics: httpx, httpx-transport, python, qs, qs-codec, query-string
- Language: Python
- Homepage: https://techouse.github.io/httpx_qs/
- Size: 155 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.rst
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- Funding: .github/FUNDING.yml
- License: LICENSE
- Code of conduct: CODE-OF-CONDUCT.md
- Codeowners: .github/CODEOWNERS
- Security: SECURITY.md
Awesome Lists containing this project
README
httpx-qs
========
Smart, policy-driven query string merging & encoding for `httpx `_ powered by
`qs-codec `_.
.. image:: https://img.shields.io/pypi/v/httpx-qs?logo=pypi
:target: https://pypi.org/project/httpx-qs/
:alt: PyPI version
.. image:: https://img.shields.io/pypi/status/httpx-qs?logo=pypi
:target: https://pypi.org/project/httpx-qs/
:alt: PyPI - Status
.. image:: https://img.shields.io/pypi/dm/httpx-qs?logo=pypi&label=PyPI%20downloads
:target: https://pypistats.org/packages/httpx-qs
:alt: PyPI - Downloads
.. image:: https://img.shields.io/pypi/pyversions/httpx-qs?logo=python&label=Python
:target: https://pypi.org/project/httpx-qs/
:alt: Supported Python versions
.. image:: https://img.shields.io/badge/PyPy-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-6f42c1?logo=pypy
:target: https://www.pypy.org/
:alt: PyPy support status
.. image:: https://img.shields.io/pypi/format/httpx-qs?logo=python
:target: https://pypi.org/project/httpx-qs/
:alt: PyPI - Format
.. image:: https://github.com/techouse/httpx_qs/actions/workflows/test.yml/badge.svg
:target: https://github.com/techouse/httpx_qs/actions/workflows/test.yml
:alt: Tests
.. image:: https://github.com/techouse/httpx_qs/actions/workflows/github-code-scanning/codeql/badge.svg
:target: https://github.com/techouse/httpx_qs/actions/workflows/github-code-scanning/codeql
:alt: CodeQL
.. image:: https://img.shields.io/github/license/techouse/httpx_qs
:target: https://github.com/techouse/httpx_qs/blob/master/LICENSE
:alt: License
.. image:: https://codecov.io/gh/techouse/httpx_qs/graph/badge.svg?token=JMt8akIZFh
:target: https://codecov.io/gh/techouse/httpx_qs
:alt: Codecov
.. image:: https://app.codacy.com/project/badge/Grade/420bf66ab90d4b3798573b6ff86d02af
:target: https://app.codacy.com/gh/techouse/httpx_qs/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade
:alt: Codacy Quality
.. image:: https://img.shields.io/github/sponsors/techouse?logo=github
:target: https://github.com/sponsors/techouse
:alt: GitHub Sponsors
.. image:: https://img.shields.io/github/stars/techouse/httpx_qs
:target: https://github.com/techouse/httpx_qs/stargazers
:alt: GitHub Repo stars
.. image:: https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg?logo=contributorcovenant
:target: CODE-OF-CONDUCT.md
:alt: Contributor Covenant
.. |flake8| image:: https://img.shields.io/badge/flake8-checked-blueviolet.svg?logo=python
:target: https://flake8.pycqa.org/en/latest/
.. image:: https://img.shields.io/badge/mypy-checked-blue.svg?logo=python
:target: https://mypy.readthedocs.io/en/stable/
:alt: mypy
.. image:: https://img.shields.io/badge/linting-pylint-yellowgreen.svg?logo=python
:target: https://github.com/pylint-dev/pylint
:alt: pylint
.. image:: https://img.shields.io/badge/imports-isort-blue.svg?logo=python
:target: https://pycqa.github.io/isort/
:alt: isort
.. image:: https://img.shields.io/badge/security-bandit-blue.svg?logo=python
:target: https://github.com/PyCQA/bandit
:alt: Security Status
Overview
--------
``httpx-qs`` provides:
* A transport wrapper ``SmartQueryStrings`` that merges *existing* URL query parameters with *additional* ones supplied via ``request.extensions``.
* A flexible ``merge_query`` utility with selectable conflict resolution policies.
* Consistent, standards-aware encoding via ``qs-codec`` (RFC3986 percent-encoding, structured arrays, nested objects, etc.).
Why?
----
HTTPX already lets you pass ``params=`` when making requests, but sometimes you need to:
* Inject **additional** query parameters from middleware/transport layers (e.g., auth tags, tracing IDs, feature flags) *without losing* the caller's original intent.
* Combine repeated keys or treat them deterministically (replace / keep / error) rather than always flattening.
* Support nested data or list semantics consistent across clients and services.
``qs-codec`` supplies the primitives (decoding & encoding with configurable ``ListFormat``). ``httpx-qs`` stitches that into HTTPX's transport pipeline so you can declaratively extend queries at request dispatch time.
Requirements
------------
* CPython 3.8-3.14, including free-threaded 3.14t, or PyPy 3.8-3.11
* ``httpx>=0.28.1,<1.0.0``
* ``qs-codec>=1.5.0``
Installation
------------
.. code-block:: bash
pip install httpx-qs
Minimal Example
---------------
.. code-block:: python
import httpx
from httpx_qs.transporters.smart_query_strings import SmartQueryStrings
client = httpx.Client(transport=SmartQueryStrings(httpx.HTTPTransport()))
response = client.get(
"https://www.google.com",
params={"a": "b", "c": "d"},
extensions={"extra_query_params": {"c": "D", "tags": ["x", "y"]}},
)
print(str(response.request.url))
# Example (order may vary): https://www.google.com/?a=b&c=d&c=D&tags=x&tags=y
Using Merge Policies
--------------------
Conflict resolution when a key already exists is controlled by ``MergePolicy``.
Available policies:
* ``combine`` (default): concatenate values → existing first, new afterward (``a=1&a=2``)
* ``replace``: last-wins, existing value is overwritten (``a=2``)
* ``keep``: first-wins, ignore the new value (``a=1``)
* ``error``: raise ``ValueError`` on duplicate key
Specify per request:
.. code-block:: python
from httpx_qs import MergePolicy
r = client.get(
"https://api.example.com/resources",
params={"dup": "original"},
extensions={
"extra_query_params": {"dup": "override"},
"extra_query_params_policy": MergePolicy.REPLACE,
},
)
# Query contains only dup=override
Async Usage
-----------
``SmartQueryStrings`` works equally for ``AsyncClient``:
.. code-block:: python
import httpx
from httpx_qs.transporters.smart_query_strings import SmartQueryStrings
async def main() -> None:
async with httpx.AsyncClient(transport=SmartQueryStrings(httpx.AsyncHTTPTransport())) as client:
r = await client.get(
"https://example.com/items",
params={"filters": "active"},
extensions={"extra_query_params": {"page": 2}},
)
print(r.request.url)
# Run with: asyncio.run(main())
``merge_query`` Utility
-----------------------
You can use the underlying function directly:
.. code-block:: python
from httpx_qs import merge_query, MergePolicy
from qs_codec import EncodeOptions, ListFormat
new_url = merge_query(
"https://example.com?a=1",
{"a": 2, "tags": ["x", "y"]},
options=EncodeOptions(list_format=ListFormat.REPEAT),
policy=MergePolicy.COMBINE,
)
# → https://example.com/?a=1&a=2&tags=x&tags=y
Why ``ListFormat.REPEAT`` by Default?
-------------------------------------
``qs-codec`` exposes several list formatting strategies (e.g. repeat, brackets, indices). ``httpx-qs`` defaults to
``ListFormat.REPEAT`` because:
* It matches common server expectations (``key=value&key=value``) without requiring bracket parsing logic.
* It preserves original ordering while remaining unambiguous and simple for log inspection.
* Many API gateways / proxies / caches reliably forward repeated keys whereas bracket syntaxes can be normalized away.
If your API prefers another convention (e.g. ``tags[]=x&tags[]=y`` or ``tags[0]=x``) just pass a custom ``EncodeOptions`` via
``extensions['extra_query_params_options']`` or parameter ``options`` when calling ``merge_query`` directly.
Advanced Per-Request Customization
----------------------------------
.. code-block:: python
from qs_codec import EncodeOptions, ListFormat
r = client.get(
"https://service.local/search",
params={"q": "test"},
extensions={
"extra_query_params": {"debug": True, "tags": ["alpha", "beta"]},
"extra_query_params_policy": "combine", # also accepts string values
"extra_query_params_options": EncodeOptions(list_format=ListFormat.BRACKETS),
},
)
# Example: ?q=test&debug=true&tags[]=alpha&tags[]=beta
Error Policy Example
--------------------
.. code-block:: python
try:
client.get(
"https://example.com",
params={"token": "abc"},
extensions={
"extra_query_params": {"token": "xyz"},
"extra_query_params_policy": "error",
},
)
except ValueError as exc:
print("Duplicate detected:", exc)
Testing Strategy
----------------
The project includes unit tests covering policy behaviors, error handling, and transport-level integration. Run them with:
.. code-block:: bash
pytest
Further Reading
---------------
* HTTPX documentation: https://www.python-httpx.org
* qs-codec documentation: https://techouse.github.io/qs_codec/
License
-------
BSD-3-Clause. See ``LICENSE`` for details.
Contributing
------------
Issues & PRs welcome. Please add tests for new behavior and keep doc examples in sync.