{"id":34927514,"url":"https://github.com/techouse/httpx_qs","last_synced_at":"2026-04-07T06:31:14.604Z","repository":{"id":316897425,"uuid":"1065248662","full_name":"techouse/httpx_qs","owner":"techouse","description":"HTTPX transport leveraging qs-codec for advanced query string encoding and decoding","archived":false,"fork":false,"pushed_at":"2026-03-31T11:09:26.000Z","size":159,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-31T12:27:21.700Z","etag":null,"topics":["httpx","httpx-transport","python","qs","qs-codec","query-string"],"latest_commit_sha":null,"homepage":"https://techouse.github.io/httpx_qs/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/techouse.png","metadata":{"files":{"readme":"README.rst","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE-OF-CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"techouse","custom":["https://paypal.me/ktusar"]}},"created_at":"2025-09-27T10:43:57.000Z","updated_at":"2026-03-31T11:06:44.000Z","dependencies_parsed_at":"2025-09-27T13:18:45.857Z","dependency_job_id":"97011f21-4504-4bbc-9f9f-ccf487419a73","html_url":"https://github.com/techouse/httpx_qs","commit_stats":null,"previous_names":["techouse/httpx_qs"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/techouse/httpx_qs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fhttpx_qs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fhttpx_qs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fhttpx_qs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fhttpx_qs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/techouse","download_url":"https://codeload.github.com/techouse/httpx_qs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fhttpx_qs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31503380,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-07T03:10:19.677Z","status":"ssl_error","status_checked_at":"2026-04-07T03:10:13.982Z","response_time":105,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["httpx","httpx-transport","python","qs","qs-codec","query-string"],"created_at":"2025-12-26T14:57:23.738Z","updated_at":"2026-04-07T06:31:14.599Z","avatar_url":"https://github.com/techouse.png","language":"Python","readme":"httpx-qs\n========\n\nSmart, policy-driven query string merging \u0026 encoding for `httpx \u003chttps://www.python-httpx.org\u003e`_ powered by\n`qs-codec \u003chttps://techouse.github.io/qs_codec/\u003e`_.\n\n.. image:: https://img.shields.io/pypi/v/httpx-qs?logo=pypi\n   :target: https://pypi.org/project/httpx-qs/\n   :alt: PyPI version\n\n.. image:: https://img.shields.io/pypi/status/httpx-qs?logo=pypi\n   :target: https://pypi.org/project/httpx-qs/\n   :alt: PyPI - Status\n\n.. image:: https://img.shields.io/pypi/dm/httpx-qs?logo=pypi\u0026label=PyPI%20downloads\n   :target: https://pypistats.org/packages/httpx-qs\n   :alt: PyPI - Downloads\n\n.. image:: https://img.shields.io/pypi/pyversions/httpx-qs?logo=python\u0026label=Python\n   :target: https://pypi.org/project/httpx-qs/\n   :alt: Supported Python versions\n\n.. image:: https://img.shields.io/badge/PyPy-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-6f42c1?logo=pypy\n   :target: https://www.pypy.org/\n   :alt: PyPy support status\n\n.. image:: https://img.shields.io/pypi/format/httpx-qs?logo=python\n   :target: https://pypi.org/project/httpx-qs/\n   :alt: PyPI - Format\n\n.. image:: https://github.com/techouse/httpx_qs/actions/workflows/test.yml/badge.svg\n   :target: https://github.com/techouse/httpx_qs/actions/workflows/test.yml\n   :alt: Tests\n\n.. image:: https://github.com/techouse/httpx_qs/actions/workflows/github-code-scanning/codeql/badge.svg\n   :target: https://github.com/techouse/httpx_qs/actions/workflows/github-code-scanning/codeql\n   :alt: CodeQL\n\n.. image:: https://img.shields.io/github/license/techouse/httpx_qs\n   :target: https://github.com/techouse/httpx_qs/blob/master/LICENSE\n   :alt: License\n\n.. image:: https://codecov.io/gh/techouse/httpx_qs/graph/badge.svg?token=JMt8akIZFh\n   :target: https://codecov.io/gh/techouse/httpx_qs\n   :alt: Codecov\n\n.. image:: https://app.codacy.com/project/badge/Grade/420bf66ab90d4b3798573b6ff86d02af\n   :target: https://app.codacy.com/gh/techouse/httpx_qs/dashboard?utm_source=gh\u0026utm_medium=referral\u0026utm_content=\u0026utm_campaign=Badge_grade\n   :alt: Codacy Quality\n\n.. image:: https://img.shields.io/github/sponsors/techouse?logo=github\n   :target: https://github.com/sponsors/techouse\n   :alt: GitHub Sponsors\n\n.. image:: https://img.shields.io/github/stars/techouse/httpx_qs\n   :target: https://github.com/techouse/httpx_qs/stargazers\n   :alt: GitHub Repo stars\n\n.. image:: https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg?logo=contributorcovenant\n   :target: CODE-OF-CONDUCT.md\n   :alt: Contributor Covenant\n\n.. |flake8| image:: https://img.shields.io/badge/flake8-checked-blueviolet.svg?logo=python\n   :target: https://flake8.pycqa.org/en/latest/\n\n.. image:: https://img.shields.io/badge/mypy-checked-blue.svg?logo=python\n   :target: https://mypy.readthedocs.io/en/stable/\n   :alt: mypy\n\n.. image:: https://img.shields.io/badge/linting-pylint-yellowgreen.svg?logo=python\n   :target: https://github.com/pylint-dev/pylint\n   :alt: pylint\n\n.. image:: https://img.shields.io/badge/imports-isort-blue.svg?logo=python\n   :target: https://pycqa.github.io/isort/\n   :alt: isort\n\n.. image:: https://img.shields.io/badge/security-bandit-blue.svg?logo=python\n   :target: https://github.com/PyCQA/bandit\n   :alt: Security Status\n\nOverview\n--------\n\n``httpx-qs`` provides:\n\n* A transport wrapper ``SmartQueryStrings`` that merges *existing* URL query parameters with *additional* ones supplied via ``request.extensions``.\n* A flexible ``merge_query`` utility with selectable conflict resolution policies.\n* Consistent, standards-aware encoding via ``qs-codec`` (RFC3986 percent-encoding, structured arrays, nested objects, etc.).\n\nWhy?\n----\n\nHTTPX already lets you pass ``params=`` when making requests, but sometimes you need to:\n\n* Inject **additional** query parameters from middleware/transport layers (e.g., auth tags, tracing IDs, feature flags) *without losing* the caller's original intent.\n* Combine repeated keys or treat them deterministically (replace / keep / error) rather than always flattening.\n* Support nested data or list semantics consistent across clients and services.\n\n``qs-codec`` supplies the primitives (decoding \u0026 encoding with configurable ``ListFormat``). ``httpx-qs`` stitches that into HTTPX's transport pipeline so you can declaratively extend queries at request dispatch time.\n\nRequirements\n------------\n\n* CPython 3.8-3.14, including free-threaded 3.14t, or PyPy 3.8-3.11\n* ``httpx\u003e=0.28.1,\u003c1.0.0``\n* ``qs-codec\u003e=1.5.0``\n\nInstallation\n------------\n\n.. code-block:: bash\n\n\tpip install httpx-qs\n\nMinimal Example\n---------------\n\n.. code-block:: python\n\n\timport httpx\n\tfrom httpx_qs.transporters.smart_query_strings import SmartQueryStrings\n\n\tclient = httpx.Client(transport=SmartQueryStrings(httpx.HTTPTransport()))\n\n\tresponse = client.get(\n\t\t\"https://www.google.com\",\n\t\tparams={\"a\": \"b\", \"c\": \"d\"},\n\t\textensions={\"extra_query_params\": {\"c\": \"D\", \"tags\": [\"x\", \"y\"]}},\n\t)\n\n\tprint(str(response.request.url))\n\t# Example (order may vary): https://www.google.com/?a=b\u0026c=d\u0026c=D\u0026tags=x\u0026tags=y\n\nUsing Merge Policies\n--------------------\n\nConflict resolution when a key already exists is controlled by ``MergePolicy``.\n\nAvailable policies:\n\n* ``combine`` (default): concatenate values → existing first, new afterward (``a=1\u0026a=2``)\n* ``replace``: last-wins, existing value is overwritten (``a=2``)\n* ``keep``: first-wins, ignore the new value (``a=1``)\n* ``error``: raise ``ValueError`` on duplicate key\n\nSpecify per request:\n\n.. code-block:: python\n\n\tfrom httpx_qs import MergePolicy\n\n\tr = client.get(\n\t\t\"https://api.example.com/resources\",\n\t\tparams={\"dup\": \"original\"},\n\t\textensions={\n\t\t\t\"extra_query_params\": {\"dup\": \"override\"},\n\t\t\t\"extra_query_params_policy\": MergePolicy.REPLACE,\n\t\t},\n\t)\n\t# Query contains only dup=override\n\nAsync Usage\n-----------\n\n``SmartQueryStrings`` works equally for ``AsyncClient``:\n\n.. code-block:: python\n\n\timport httpx\n\tfrom httpx_qs.transporters.smart_query_strings import SmartQueryStrings\n\n\tasync def main() -\u003e None:\n\t\tasync with httpx.AsyncClient(transport=SmartQueryStrings(httpx.AsyncHTTPTransport())) as client:\n\t\t\tr = await client.get(\n\t\t\t\t\"https://example.com/items\",\n\t\t\t\tparams={\"filters\": \"active\"},\n\t\t\t\textensions={\"extra_query_params\": {\"page\": 2}},\n\t\t\t)\n\t\t\tprint(r.request.url)\n\n\t# Run with: asyncio.run(main())\n\n``merge_query`` Utility\n-----------------------\n\nYou can use the underlying function directly:\n\n.. code-block:: python\n\n\tfrom httpx_qs import merge_query, MergePolicy\n\tfrom qs_codec import EncodeOptions, ListFormat\n\n\tnew_url = merge_query(\n\t\t\"https://example.com?a=1\",\n\t\t{\"a\": 2, \"tags\": [\"x\", \"y\"]},\n\t\toptions=EncodeOptions(list_format=ListFormat.REPEAT),\n\t\tpolicy=MergePolicy.COMBINE,\n\t)\n\t# → https://example.com/?a=1\u0026a=2\u0026tags=x\u0026tags=y\n\nWhy ``ListFormat.REPEAT`` by Default?\n-------------------------------------\n\n``qs-codec`` exposes several list formatting strategies (e.g. repeat, brackets, indices). ``httpx-qs`` defaults to\n``ListFormat.REPEAT`` because:\n\n* It matches common server expectations (``key=value\u0026key=value``) without requiring bracket parsing logic.\n* It preserves original ordering while remaining unambiguous and simple for log inspection.\n* Many API gateways / proxies / caches reliably forward repeated keys whereas bracket syntaxes can be normalized away.\n\nIf your API prefers another convention (e.g. ``tags[]=x\u0026tags[]=y`` or ``tags[0]=x``) just pass a custom ``EncodeOptions`` via\n``extensions['extra_query_params_options']`` or parameter ``options`` when calling ``merge_query`` directly.\n\nAdvanced Per-Request Customization\n----------------------------------\n\n.. code-block:: python\n\n\tfrom qs_codec import EncodeOptions, ListFormat\n\n\tr = client.get(\n\t\t\"https://service.local/search\",\n\t\tparams={\"q\": \"test\"},\n\t\textensions={\n\t\t\t\"extra_query_params\": {\"debug\": True, \"tags\": [\"alpha\", \"beta\"]},\n\t\t\t\"extra_query_params_policy\": \"combine\",  # also accepts string values\n\t\t\t\"extra_query_params_options\": EncodeOptions(list_format=ListFormat.BRACKETS),\n\t\t},\n\t)\n\t# Example: ?q=test\u0026debug=true\u0026tags[]=alpha\u0026tags[]=beta\n\nError Policy Example\n--------------------\n\n.. code-block:: python\n\n\ttry:\n\t\tclient.get(\n\t\t\t\"https://example.com\",\n\t\t\tparams={\"token\": \"abc\"},\n\t\t\textensions={\n\t\t\t\t\"extra_query_params\": {\"token\": \"xyz\"},\n\t\t\t\t\"extra_query_params_policy\": \"error\",\n\t\t\t},\n\t\t)\n\texcept ValueError as exc:\n\t\tprint(\"Duplicate detected:\", exc)\n\nTesting Strategy\n----------------\n\nThe project includes unit tests covering policy behaviors, error handling, and transport-level integration. Run them with:\n\n.. code-block:: bash\n\n\tpytest\n\nFurther Reading\n---------------\n\n* HTTPX documentation: https://www.python-httpx.org\n* qs-codec documentation: https://techouse.github.io/qs_codec/\n\nLicense\n-------\n\nBSD-3-Clause. See ``LICENSE`` for details.\n\nContributing\n------------\n\nIssues \u0026 PRs welcome. Please add tests for new behavior and keep doc examples in sync.\n","funding_links":["https://github.com/sponsors/techouse","https://paypal.me/ktusar"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechouse%2Fhttpx_qs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftechouse%2Fhttpx_qs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechouse%2Fhttpx_qs/lists"}