diff --git a/CHANGELOG.md b/CHANGELOG.md index 96f03d2..63f2127 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,17 @@ # Changelog -## Unreleased +## 0.2.0 (2026-08-21) +- SDK + testkit (breaking): migrated from `httpx` to [`httpx2`](https://github.com/pydantic/httpx2), Pydantic's maintained continuation of httpx, for timely security updates. `httpx` types are part of the public surface (`http_client=`, `with_options(timeout=)`, the testkit `Handler` alias), so callers must swap `import httpx` for `import httpx2` and pass `httpx2.Client` / `httpx2.AsyncClient` / `httpx2.Timeout`. Note httpx2 verifies TLS against the OS trust store via `truststore` instead of bundled `certifi` roots. - CLI (fix): `auth login` and `auth status` now honor the global `--base-url` and `--api-key`. Every other command routed through `get_client(ctx)`; these two built their own client, so `--base-url` was ignored and `auth status` reported `"valid": true` for a host it never contacted, while `--api-key` was ignored in favour of the environment or config key. `auth status` gains a third `source` value, `option`, for a key passed explicitly on the command line. An ambient `DISCOLIKE_API_KEY` still does not skip the `auth login` prompt — only an explicit flag does. - SDK: `email.job(job_id)` accepts `kind="find"|"verify"` (default `"find"`), so verify jobs can be rehydrated — previously every rehydrated job decoded as a find job. `wait()` now returns `EnumerationOutput | ValidationOutput` per the handle's kind. Job and batch results also honor a server-reported `kind` field when present, so a handle rehydrated with the wrong kind still parses each result into the right model. - SDK: `contacts.count` returns a typed `Count` and `contacts.discover` a typed `ContactsDiscoverResponse` (`results` map of domain → `ContactsByCompany`, `total_contacts`, `total_domains`) — both previously returned a bare passthrough model with everything in `.extra`. `ContactsByCompany` now extends `CompanyProfile` (firmographics + nested `contacts`, `email_pattern`, `email_pattern_confidence`, `email_pattern_guess`), mirroring the platform's `DomainContactsEntry`; it was previously defined but never constructed. - SDK: the client-level `discover`, `count`, `validate_icp`, `append`, and `segment` methods now declare explicit typed signatures mirroring their underlying resource methods (sync + async) instead of untyped `**kwargs` — misspelled keywords are caught statically and editors autocomplete every parameter. -- SDK: new `client.with_options(timeout=...)` (sync + async) — returns a lightweight client view with a per-request timeout override (float or `httpx.Timeout`), sharing the parent's connection pool. Client-level rate limiting and pagination stay out by design: the transport already retries 429 honoring `Retry-After`, and search/discover paginate via `offset`/`max_records`. +- SDK: new `client.with_options(timeout=...)` (sync + async) — returns a lightweight client view with a per-request timeout override (float or `httpx2.Timeout`), sharing the parent's connection pool. Client-level rate limiting and pagination stay out by design: the transport already retries 429 honoring `Retry-After`, and search/discover paginate via `offset`/`max_records`. - CLI: new `discolike email` command group wrapping the SDK email resource — `find FIRST LAST DOMAIN [--known-pattern X]`, `find-batch` (CSV file and/or repeatable `--contact "first,last,domain"`, max 500 per batch), `results BATCH_ID [--kind find|verify]`, and `job JOB_ID`, each with `--wait/--no-wait` polling. - SDK: `email.find` accepts `known_pattern` (sync + async), matching the platform's `POST /email/find` body. Omitted from the request when unset. - SDK: email routes are no longer `openapi=False` — the platform now exposes `/email/find`, `/email/find/batch`, and the poll routes in its OpenAPI spec, so `check_contract.py` validates them like every other route. - Examples: new `examples/` folder with runnable end-to-end scripts — `match_crm_contacts.py` (bulk-match a CRM CSV to personas with resumable checkpointing and website+email domain keys), `find_emails_from_csv.py` (batch email finding), `discover_and_enrich.py` (discover + DiscoGen enrichment). Referenced from the README. - -## 0.1.2 (2026-08-19) - - Packaging: both wheels now ship the MIT license text (`dist-info/licenses/LICENSE`) — it was absent from every release so far, since the only `LICENSE` sat at the repo root, outside either package root. - Packaging: `discolike-cli` ships `py.typed`, so its annotations are visible to type checkers importing `discolike_cli`. Added classifiers: `Python :: 3 :: Only`, `OS Independent`, `Typing :: Typed`, plus `Libraries :: Python Modules` (SDK) and `Environment :: Console` / `Topic :: Utilities` (CLI). Added `Changelog` and `Issues` project URLs. - CI: tests also run on Python 3.15 prereleases in a non-blocking job. 3.15 stays out of the supported matrix and classifiers until 3.15.0 final. diff --git a/README.md b/README.md index 9475c05..c7577c0 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ except ValidationError as err: | `base_url` | `https://api.discolike.com/v1` | | | `timeout` | `60.0` seconds | | | `max_retries` | `3` | | -| `http_client` | — | Bring your own `httpx.Client` / `httpx.AsyncClient` | +| `http_client` | — | Bring your own `httpx2.Client` / `httpx2.AsyncClient` | A provided `http_client` is mutated in place (the auth header is stamped on it, and `base_url` is set if it's unset) — use a client dedicated to DiscoLike, not one shared across other services. diff --git a/packages/discolike-cli/pyproject.toml b/packages/discolike-cli/pyproject.toml index bb11645..fa97293 100644 --- a/packages/discolike-cli/pyproject.toml +++ b/packages/discolike-cli/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "discolike-cli" -version = "0.1.2" +version = "0.2.0" description = "Official CLI for the DiscoLike API" readme = "README.md" license = "MIT" @@ -12,7 +12,7 @@ license-files = ["LICENSE"] requires-python = ">=3.10" authors = [{ name = "DiscoLike", email = "support@discolike.com" }] dependencies = [ - "discolike>=0.1.2", + "discolike==0.2.0", "typer>=0.12", "rich>=13.0", ] diff --git a/packages/discolike-cli/tests/conftest.py b/packages/discolike-cli/tests/conftest.py index c0450db..44da363 100644 --- a/packages/discolike-cli/tests/conftest.py +++ b/packages/discolike-cli/tests/conftest.py @@ -8,13 +8,13 @@ from collections.abc import Callable from typing import Any -import httpx +import httpx2 import pytest import discolike_cli.main as cli_main from discolike import Discolike -Handler = Callable[[httpx.Request], httpx.Response] +Handler = Callable[[httpx2.Request], httpx2.Response] @pytest.fixture diff --git a/packages/discolike-cli/tests/test_account.py b/packages/discolike-cli/tests/test_account.py index ec1a027..e91c982 100644 --- a/packages/discolike-cli/tests/test_account.py +++ b/packages/discolike-cli/tests/test_account.py @@ -3,7 +3,7 @@ import json from collections.abc import Callable -import httpx +import httpx2 from typer.testing import CliRunner from discolike_cli.main import app @@ -13,9 +13,9 @@ def test_account_usage_hits_usage_endpoint(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.path == "/v1/usage" - return httpx.Response(200, json={"requests_mtd": 100, "records_mtd": 500, "spend_mtd": 12.5}) + return httpx2.Response(200, json={"requests_mtd": 100, "records_mtd": 500, "spend_mtd": 12.5}) install_build_client(handler) result = runner.invoke(app, ["account", "usage"]) diff --git a/packages/discolike-cli/tests/test_auth.py b/packages/discolike-cli/tests/test_auth.py index 3a5acc8..b2f5f72 100644 --- a/packages/discolike-cli/tests/test_auth.py +++ b/packages/discolike-cli/tests/test_auth.py @@ -5,7 +5,7 @@ from collections.abc import Callable from typing import Any -import httpx +import httpx2 import pytest from typer.testing import CliRunner @@ -18,12 +18,12 @@ runner = CliRunner() -def _usage_ok(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"requests_mtd": 1, "records_mtd": 2, "spend_mtd": 3.0}) +def _usage_ok(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"requests_mtd": 1, "records_mtd": 2, "spend_mtd": 3.0}) -def _usage_unauthorized(request: httpx.Request) -> httpx.Response: - return httpx.Response(401, json={"detail": "invalid key"}) +def _usage_unauthorized(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(401, json={"detail": "invalid key"}) def test_login_with_api_key_option_verifies_and_saves(install_build_client: Callable[[Handler], None]) -> None: diff --git a/packages/discolike-cli/tests/test_company.py b/packages/discolike-cli/tests/test_company.py index 18420ec..eefc90e 100644 --- a/packages/discolike-cli/tests/test_company.py +++ b/packages/discolike-cli/tests/test_company.py @@ -3,7 +3,7 @@ import json from collections.abc import Callable -import httpx +import httpx2 import pytest from typer.testing import CliRunner @@ -24,11 +24,11 @@ def test_company_subcommands_route_to_expected_path( args: list[str], expected_path: str, install_build_client: Callable[[Handler], None] ) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"domain": "acme.com", "ok": True}) + return httpx2.Response(200, json={"domain": "acme.com", "ok": True}) install_build_client(handler) result = runner.invoke(app, args) @@ -50,11 +50,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_company_list_subcommands_route_to_expected_path( args: list[str], expected_path: str, install_build_client: Callable[[Handler], None] ) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json=[{"linked_domain": "acme.io", "ok": True}]) + return httpx2.Response(200, json=[{"linked_domain": "acme.io", "ok": True}]) install_build_client(handler) result = runner.invoke(app, args) @@ -68,11 +68,11 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.parametrize("subcommand", ["redirects", "vendors", "subsidiaries"]) def test_company_match_option_forwarded(subcommand: str, install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json=[{"linked_domain": "acme.io"}]) + return httpx2.Response(200, json=[{"linked_domain": "acme.io"}]) install_build_client(handler) result = runner.invoke(app, ["company", subcommand, "acme.com", "--match", "loose"]) @@ -81,11 +81,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_company_public_links_hits_publiclink_endpoint(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json=[{"linked_domain": "acme.io"}]) + return httpx2.Response(200, json=[{"linked_domain": "acme.io"}]) install_build_client(handler) result = runner.invoke(app, ["company", "public-links", "acme.com", "--source", "crunchbase"]) @@ -97,8 +97,8 @@ def handler(request: httpx.Request) -> httpx.Response: def test_company_public_links_requires_source(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"ok": True}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"ok": True}) install_build_client(handler) result = runner.invoke(app, ["company", "public-links", "acme.com"]) @@ -106,8 +106,8 @@ def handler(request: httpx.Request) -> httpx.Response: def test_company_data_format_table_falls_back_to_json_for_dict(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"name": "Acme", "domain": "acme.com"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"name": "Acme", "domain": "acme.com"}) install_build_client(handler) result = runner.invoke(app, ["company", "data", "acme.com", "--format", "table"]) @@ -119,11 +119,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_extract_hits_extract_endpoint_with_url(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"text": "hello", "language": "en"}) + return httpx2.Response(200, json={"text": "hello", "language": "en"}) install_build_client(handler) result = runner.invoke(app, ["extract", "https://acme.com/about"]) diff --git a/packages/discolike-cli/tests/test_contacts_cli.py b/packages/discolike-cli/tests/test_contacts_cli.py index 1d969fc..ee5cc65 100644 --- a/packages/discolike-cli/tests/test_contacts_cli.py +++ b/packages/discolike-cli/tests/test_contacts_cli.py @@ -3,7 +3,7 @@ import json from collections.abc import Callable -import httpx +import httpx2 import pytest from typer.testing import CliRunner @@ -14,11 +14,11 @@ def test_contacts_search_sends_options_and_param_escape_hatch(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.QueryParams] = {} + captured: dict[str, httpx2.QueryParams] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["params"] = request.url.params - return httpx.Response(200, json=[{"persona_id": 1, "domain": "acme.com"}]) + return httpx2.Response(200, json=[{"persona_id": 1, "domain": "acme.com"}]) install_build_client(handler) result = runner.invoke( @@ -77,11 +77,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_contacts_search_forwards_negate_options(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.QueryParams] = {} + captured: dict[str, httpx2.QueryParams] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["params"] = request.url.params - return httpx.Response(200, json=[]) + return httpx2.Response(200, json=[]) install_build_client(handler) result = runner.invoke( @@ -114,8 +114,8 @@ def handler(request: httpx.Request) -> httpx.Response: def test_contacts_search_icp_text_is_removed(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json=[]) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json=[]) install_build_client(handler) result = runner.invoke(app, ["contacts", "search", "--icp-text", "X"]) @@ -123,8 +123,8 @@ def handler(request: httpx.Request) -> httpx.Response: def test_contacts_search_invalid_param_kwarg_exits_2(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json=[]) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json=[]) install_build_client(handler) result = runner.invoke(app, ["contacts", "search", "--param", "bogus_kwarg=1"]) @@ -133,8 +133,8 @@ def handler(request: httpx.Request) -> httpx.Response: def test_contacts_search_unauthorized_exits_3(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(401, json={"detail": "invalid key"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(401, json={"detail": "invalid key"}) install_build_client(handler) result = runner.invoke(app, ["contacts", "search", "--icp-prompt", "X"]) @@ -144,11 +144,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_contacts_count_sends_shared_subset(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.QueryParams] = {} + captured: dict[str, httpx2.QueryParams] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["params"] = request.url.params - return httpx.Response(200, json={"count": 12}) + return httpx2.Response(200, json={"count": 12}) install_build_client(handler) result = runner.invoke(app, ["contacts", "count", "--seniority", "vp", "--param", "min_connections=5"]) @@ -161,11 +161,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_contacts_lookup_by_persona_id(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"persona_id": 7, "domain": "acme.com"}) + return httpx2.Response(200, json={"persona_id": 7, "domain": "acme.com"}) install_build_client(handler) result = runner.invoke(app, ["contacts", "lookup", "--persona-id", "7", "--email", "jane@acme.com"]) @@ -184,11 +184,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_contacts_match_hits_match_endpoint(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"matches": []}) + return httpx2.Response(200, json={"matches": []}) install_build_client(handler) result = runner.invoke( @@ -209,11 +209,11 @@ def test_contacts_bulk_match_without_wait_prints_task_hint( ) -> None: queries_file = tmp_path / "queries.json" queries_file.write_text(json.dumps([{"name": "Jane Doe"}])) - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"task_id": "cm-1"}) + return httpx2.Response(200, json={"task_id": "cm-1"}) install_build_client(handler) result = runner.invoke(app, ["contacts", "bulk-match", "--queries-file", str(queries_file)]) @@ -234,8 +234,8 @@ def test_contacts_bulk_match_bad_queries_file_exits_2( queries_file = tmp_path / "queries.json" queries_file.write_text(content) - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"task_id": "cm-1"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"task_id": "cm-1"}) install_build_client(handler) result = runner.invoke(app, ["contacts", "bulk-match", "--queries-file", str(queries_file)]) @@ -249,14 +249,14 @@ def test_contacts_bulk_match_with_wait_polls_to_completion( queries_file.write_text(json.dumps([{"name": "Jane Doe"}])) statuses = iter( [ - httpx.Response(200, json={"status": "processing", "progress": 30}), - httpx.Response(200, json={"status": "completed", "progress": 100, "results": [{"persona_id": 1}]}), + httpx2.Response(200, json={"status": "processing", "progress": 30}), + httpx2.Response(200, json={"status": "completed", "progress": 100, "results": [{"persona_id": 1}]}), ] ) - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.url.path == "/v1/contacts/bulk-match": - return httpx.Response(200, json={"task_id": "cm-2"}) + return httpx2.Response(200, json={"task_id": "cm-2"}) assert request.url.path == "/v1/contactmatch/status/cm-2" return next(statuses) @@ -273,10 +273,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_contacts_discover_posts_json_body(install_build_client: Callable[[Handler], None]) -> None: captured: dict[str, object] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["path"] = request.url.path captured["body"] = json.loads(request.content) - return httpx.Response(200, json={"results": {}, "total_contacts": 0}) + return httpx2.Response(200, json={"results": {}, "total_contacts": 0}) install_build_client(handler) result = runner.invoke( @@ -306,10 +306,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_contacts_generate_without_wait_prints_task_hint(install_build_client: Callable[[Handler], None]) -> None: captured: dict[str, object] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["path"] = request.url.path captured["body"] = json.loads(request.content) - return httpx.Response(200, json={"task_id": "dg-1"}) + return httpx2.Response(200, json={"task_id": "dg-1"}) install_build_client(handler) result = runner.invoke( diff --git a/packages/discolike-cli/tests/test_discogen_cli.py b/packages/discolike-cli/tests/test_discogen_cli.py index f68ad53..4397665 100644 --- a/packages/discolike-cli/tests/test_discogen_cli.py +++ b/packages/discolike-cli/tests/test_discogen_cli.py @@ -3,7 +3,7 @@ import json from collections.abc import Callable -import httpx +import httpx2 from typer.testing import CliRunner from discolike_cli.main import app @@ -15,10 +15,10 @@ def test_discogen_run_posts_json_and_prints_task_hint(install_build_client: Callable[[Handler], None]) -> None: captured: dict[str, object] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["path"] = request.url.path captured["body"] = json.loads(request.content) - return httpx.Response(200, json={"task_id": "dg-1"}) + return httpx2.Response(200, json={"task_id": "dg-1"}) install_build_client(handler) result = runner.invoke( @@ -50,9 +50,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_discogen_run_sends_include_x_search_when_passed(install_build_client: Callable[[Handler], None]) -> None: captured: dict[str, object] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["body"] = json.loads(request.content) - return httpx.Response(200, json={"task_id": "dg-1b"}) + return httpx2.Response(200, json={"task_id": "dg-1b"}) install_build_client(handler) result = runner.invoke( @@ -70,10 +70,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_discogen_run_personas_posts_persona_ids(install_build_client: Callable[[Handler], None]) -> None: captured: dict[str, object] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["path"] = request.url.path captured["body"] = json.loads(request.content) - return httpx.Response(200, json={"task_id": "dg-2"}) + return httpx2.Response(200, json={"task_id": "dg-2"}) install_build_client(handler) result = runner.invoke( @@ -93,9 +93,9 @@ def test_discogen_run_personas_sends_include_x_search_when_passed( ) -> None: captured: dict[str, object] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["body"] = json.loads(request.content) - return httpx.Response(200, json={"task_id": "dg-2b"}) + return httpx2.Response(200, json={"task_id": "dg-2b"}) install_build_client(handler) result = runner.invoke( @@ -113,14 +113,14 @@ def handler(request: httpx.Request) -> httpx.Response: def test_discogen_run_with_wait_polls_to_completion(install_build_client: Callable[[Handler], None]) -> None: statuses = iter( [ - httpx.Response(200, json={"status": "processing", "progress": 50}), - httpx.Response(200, json={"status": "completed", "progress": 100, "results": [{"summary": "ok"}]}), + httpx2.Response(200, json={"status": "processing", "progress": 50}), + httpx2.Response(200, json={"status": "completed", "progress": 100, "results": [{"summary": "ok"}]}), ] ) - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.url.path == "/v1/discogen/process": - return httpx.Response(200, json={"task_id": "dg-3"}) + return httpx2.Response(200, json={"task_id": "dg-3"}) assert request.url.path == "/v1/discogen/status/dg-3" return next(statuses) @@ -135,9 +135,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_discogen_models_hits_models_endpoint(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.path == "/v1/discogen/models" - return httpx.Response(200, json={"models": {"openai": [{"name": "gpt-5.4", "supports_web_search": False}]}}) + return httpx2.Response(200, json={"models": {"openai": [{"name": "gpt-5.4", "supports_web_search": False}]}}) install_build_client(handler) result = runner.invoke(app, ["discogen", "models"]) @@ -146,9 +146,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_discogen_status_default_family_discogen(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.path == "/v1/discogen/status/dg-4" - return httpx.Response(200, json={"status": "completed", "progress": 100}) + return httpx2.Response(200, json={"status": "completed", "progress": 100}) install_build_client(handler) result = runner.invoke(app, ["discogen", "status", "dg-4"]) @@ -157,9 +157,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_discogen_status_with_family_segment(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.path == "/v1/segment/status/seg-1" - return httpx.Response(200, json={"status": "processing", "progress": 10}) + return httpx2.Response(200, json={"status": "processing", "progress": 10}) install_build_client(handler) result = runner.invoke(app, ["discogen", "status", "seg-1", "--family", "segment"]) @@ -168,10 +168,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_discogen_cancel_hits_cancel_endpoint(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.path == "/v1/discogen/cancel/dg-5" assert request.method == "DELETE" - return httpx.Response(200, json={"ok": True}) + return httpx2.Response(200, json={"ok": True}) install_build_client(handler) result = runner.invoke(app, ["discogen", "cancel", "dg-5"]) @@ -180,9 +180,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_discogen_cancel_with_family_bulkmatch(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.path == "/v1/bulkmatch/cancel/bm-1" - return httpx.Response(200, json={"ok": True}) + return httpx2.Response(200, json={"ok": True}) install_build_client(handler) result = runner.invoke(app, ["discogen", "cancel", "bm-1", "--family", "bulkmatch"]) @@ -191,8 +191,8 @@ def handler(request: httpx.Request) -> httpx.Response: def test_discogen_status_invalid_family_exits_2(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={}) install_build_client(handler) result = runner.invoke(app, ["discogen", "status", "dg-6", "--family", "bogus"]) diff --git a/packages/discolike-cli/tests/test_discover.py b/packages/discolike-cli/tests/test_discover.py index 2a1f210..38a93a6 100644 --- a/packages/discolike-cli/tests/test_discover.py +++ b/packages/discolike-cli/tests/test_discover.py @@ -3,7 +3,7 @@ import json from collections.abc import Callable -import httpx +import httpx2 from typer.testing import CliRunner from discolike_cli.main import app @@ -12,22 +12,22 @@ runner = CliRunner() -def _discover_ok(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json=[{"domain": "acme.com"}]) +def _discover_ok(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json=[{"domain": "acme.com"}]) -def _count_ok(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"count": 42}) +def _count_ok(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"count": 42}) -def _unauthorized(request: httpx.Request) -> httpx.Response: - return httpx.Response(401, json={"detail": "invalid key"}) +def _unauthorized(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(401, json={"detail": "invalid key"}) def test_discover_sends_options_and_param_escape_hatch(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.QueryParams] = {} + captured: dict[str, httpx2.QueryParams] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["params"] = request.url.params return _discover_ok(request) @@ -46,9 +46,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_discover_param_with_comma_value_becomes_list(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.QueryParams] = {} + captured: dict[str, httpx2.QueryParams] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["params"] = request.url.params return _discover_ok(request) @@ -71,9 +71,9 @@ def test_discover_param_removed_kwarg_exits_2(install_build_client: Callable[[Ha def test_discover_explicit_option_wins_over_param_duplicate(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.QueryParams] = {} + captured: dict[str, httpx2.QueryParams] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["params"] = request.url.params return _discover_ok(request) @@ -84,9 +84,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_discover_negate_option_forwarded(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.QueryParams] = {} + captured: dict[str, httpx2.QueryParams] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["params"] = request.url.params return _discover_ok(request) @@ -103,9 +103,9 @@ def test_discover_icp_text_option_removed(install_build_client: Callable[[Handle def test_count_sends_shared_filter_subset(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.QueryParams] = {} + captured: dict[str, httpx2.QueryParams] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["params"] = request.url.params return _count_ok(request) diff --git a/packages/discolike-cli/tests/test_email_cli.py b/packages/discolike-cli/tests/test_email_cli.py index a4426b1..776ee5b 100644 --- a/packages/discolike-cli/tests/test_email_cli.py +++ b/packages/discolike-cli/tests/test_email_cli.py @@ -5,7 +5,7 @@ import time from collections.abc import Callable -import httpx +import httpx2 import pytest from typer.testing import CliRunner @@ -30,11 +30,11 @@ def no_sleep(monkeypatch: pytest.MonkeyPatch) -> None: def test_email_find_without_wait_prints_job_hint(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"job_id": "ej-1"}) + return httpx2.Response(200, json={"job_id": "ej-1"}) install_build_client(handler) result = runner.invoke(app, ["email", "find", "Jane", "Doe", "acme.com"]) @@ -50,14 +50,14 @@ def handler(request: httpx.Request) -> httpx.Response: def test_email_find_with_wait_polls_to_completion(install_build_client: Callable[[Handler], None]) -> None: statuses = iter( [ - httpx.Response(200, json={"job_id": "ej-2", "status": "processing"}), - httpx.Response(200, json={"job_id": "ej-2", "status": "completed", "result": FOUND_RESULT}), + httpx2.Response(200, json={"job_id": "ej-2", "status": "processing"}), + httpx2.Response(200, json={"job_id": "ej-2", "status": "completed", "result": FOUND_RESULT}), ] ) - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.url.path == "/v1/email/find": - return httpx.Response(200, json={"job_id": "ej-2"}) + return httpx2.Response(200, json={"job_id": "ej-2"}) assert request.url.path == "/v1/email/jobs/ej-2" return next(statuses) @@ -75,11 +75,11 @@ def test_email_find_batch_from_csv_file( ) -> None: contacts_file = tmp_path / "contacts.csv" contacts_file.write_text("first_name,last_name,domain\nJane,Doe,acme.com\nJohn,Smith,beta.com\n") - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"batch_id": "eb-1"}) + return httpx2.Response(200, json={"batch_id": "eb-1"}) install_build_client(handler) result = runner.invoke(app, ["email", "find-batch", "--contacts-file", str(contacts_file)]) @@ -98,11 +98,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_email_find_batch_from_inline_contacts(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"batch_id": "eb-2"}) + return httpx2.Response(200, json={"batch_id": "eb-2"}) install_build_client(handler) result = runner.invoke( @@ -120,8 +120,8 @@ def handler(request: httpx.Request) -> httpx.Response: def test_email_find_batch_with_wait_polls_results(install_build_client: Callable[[Handler], None]) -> None: results_pages = iter( [ - httpx.Response(200, json={"batch_id": "eb-3", "total": 1, "completed": 0, "failed": 0, "results": []}), - httpx.Response( + httpx2.Response(200, json={"batch_id": "eb-3", "total": 1, "completed": 0, "failed": 0, "results": []}), + httpx2.Response( 200, json={ "batch_id": "eb-3", @@ -134,9 +134,9 @@ def test_email_find_batch_with_wait_polls_results(install_build_client: Callable ] ) - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.url.path == "/v1/email/find/batch": - return httpx.Response(200, json={"batch_id": "eb-3"}) + return httpx2.Response(200, json={"batch_id": "eb-3"}) assert request.url.path == "/v1/email/batch/eb-3/results" return next(results_pages) @@ -161,8 +161,8 @@ def handler(request: httpx.Request) -> httpx.Response: ], ) def test_email_find_batch_bad_contacts_exit_2(args: list[str], install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"batch_id": "eb-4"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"batch_id": "eb-4"}) install_build_client(handler) result = runner.invoke(app, ["email", "find-batch", *args]) @@ -175,8 +175,8 @@ def test_email_find_batch_missing_csv_columns_exits_2( contacts_file = tmp_path / "contacts.csv" contacts_file.write_text("first,last,site\nJane,Doe,acme.com\n") - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"batch_id": "eb-5"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"batch_id": "eb-5"}) install_build_client(handler) result = runner.invoke(app, ["email", "find-batch", "--contacts-file", str(contacts_file)]) @@ -191,8 +191,8 @@ def test_email_find_batch_over_500_contacts_exits_2( contacts_file = tmp_path / "contacts.csv" contacts_file.write_text(f"first_name,last_name,domain\n{rows}\n") - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"batch_id": "eb-6"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"batch_id": "eb-6"}) install_build_client(handler) result = runner.invoke(app, ["email", "find-batch", "--contacts-file", str(contacts_file)]) @@ -203,11 +203,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_email_results_without_wait_returns_partial_snapshot( install_build_client: Callable[[Handler], None], ) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response( + return httpx2.Response( 200, json={"batch_id": "eb-7", "total": 2, "completed": 1, "failed": 0, "results": [{"status": "completed"}]}, ) @@ -224,9 +224,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_email_results_verify_kind_decodes_validation_output( install_build_client: Callable[[Handler], None], ) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.path == "/v1/email/batch/eb-8/results" - return httpx.Response( + return httpx2.Response( 200, json={ "batch_id": "eb-8", @@ -251,8 +251,8 @@ def handler(request: httpx.Request) -> httpx.Response: def test_email_results_invalid_kind_exits_2(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={}) install_build_client(handler) result = runner.invoke(app, ["email", "results", "eb-9", "--kind", "bogus"]) @@ -260,9 +260,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_email_job_prints_current_status(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.path == "/v1/email/jobs/ej-5" - return httpx.Response(200, json={"job_id": "ej-5", "status": "processing"}) + return httpx2.Response(200, json={"job_id": "ej-5", "status": "processing"}) install_build_client(handler) result = runner.invoke(app, ["email", "job", "ej-5"]) @@ -273,8 +273,8 @@ def handler(request: httpx.Request) -> httpx.Response: def test_email_job_unauthorized_exits_3(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(401, json={"detail": "invalid key"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(401, json={"detail": "invalid key"}) install_build_client(handler) result = runner.invoke(app, ["email", "job", "ej-6"]) diff --git a/packages/discolike-cli/tests/test_enrich_cli.py b/packages/discolike-cli/tests/test_enrich_cli.py index 4f146b3..6210386 100644 --- a/packages/discolike-cli/tests/test_enrich_cli.py +++ b/packages/discolike-cli/tests/test_enrich_cli.py @@ -3,7 +3,7 @@ import json from collections.abc import Callable -import httpx +import httpx2 from typer.testing import CliRunner from discolike_cli.main import app @@ -15,10 +15,10 @@ def test_validate_icp_with_domain_options_posts_json(install_build_client: Callable[[Handler], None]) -> None: captured: dict[str, object] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["path"] = request.url.path captured["body"] = json.loads(request.content) - return httpx.Response(200, json={"task_id": "vi-1"}) + return httpx2.Response(200, json={"task_id": "vi-1"}) install_build_client(handler) result = runner.invoke( @@ -38,9 +38,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_validate_icp_sends_web_search_when_passed(install_build_client: Callable[[Handler], None]) -> None: captured: dict[str, object] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["body"] = json.loads(request.content) - return httpx.Response(200, json={"task_id": "vi-1b"}) + return httpx2.Response(200, json={"task_id": "vi-1b"}) install_build_client(handler) result = runner.invoke( @@ -62,9 +62,9 @@ def test_validate_icp_with_file_reads_domains_and_strips_blanks( domains_file.write_text("acme.com\n\n beta.com \n") captured: dict[str, object] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["body"] = json.loads(request.content) - return httpx.Response(200, json={"task_id": "vi-2"}) + return httpx2.Response(200, json={"task_id": "vi-2"}) install_build_client(handler) result = runner.invoke(app, ["validate-icp", "--icp", "VPs", "--file", str(domains_file)]) @@ -76,8 +76,8 @@ def test_validate_icp_both_domain_and_file_exits_2(tmp_path, install_build_clien domains_file = tmp_path / "domains.txt" domains_file.write_text("acme.com\n") - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"task_id": "vi-3"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"task_id": "vi-3"}) install_build_client(handler) result = runner.invoke(app, ["validate-icp", "--icp", "VPs", "--domain", "acme.com", "--file", str(domains_file)]) @@ -85,8 +85,8 @@ def handler(request: httpx.Request) -> httpx.Response: def test_validate_icp_neither_domain_nor_file_exits_2(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"task_id": "vi-4"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"task_id": "vi-4"}) install_build_client(handler) result = runner.invoke(app, ["validate-icp", "--icp", "VPs"]) @@ -96,14 +96,14 @@ def handler(request: httpx.Request) -> httpx.Response: def test_validate_icp_with_wait_polls_to_completion(install_build_client: Callable[[Handler], None]) -> None: statuses = iter( [ - httpx.Response(200, json={"status": "processing", "progress": 20}), - httpx.Response(200, json={"status": "completed", "progress": 100, "results": [{"domain": "acme.com"}]}), + httpx2.Response(200, json={"status": "processing", "progress": 20}), + httpx2.Response(200, json={"status": "completed", "progress": 100, "results": [{"domain": "acme.com"}]}), ] ) - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.url.path == "/v1/validate/icp": - return httpx.Response(200, json={"task_id": "vi-5"}) + return httpx2.Response(200, json={"task_id": "vi-5"}) assert request.url.path == "/v1/discogen/status/vi-5" return next(statuses) @@ -121,9 +121,9 @@ def test_append_json_response_emits_list(tmp_path, install_build_client: Callabl input_file = tmp_path / "domains.csv" input_file.write_text("domain\nacme.com\n") - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.path == "/v1/append" - return httpx.Response(200, json=[{"domain": "acme.com"}]) + return httpx2.Response(200, json=[{"domain": "acme.com"}]) install_build_client(handler) result = runner.invoke(app, ["append", str(input_file), "--dataset", "bizdata"]) @@ -136,8 +136,8 @@ def test_append_csv_response_writes_output_file(tmp_path, install_build_client: input_file.write_text("domain\nacme.com\n") output_file = tmp_path / "enriched.csv" - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, content=b"domain,industry\nacme.com,SAAS\n", headers={"Content-Type": "text/csv"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, content=b"domain,industry\nacme.com,SAAS\n", headers={"Content-Type": "text/csv"}) install_build_client(handler) result = runner.invoke(app, ["append", str(input_file), "--csv", "--output", str(output_file)]) @@ -152,8 +152,8 @@ def test_append_csv_response_without_output_exits_2(tmp_path, install_build_clie input_file = tmp_path / "domains.csv" input_file.write_text("domain\nacme.com\n") - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, content=b"domain\nacme.com\n", headers={"Content-Type": "text/csv"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, content=b"domain\nacme.com\n", headers={"Content-Type": "text/csv"}) install_build_client(handler) result = runner.invoke(app, ["append", str(input_file), "--csv"]) @@ -161,11 +161,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_segment_with_domain_options_prints_task_hint(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"task_id": "seg-1"}) + return httpx2.Response(200, json={"task_id": "seg-1"}) install_build_client(handler) result = runner.invoke(app, ["segment", "--domain", "acme.com", "--domain", "beta.com", "--max-segments", "3"]) @@ -182,11 +182,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_segment_with_file_posts_upload(tmp_path, install_build_client: Callable[[Handler], None]) -> None: domains_file = tmp_path / "domains.csv" domains_file.write_text("domain\nacme.com\n") - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"task_id": "seg-2"}) + return httpx2.Response(200, json={"task_id": "seg-2"}) install_build_client(handler) result = runner.invoke(app, ["segment", "--file", str(domains_file)]) @@ -201,8 +201,8 @@ def test_segment_both_domain_and_file_exits_2(tmp_path, install_build_client: Ca domains_file = tmp_path / "domains.csv" domains_file.write_text("domain\nacme.com\n") - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"task_id": "seg-3"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"task_id": "seg-3"}) install_build_client(handler) result = runner.invoke(app, ["segment", "--domain", "acme.com", "--file", str(domains_file)]) @@ -210,8 +210,8 @@ def handler(request: httpx.Request) -> httpx.Response: def test_segment_neither_domain_nor_file_exits_2(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"task_id": "seg-4"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"task_id": "seg-4"}) install_build_client(handler) result = runner.invoke(app, ["segment"]) diff --git a/packages/discolike-cli/tests/test_match_cli.py b/packages/discolike-cli/tests/test_match_cli.py index 8358a9d..f53fcc2 100644 --- a/packages/discolike-cli/tests/test_match_cli.py +++ b/packages/discolike-cli/tests/test_match_cli.py @@ -3,7 +3,7 @@ import json from collections.abc import Callable -import httpx +import httpx2 import pytest from typer.testing import CliRunner @@ -14,11 +14,11 @@ def test_match_single_name_hits_match_endpoint(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response( + return httpx2.Response( 200, json={"query": {"name": "Acme"}, "matches": [{"domain": "acme.com", "match_confidence": 98.0}]}, ) @@ -41,14 +41,14 @@ def test_match_bulk_file_with_wait_polls_to_completion( names_file.write_text("company\nAcme\n") statuses = iter( [ - httpx.Response(200, json={"status": "processing", "progress": 40}), - httpx.Response(200, json={"status": "completed", "progress": 100, "results": [{"domain": "acme.com"}]}), + httpx2.Response(200, json={"status": "processing", "progress": 40}), + httpx2.Response(200, json={"status": "completed", "progress": 100, "results": [{"domain": "acme.com"}]}), ] ) - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.url.path == "/v1/bulkmatch": - return httpx.Response(200, json={"task_id": "bm-1"}) + return httpx2.Response(200, json={"task_id": "bm-1"}) assert request.url.path == "/v1/bulkmatch/status/bm-1" return next(statuses) @@ -68,11 +68,11 @@ def test_match_bulk_file_with_wait_format_table_renders_table( names_file = tmp_path / "names.csv" names_file.write_text("company\nAcme\n") - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.url.path == "/v1/bulkmatch": - return httpx.Response(200, json={"task_id": "bm-5"}) + return httpx2.Response(200, json={"task_id": "bm-5"}) assert request.url.path == "/v1/bulkmatch/status/bm-5" - return httpx.Response(200, json={"status": "completed", "progress": 100, "results": [{"domain": "acme.com"}]}) + return httpx2.Response(200, json={"status": "completed", "progress": 100, "results": [{"domain": "acme.com"}]}) install_build_client(handler) result = runner.invoke( @@ -103,9 +103,9 @@ def test_match_bulk_file_without_wait_prints_task_hint( names_file = tmp_path / "names.csv" names_file.write_text("name\nAcme\n") - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.path == "/v1/bulkmatch" - return httpx.Response(200, json={"task_id": "bm-2"}) + return httpx2.Response(200, json={"task_id": "bm-2"}) install_build_client(handler) result = runner.invoke(app, ["match", "--file", str(names_file)]) @@ -119,8 +119,8 @@ def test_match_both_name_and_file_exits_2(tmp_path, install_build_client: Callab names_file = tmp_path / "names.csv" names_file.write_text("name\nAcme\n") - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={}) install_build_client(handler) result = runner.invoke(app, ["match", "Acme", "--file", str(names_file)]) @@ -128,8 +128,8 @@ def handler(request: httpx.Request) -> httpx.Response: def test_match_neither_name_nor_file_exits_2(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={}) install_build_client(handler) result = runner.invoke(app, ["match"]) @@ -137,11 +137,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_match_passes_optional_filters(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"domain": "acme.com"}) + return httpx2.Response(200, json={"domain": "acme.com"}) install_build_client(handler) result = runner.invoke( @@ -175,11 +175,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_match_local_mode_omitted_when_not_passed(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"domain": "acme.com"}) + return httpx2.Response(200, json={"domain": "acme.com"}) install_build_client(handler) result = runner.invoke(app, ["match", "Acme"]) @@ -192,11 +192,11 @@ def test_match_bulk_local_mode_omitted_when_not_passed( ) -> None: names_file = tmp_path / "names.csv" names_file.write_text("name\nAcme\n") - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"task_id": "bm-3"}) + return httpx2.Response(200, json={"task_id": "bm-3"}) install_build_client(handler) result = runner.invoke(app, ["match", "--file", str(names_file)]) @@ -207,11 +207,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_match_bulk_local_mode_sent_when_passed(tmp_path, install_build_client: Callable[[Handler], None]) -> None: names_file = tmp_path / "names.csv" names_file.write_text("name\nAcme\n") - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"task_id": "bm-4"}) + return httpx2.Response(200, json={"task_id": "bm-4"}) install_build_client(handler) result = runner.invoke(app, ["match", "--file", str(names_file), "--local-mode"]) diff --git a/packages/discolike-cli/tests/test_providers_cli.py b/packages/discolike-cli/tests/test_providers_cli.py index 7465cb0..b365d5a 100644 --- a/packages/discolike-cli/tests/test_providers_cli.py +++ b/packages/discolike-cli/tests/test_providers_cli.py @@ -3,7 +3,7 @@ import json from collections.abc import Callable -import httpx +import httpx2 from typer.testing import CliRunner from discolike_cli.main import app @@ -13,11 +13,11 @@ def test_search_providers_list_hits_collection(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"providers": []}) + return httpx2.Response(200, json={"providers": []}) install_build_client(handler) result = runner.invoke(app, ["search-providers", "list"]) @@ -29,11 +29,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_search_providers_create_posts_json(install_build_client: Callable[[Handler], None]) -> None: captured: dict[str, object] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["path"] = request.url.path captured["method"] = request.method captured["body"] = json.loads(request.content) - return httpx.Response(200, json={"integration_id": "sp1", "integration_name": "Tavily"}) + return httpx2.Response(200, json={"integration_id": "sp1", "integration_name": "Tavily"}) install_build_client(handler) result = runner.invoke( @@ -65,10 +65,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_search_providers_set_default_puts_subroute(install_build_client: Callable[[Handler], None]) -> None: captured: dict[str, object] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["path"] = request.url.path captured["method"] = request.method - return httpx.Response(200, json={"message": "ok", "integration_id": "sp2"}) + return httpx2.Response(200, json={"message": "ok", "integration_id": "sp2"}) install_build_client(handler) result = runner.invoke(app, ["search-providers", "set-default", "sp2"]) @@ -78,10 +78,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_search_providers_delete_emits_deleted(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.path == "/v1/search-providers/sp3" assert request.method == "DELETE" - return httpx.Response(200, json={"message": "ok"}) + return httpx2.Response(200, json={"message": "ok"}) install_build_client(handler) result = runner.invoke(app, ["search-providers", "delete", "sp3"]) @@ -90,11 +90,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_search_providers_models_hits_models_route(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"models": {}}) + return httpx2.Response(200, json={"models": {}}) install_build_client(handler) result = runner.invoke(app, ["search-providers", "models"]) @@ -105,11 +105,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_llm_providers_create_posts_json(install_build_client: Callable[[Handler], None]) -> None: captured: dict[str, object] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["path"] = request.url.path captured["method"] = request.method captured["body"] = json.loads(request.content) - return httpx.Response(200, json={"message": "created", "integration_id": "llm1"}) + return httpx2.Response(200, json={"message": "created", "integration_id": "llm1"}) install_build_client(handler) result = runner.invoke( @@ -141,11 +141,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_llm_providers_update_keeps_null_api_key(install_build_client: Callable[[Handler], None]) -> None: captured: dict[str, object] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["path"] = request.url.path captured["method"] = request.method captured["body"] = json.loads(request.content) - return httpx.Response(200, json={"message": "updated"}) + return httpx2.Response(200, json={"message": "updated"}) install_build_client(handler) result = runner.invoke( @@ -176,10 +176,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_llm_providers_test_connection_posts_body(install_build_client: Callable[[Handler], None]) -> None: captured: dict[str, object] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["path"] = request.url.path captured["body"] = json.loads(request.content) - return httpx.Response(200, json={"status": "success", "message": "ok"}) + return httpx2.Response(200, json={"status": "success", "message": "ok"}) install_build_client(handler) result = runner.invoke( @@ -206,11 +206,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_llm_providers_get_hits_config_item(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"integration_id": "llm3", "integration_name": "Anthropic"}) + return httpx2.Response(200, json={"integration_id": "llm3", "integration_name": "Anthropic"}) install_build_client(handler) result = runner.invoke(app, ["llm-providers", "get", "llm3"]) diff --git a/packages/discolike-cli/tests/test_queries_cli.py b/packages/discolike-cli/tests/test_queries_cli.py index 893147b..64154f0 100644 --- a/packages/discolike-cli/tests/test_queries_cli.py +++ b/packages/discolike-cli/tests/test_queries_cli.py @@ -5,7 +5,7 @@ from collections.abc import Callable from pathlib import Path -import httpx +import httpx2 from typer.testing import CliRunner from discolike_cli.main import app @@ -15,11 +15,11 @@ def test_queries_list_sends_params(install_build_client: Callable[[Handler], None]) -> None: - captured: dict[str, httpx.Request] = {} + captured: dict[str, httpx2.Request] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["request"] = request - return httpx.Response(200, json={"results": [{"query_id": "q1"}]}) + return httpx2.Response(200, json={"results": [{"query_id": "q1"}]}) install_build_client(handler) result = runner.invoke( @@ -38,10 +38,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_queries_create_exclusion_list_posts_json(install_build_client: Callable[[Handler], None]) -> None: captured: dict[str, object] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["path"] = request.url.path captured["body"] = json.loads(request.content) - return httpx.Response(200, json={"query_id": "q2", "query_name": "My List"}) + return httpx2.Response(200, json={"query_id": "q2", "query_name": "My List"}) install_build_client(handler) result = runner.invoke( @@ -72,11 +72,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_queries_update_patches_body(install_build_client: Callable[[Handler], None]) -> None: captured: dict[str, object] = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: captured["path"] = request.url.path captured["method"] = request.method captured["body"] = json.loads(request.content) - return httpx.Response(200, json={"query_id": "q3", "query_name": "Renamed"}) + return httpx2.Response(200, json={"query_id": "q3", "query_name": "Renamed"}) install_build_client(handler) result = runner.invoke(app, ["queries", "update", "q3", "--name", "Renamed", "--tag", "hot"]) @@ -87,10 +87,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_queries_delete_hits_delete_endpoint(install_build_client: Callable[[Handler], None]) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.path == "/v1/queries/q4" assert request.method == "DELETE" - return httpx.Response(200, json={"message": "ok"}) + return httpx2.Response(200, json={"message": "ok"}) install_build_client(handler) result = runner.invoke(app, ["queries", "delete", "q4"]) @@ -104,7 +104,7 @@ def test_queries_save_results_json_input(install_build_client: Callable[[Handler def handler(request): captured["path"] = request.url.path captured["body"] = json.loads(request.content) - return httpx.Response(200, json={"query_id": "q6", "row_count": 2}) + return httpx2.Response(200, json={"query_id": "q6", "row_count": 2}) install_build_client(handler) f = tmp_path / "rows.json" @@ -125,7 +125,7 @@ def test_queries_save_results_csv_input(install_build_client: Callable[[Handler] def handler(request): captured["body"] = json.loads(request.content) - return httpx.Response(200, json={"query_id": "q7"}) + return httpx2.Response(200, json={"query_id": "q7"}) install_build_client(handler) f = tmp_path / "rows.csv" @@ -166,7 +166,7 @@ def handler(request): def test_queries_save_results_invalid_action_rejected( install_build_client: Callable[[Handler], None], tmp_path: Path ) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: raise AssertionError("handler should not be reached for an invalid --action") install_build_client(handler) diff --git a/packages/discolike-testkit/pyproject.toml b/packages/discolike-testkit/pyproject.toml index 0cdc4ed..4534e5f 100644 --- a/packages/discolike-testkit/pyproject.toml +++ b/packages/discolike-testkit/pyproject.toml @@ -10,7 +10,7 @@ license = "MIT" requires-python = ">=3.10" dependencies = [ "discolike", - "httpx>=0.27", + "httpx2>=2.9", "pytest>=8", ] # Guards against an accidental `uv publish` picking this up; the publish diff --git a/packages/discolike-testkit/src/discolike_testkit/__init__.py b/packages/discolike-testkit/src/discolike_testkit/__init__.py index f5bfd30..c6a084d 100644 --- a/packages/discolike-testkit/src/discolike_testkit/__init__.py +++ b/packages/discolike-testkit/src/discolike_testkit/__init__.py @@ -9,13 +9,13 @@ from collections.abc import Callable -import httpx +import httpx2 from discolike import AsyncDiscolike from discolike import Discolike __all__ = ["AsyncClientFactory", "ClientFactory", "Handler"] -Handler = Callable[[httpx.Request], httpx.Response] +Handler = Callable[[httpx2.Request], httpx2.Response] ClientFactory = Callable[[Handler], Discolike] AsyncClientFactory = Callable[[Handler], AsyncDiscolike] diff --git a/packages/discolike-testkit/src/discolike_testkit/plugin.py b/packages/discolike-testkit/src/discolike_testkit/plugin.py index 524904f..b80ebc6 100644 --- a/packages/discolike-testkit/src/discolike_testkit/plugin.py +++ b/packages/discolike-testkit/src/discolike_testkit/plugin.py @@ -19,7 +19,7 @@ from pathlib import Path -import httpx +import httpx2 import pytest from discolike import AsyncDiscolike @@ -42,7 +42,7 @@ def make_client() -> ClientFactory: """Build a sync client whose transport is backed by ``handler``.""" def _make(handler: Handler) -> Discolike: - http = httpx.Client(transport=httpx.MockTransport(handler), base_url=BASE_URL) + http = httpx2.Client(transport=httpx2.MockTransport(handler), base_url=BASE_URL) return Discolike(api_key="test-key", base_url=BASE_URL, http_client=http) return _make @@ -53,7 +53,7 @@ def make_async_client() -> AsyncClientFactory: """Build an async client whose transport is backed by ``handler``.""" def _make(handler: Handler) -> AsyncDiscolike: - http = httpx.AsyncClient(transport=httpx.MockTransport(handler), base_url=BASE_URL) + http = httpx2.AsyncClient(transport=httpx2.MockTransport(handler), base_url=BASE_URL) return AsyncDiscolike(api_key="test-key", base_url=BASE_URL, http_client=http) return _make diff --git a/packages/discolike/pyproject.toml b/packages/discolike/pyproject.toml index 4ea882e..875085c 100644 --- a/packages/discolike/pyproject.toml +++ b/packages/discolike/pyproject.toml @@ -12,7 +12,7 @@ license-files = ["LICENSE"] requires-python = ">=3.10" authors = [{ name = "DiscoLike", email = "support@discolike.com" }] dependencies = [ - "httpx>=0.27", + "httpx2>=2.9", "pydantic>=2.7", "typing-extensions>=4.1", ] @@ -33,7 +33,7 @@ classifiers = [ ] [project.optional-dependencies] -cli = ["discolike-cli>=0.1.2"] +cli = ["discolike-cli==0.2.0"] [project.urls] Homepage = "https://www.discolike.com" diff --git a/packages/discolike/src/discolike/_client.py b/packages/discolike/src/discolike/_client.py index e23aaaf..483c6e7 100644 --- a/packages/discolike/src/discolike/_client.py +++ b/packages/discolike/src/discolike/_client.py @@ -3,7 +3,7 @@ import pathlib from typing import BinaryIO -import httpx +import httpx2 from discolike._config import DEFAULT_BASE_URL from discolike._config import resolve_api_key @@ -51,7 +51,7 @@ def __init__( base_url: str = DEFAULT_BASE_URL, timeout: float = DEFAULT_TIMEOUT_SECONDS, max_retries: int = DEFAULT_MAX_RETRIES, - http_client: httpx.Client | None = None, + http_client: httpx2.Client | None = None, ) -> None: self._attach( Transport( @@ -78,7 +78,7 @@ def _attach(self, transport: Transport) -> None: self._validate = ValidateResource(self._transport) self._enrich = EnrichResource(self._transport) - def with_options(self, *, timeout: float | httpx.Timeout) -> Discolike: + def with_options(self, *, timeout: float | httpx2.Timeout) -> Discolike: """A client view with a different request timeout, sharing this client's connection pool.""" clone = object.__new__(Discolike) clone._attach(self._transport.with_timeout(timeout)) @@ -301,7 +301,7 @@ def __init__( base_url: str = DEFAULT_BASE_URL, timeout: float = DEFAULT_TIMEOUT_SECONDS, max_retries: int = DEFAULT_MAX_RETRIES, - http_client: httpx.AsyncClient | None = None, + http_client: httpx2.AsyncClient | None = None, ) -> None: self._attach( AsyncTransport( @@ -328,7 +328,7 @@ def _attach(self, transport: AsyncTransport) -> None: self._validate = AsyncValidateResource(self._transport) self._enrich = AsyncEnrichResource(self._transport) - def with_options(self, *, timeout: float | httpx.Timeout) -> AsyncDiscolike: + def with_options(self, *, timeout: float | httpx2.Timeout) -> AsyncDiscolike: """A client view with a different request timeout, sharing this client's connection pool.""" clone = object.__new__(AsyncDiscolike) clone._attach(self._transport.with_timeout(timeout)) diff --git a/packages/discolike/src/discolike/_exceptions.py b/packages/discolike/src/discolike/_exceptions.py index 04971f3..23436b0 100644 --- a/packages/discolike/src/discolike/_exceptions.py +++ b/packages/discolike/src/discolike/_exceptions.py @@ -3,7 +3,7 @@ import json from typing import Any -import httpx +import httpx2 class DiscolikeError(Exception): @@ -66,7 +66,7 @@ def __init__( } -def _extract_message(response: httpx.Response) -> tuple[str, Any]: +def _extract_message(response: httpx2.Response) -> tuple[str, Any]: try: payload = response.json() except ValueError: @@ -85,7 +85,7 @@ def _extract_message(response: httpx.Response) -> tuple[str, Any]: return json.dumps(payload)[:500], payload -def raise_for_status(response: httpx.Response) -> None: +def raise_for_status(response: httpx2.Response) -> None: if response.status_code < 400: return message, payload = _extract_message(response) diff --git a/packages/discolike/src/discolike/_transport.py b/packages/discolike/src/discolike/_transport.py index c0a5922..95296cd 100644 --- a/packages/discolike/src/discolike/_transport.py +++ b/packages/discolike/src/discolike/_transport.py @@ -5,7 +5,7 @@ from collections.abc import Mapping from typing import Any -import httpx +import httpx2 from discolike._exceptions import APIConnectionError from discolike._exceptions import raise_for_status @@ -14,7 +14,7 @@ IDEMPOTENT_METHODS = frozenset({"GET", "DELETE"}) RETRYABLE_STATUSES = frozenset({429, 502, 503, 504}) NON_IDEMPOTENT_RETRYABLE_STATUSES = frozenset({429}) -NON_IDEMPOTENT_RETRYABLE_EXCEPTIONS: tuple[type[Exception], ...] = (httpx.ConnectError,) +NON_IDEMPOTENT_RETRYABLE_EXCEPTIONS: tuple[type[Exception], ...] = (httpx2.ConnectError,) BACKOFF_BASE_SECONDS = 0.5 @@ -31,10 +31,10 @@ def _retryable_statuses(method: str) -> frozenset[int]: def _retryable_exceptions(method: str) -> tuple[type[Exception], ...]: - return (httpx.TransportError,) if method in IDEMPOTENT_METHODS else NON_IDEMPOTENT_RETRYABLE_EXCEPTIONS + return (httpx2.TransportError,) if method in IDEMPOTENT_METHODS else NON_IDEMPOTENT_RETRYABLE_EXCEPTIONS -def _retry_delay(response: httpx.Response | None, attempt: int) -> float: +def _retry_delay(response: httpx2.Response | None, attempt: int) -> float: if response is not None: header = response.headers.get("Retry-After") if header and header.replace(".", "", 1).isdigit(): @@ -50,17 +50,17 @@ def __init__( base_url: str, timeout: float, max_retries: int, - http_client: httpx.Client | None = None, + http_client: httpx2.Client | None = None, ) -> None: if http_client is not None and not str(http_client.base_url): http_client.base_url = base_url - self._client = http_client or httpx.Client(base_url=base_url, timeout=timeout) + self._client = http_client or httpx2.Client(base_url=base_url, timeout=timeout) self._client.headers.update(_default_headers(api_key)) self._max_retries = max_retries - self._timeout_override: float | httpx.Timeout | None = None + self._timeout_override: float | httpx2.Timeout | None = None self._is_view = False - def with_timeout(self, timeout: float | httpx.Timeout) -> Transport: + def with_timeout(self, timeout: float | httpx2.Timeout) -> Transport: clone = object.__new__(Transport) clone._client = self._client clone._max_retries = self._max_retries @@ -74,12 +74,12 @@ def request( path: str, *, params: Mapping[str, Any] | None = None, - json_body: Any = None, # noqa: ANN401 -- forwarded verbatim to httpx.Client.request - files: Any = None, # noqa: ANN401 -- forwarded verbatim to httpx.Client.request - data: Any = None, # noqa: ANN401 -- forwarded verbatim to httpx.Client.request - ) -> httpx.Response: + json_body: Any = None, # noqa: ANN401 -- forwarded verbatim to httpx2.Client.request + files: Any = None, # noqa: ANN401 -- forwarded verbatim to httpx2.Client.request + data: Any = None, # noqa: ANN401 -- forwarded verbatim to httpx2.Client.request + ) -> httpx2.Response: clean_params = drop_none(params) - timeout = self._timeout_override if self._timeout_override is not None else httpx.USE_CLIENT_DEFAULT + timeout = self._timeout_override if self._timeout_override is not None else httpx2.USE_CLIENT_DEFAULT retryable_statuses = _retryable_statuses(method) retryable_exceptions = _retryable_exceptions(method) for attempt in range(self._max_retries + 1): @@ -92,7 +92,7 @@ def request( raise APIConnectionError(f"Connection to DiscoLike API failed: {exc}") from exc time.sleep(_retry_delay(None, attempt)) continue - except httpx.TransportError as exc: + except httpx2.TransportError as exc: raise APIConnectionError(f"Connection to DiscoLike API failed: {exc}") from exc if response.status_code in retryable_statuses and attempt < self._max_retries: time.sleep(_retry_delay(response, attempt)) @@ -114,17 +114,17 @@ def __init__( base_url: str, timeout: float, max_retries: int, - http_client: httpx.AsyncClient | None = None, + http_client: httpx2.AsyncClient | None = None, ) -> None: if http_client is not None and not str(http_client.base_url): http_client.base_url = base_url - self._client = http_client or httpx.AsyncClient(base_url=base_url, timeout=timeout) + self._client = http_client or httpx2.AsyncClient(base_url=base_url, timeout=timeout) self._client.headers.update(_default_headers(api_key)) self._max_retries = max_retries - self._timeout_override: float | httpx.Timeout | None = None + self._timeout_override: float | httpx2.Timeout | None = None self._is_view = False - def with_timeout(self, timeout: float | httpx.Timeout) -> AsyncTransport: + def with_timeout(self, timeout: float | httpx2.Timeout) -> AsyncTransport: clone = object.__new__(AsyncTransport) clone._client = self._client clone._max_retries = self._max_retries @@ -138,12 +138,12 @@ async def request( path: str, *, params: Mapping[str, Any] | None = None, - json_body: Any = None, # noqa: ANN401 -- forwarded verbatim to httpx.Client.request - files: Any = None, # noqa: ANN401 -- forwarded verbatim to httpx.Client.request - data: Any = None, # noqa: ANN401 -- forwarded verbatim to httpx.Client.request - ) -> httpx.Response: + json_body: Any = None, # noqa: ANN401 -- forwarded verbatim to httpx2.Client.request + files: Any = None, # noqa: ANN401 -- forwarded verbatim to httpx2.Client.request + data: Any = None, # noqa: ANN401 -- forwarded verbatim to httpx2.Client.request + ) -> httpx2.Response: clean_params = drop_none(params) - timeout = self._timeout_override if self._timeout_override is not None else httpx.USE_CLIENT_DEFAULT + timeout = self._timeout_override if self._timeout_override is not None else httpx2.USE_CLIENT_DEFAULT retryable_statuses = _retryable_statuses(method) retryable_exceptions = _retryable_exceptions(method) for attempt in range(self._max_retries + 1): @@ -156,7 +156,7 @@ async def request( raise APIConnectionError(f"Connection to DiscoLike API failed: {exc}") from exc await asyncio.sleep(_retry_delay(None, attempt)) continue - except httpx.TransportError as exc: + except httpx2.TransportError as exc: raise APIConnectionError(f"Connection to DiscoLike API failed: {exc}") from exc if response.status_code in retryable_statuses and attempt < self._max_retries: await asyncio.sleep(_retry_delay(response, attempt)) diff --git a/packages/discolike/src/discolike/_version.py b/packages/discolike/src/discolike/_version.py index b3f4756..d3ec452 100644 --- a/packages/discolike/src/discolike/_version.py +++ b/packages/discolike/src/discolike/_version.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.2.0" diff --git a/packages/discolike/tests/test_client.py b/packages/discolike/tests/test_client.py index 51b24a1..8dfd2d8 100644 --- a/packages/discolike/tests/test_client.py +++ b/packages/discolike/tests/test_client.py @@ -1,4 +1,4 @@ -import httpx +import httpx2 import pytest from discolike import AuthenticationError @@ -19,9 +19,9 @@ def test_client_reads_env_key(monkeypatch) -> None: def test_usage(monkeypatch, make_client: ClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.path == "/v1/usage" - return httpx.Response(200, json={"requests_mtd": 42, "spend_mtd": 1.5}) + return httpx2.Response(200, json={"requests_mtd": 42, "spend_mtd": 1.5}) with make_client(handler) as client: usage = client.account.usage() @@ -29,8 +29,8 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_usage_async(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"requests_mtd": 7}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"requests_mtd": 7}) async with make_async_client(handler) as client: usage = await client.account.usage() @@ -47,9 +47,9 @@ def test_route_metadata_stamped() -> None: def test_with_options_timeout_applies_only_to_the_view(make_client: ClientFactory) -> None: seen = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen.append(request.extensions["timeout"]) - return httpx.Response(200, json={"balance": 1}) + return httpx2.Response(200, json={"balance": 1}) with make_client(handler) as client: client.with_options(timeout=120.0).account.usage() @@ -62,9 +62,9 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_with_options_timeout_async(make_async_client: AsyncClientFactory) -> None: seen = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen.append(request.extensions["timeout"]) - return httpx.Response(200, json={"balance": 1}) + return httpx2.Response(200, json={"balance": 1}) async with make_async_client(handler) as client: await client.with_options(timeout=90.0).account.usage() @@ -75,8 +75,8 @@ def handler(request: httpx.Request) -> httpx.Response: def test_closing_a_with_options_view_leaves_parent_usable(make_client: ClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"balance": 1}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"balance": 1}) with make_client(handler) as client: with client.with_options(timeout=30.0) as view: @@ -85,8 +85,8 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_closing_a_with_options_view_leaves_parent_usable_async(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"balance": 1}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"balance": 1}) async with make_async_client(handler) as client: async with client.with_options(timeout=30.0) as view: diff --git a/packages/discolike/tests/test_companies.py b/packages/discolike/tests/test_companies.py index a1bcefb..42971ef 100644 --- a/packages/discolike/tests/test_companies.py +++ b/packages/discolike/tests/test_companies.py @@ -1,4 +1,4 @@ -import httpx +import httpx2 import pytest from discolike_testkit import AsyncClientFactory @@ -23,10 +23,10 @@ def test_companies_methods(method: str, kwargs: dict, path: str, make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path - seen["params"] = dict(httpx.QueryParams(request.url.query)) - return httpx.Response(200, json={"domain": "acme.com", "anything": 1}) + seen["params"] = dict(httpx2.QueryParams(request.url.query)) + return httpx2.Response(200, json={"domain": "acme.com", "anything": 1}) with make_client(handler) as client: result = getattr(client.companies, method)(**kwargs) @@ -43,10 +43,10 @@ async def test_companies_methods_async( ) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path - seen["params"] = dict(httpx.QueryParams(request.url.query)) - return httpx.Response(200, json={"domain": "acme.com", "anything": 1}) + seen["params"] = dict(httpx2.QueryParams(request.url.query)) + return httpx2.Response(200, json={"domain": "acme.com", "anything": 1}) async with make_async_client(handler) as client: result = await getattr(client.companies, method)(**kwargs) @@ -61,10 +61,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_companies_list_methods(method: str, kwargs: dict, path: str, make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path - seen["params"] = dict(httpx.QueryParams(request.url.query)) - return httpx.Response(200, json=[{"linked_domain": "acme.io", "anything": 1}]) + seen["params"] = dict(httpx2.QueryParams(request.url.query)) + return httpx2.Response(200, json=[{"linked_domain": "acme.io", "anything": 1}]) with make_client(handler) as client: result = getattr(client.companies, method)(**kwargs) @@ -81,8 +81,8 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_companies_list_methods_async( method: str, kwargs: dict, path: str, make_async_client: AsyncClientFactory ) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json=[{"linked_domain": "acme.io"}]) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json=[{"linked_domain": "acme.io"}]) async with make_async_client(handler) as client: result = await getattr(client.companies, method)(**kwargs) @@ -92,8 +92,8 @@ def handler(request: httpx.Request) -> httpx.Response: def test_extract_parses_text_and_language(make_client: ClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"language": "en", "text": "Acme makes widgets."}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"language": "en", "text": "Acme makes widgets."}) with make_client(handler) as client: result = client.companies.extract(domain="acme.com") diff --git a/packages/discolike/tests/test_contacts.py b/packages/discolike/tests/test_contacts.py index 4de2a21..2ef38f4 100644 --- a/packages/discolike/tests/test_contacts.py +++ b/packages/discolike/tests/test_contacts.py @@ -2,7 +2,7 @@ import json -import httpx +import httpx2 from discolike._jobs import FAMILY_CONTACTMATCH from discolike._jobs import FAMILY_DISCOGEN @@ -15,11 +15,11 @@ def test_search_builds_query_and_parses_list(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method - seen["params"] = httpx.QueryParams(request.url.query) - return httpx.Response( + seen["params"] = httpx2.QueryParams(request.url.query) + return httpx2.Response( 200, json=[{"persona_id": 1, "domain": "acme.com", "name": "Jane Doe", "extra_field": "kept"}], ) @@ -45,11 +45,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_count(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method - seen["params"] = httpx.QueryParams(request.url.query) - return httpx.Response(200, json={"count": 1234}) + seen["params"] = httpx2.QueryParams(request.url.query) + return httpx2.Response(200, json={"count": 1234}) with make_client(handler) as client: result = client.contacts.count(seniority=["vp"], has_email=True, jobstart_date="2025-01-01") @@ -65,10 +65,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_lookup(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path - seen["params"] = httpx.QueryParams(request.url.query) - return httpx.Response( + seen["params"] = httpx2.QueryParams(request.url.query) + return httpx2.Response( 200, json={"persona_id": 12345678, "name": "Jane Doe", "domain": "example.com"}, ) @@ -86,10 +86,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_match(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path - seen["params"] = httpx.QueryParams(request.url.query) - return httpx.Response( + seen["params"] = httpx2.QueryParams(request.url.query) + return httpx2.Response( 200, json={ "query": {"name": "Jane Doe", "company_name": "Acme Corp", "domain": None}, @@ -120,11 +120,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_bulk_match_posts_json_and_returns_job(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"task_id": "cm-1"}) + return httpx2.Response(200, json={"task_id": "cm-1"}) with make_client(handler) as client: job = client.contacts.bulk_match( @@ -148,11 +148,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_discover_posts_json_body(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method seen["body"] = json.loads(request.content) - return httpx.Response( + return httpx2.Response( 200, json={ "results": {"acme.com": {"domain": "acme.com", "contacts": []}}, @@ -187,11 +187,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_generate_posts_json_and_returns_job(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"task_id": "dg-1"}) + return httpx2.Response(200, json={"task_id": "dg-1"}) with make_client(handler) as client: job = client.contacts.generate( @@ -213,8 +213,8 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_search_async(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json=[{"persona_id": 2, "domain": "b.com"}]) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json=[{"persona_id": 2, "domain": "b.com"}]) async with make_async_client(handler) as client: results = await client.contacts.search(domain=["b.com"]) @@ -222,8 +222,8 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_bulk_match_async_returns_async_job(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"task_id": "cm-2"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"task_id": "cm-2"}) async with make_async_client(handler) as client: job = await client.contacts.bulk_match(queries=[{"name": "Jane Doe"}]) @@ -234,8 +234,8 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_generate_async_returns_async_job(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"task_id": "dg-2"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"task_id": "dg-2"}) async with make_async_client(handler) as client: job = await client.contacts.generate(icp_text="VPs", domains=["a.com"]) diff --git a/packages/discolike/tests/test_discogen.py b/packages/discolike/tests/test_discogen.py index 25a78c0..eca08a4 100644 --- a/packages/discolike/tests/test_discogen.py +++ b/packages/discolike/tests/test_discogen.py @@ -2,7 +2,7 @@ import json -import httpx +import httpx2 import pytest from discolike._jobs import FAMILY_DISCOGEN @@ -15,11 +15,11 @@ def test_process_posts_json_and_returns_job(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"task_id": "dg-1"}) + return httpx2.Response(200, json={"task_id": "dg-1"}) with make_client(handler) as client: job = client.discogen.process( @@ -43,9 +43,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_process_drops_unset_optionals(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"task_id": "dg-2"}) + return httpx2.Response(200, json={"task_id": "dg-2"}) with make_client(handler) as client: client.discogen.process(query="q", domains=["a.com"]) @@ -56,9 +56,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_process_all_optionals_present(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"task_id": "dg-3"}) + return httpx2.Response(200, json={"task_id": "dg-3"}) with make_client(handler) as client: client.discogen.process( @@ -87,11 +87,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_process_personas_posts_json_and_returns_job(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"task_id": "dg-4"}) + return httpx2.Response(200, json={"task_id": "dg-4"}) with make_client(handler) as client: job = client.discogen.process_personas( @@ -110,10 +110,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_models(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method - return httpx.Response( + return httpx2.Response( 200, json={ "models": { @@ -134,7 +134,7 @@ def handler(request: httpx.Request) -> httpx.Response: def test_job_reattaches_without_http_call(make_client: ClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: pytest.fail("job() must not perform an HTTP request") with make_client(handler) as client: @@ -148,11 +148,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_validate_icp_posts_json_and_returns_job(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"task_id": "val-1"}) + return httpx2.Response(200, json={"task_id": "val-1"}) with make_client(handler) as client: job = client.validate_icp( @@ -174,9 +174,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_validate_icp_all_optionals_present(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"task_id": "val-2"}) + return httpx2.Response(200, json={"task_id": "val-2"}) with make_client(handler) as client: client.validate_icp( @@ -199,8 +199,8 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_process_async_returns_async_job(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"task_id": "dg-async-1"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"task_id": "dg-async-1"}) async with make_async_client(handler) as client: job = await client.discogen.process(query="q", domains=["a.com"]) @@ -211,8 +211,8 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_process_personas_async_returns_async_job(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"task_id": "dg-async-2"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"task_id": "dg-async-2"}) async with make_async_client(handler) as client: job = await client.discogen.process_personas(query="q", persona_ids=[1]) @@ -223,8 +223,8 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_models_async(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"models": {"xai": [{"name": "grok-4", "supports_web_search": True}]}}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"models": {"xai": [{"name": "grok-4", "supports_web_search": True}]}}) async with make_async_client(handler) as client: result = await client.discogen.models() @@ -233,7 +233,7 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_job_async_reattaches_without_http_call(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: pytest.fail("job() must not perform an HTTP request") async with make_async_client(handler) as client: @@ -245,8 +245,8 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_validate_icp_async_returns_async_job(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"task_id": "val-async-1"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"task_id": "val-async-1"}) async with make_async_client(handler) as client: job = await client.validate_icp(icp_text="q", domains=["a.com"]) diff --git a/packages/discolike/tests/test_discovery.py b/packages/discolike/tests/test_discovery.py index e317d50..208a252 100644 --- a/packages/discolike/tests/test_discovery.py +++ b/packages/discolike/tests/test_discovery.py @@ -1,6 +1,6 @@ from __future__ import annotations -import httpx +import httpx2 from discolike_testkit import AsyncClientFactory from discolike_testkit import ClientFactory @@ -9,10 +9,10 @@ def test_discover_builds_query_and_parses_list(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path - seen["params"] = httpx.QueryParams(request.url.query) - return httpx.Response( + seen["params"] = httpx2.QueryParams(request.url.query) + return httpx2.Response( 200, json=[{"domain": "acme.com", "name": "Acme", "similarity": 87.3, "extra_field": "kept"}], ) @@ -30,9 +30,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_count(make_client: ClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.path == "/v1/count" - return httpx.Response(200, json={"count": 1234}) + return httpx2.Response(200, json={"count": 1234}) with make_client(handler) as client: result = client.count(category=["CYBERSECURITY"]) @@ -40,8 +40,8 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_discover_async(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json=[{"domain": "a.com"}]) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json=[{"domain": "a.com"}]) async with make_async_client(handler) as client: results = await client.discover(domain=["stripe.com"]) diff --git a/packages/discolike/tests/test_email.py b/packages/discolike/tests/test_email.py index c08fb94..b076d07 100644 --- a/packages/discolike/tests/test_email.py +++ b/packages/discolike/tests/test_email.py @@ -2,7 +2,7 @@ import json -import httpx +import httpx2 import pytest import discolike._jobs as jobs_module @@ -30,12 +30,12 @@ async def fake_sleep(seconds: float) -> None: def _results_sequence(payloads): state = {"i": 0} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.path.startswith("/v1/email/batch/") assert request.url.path.endswith("/results") payload = payloads[min(state["i"], len(payloads) - 1)] state["i"] += 1 - return httpx.Response(200, json=payload) + return httpx2.Response(200, json=payload) return handler @@ -43,11 +43,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_find_batch_posts_and_returns_batch(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method seen["body"] = json.loads(request.content) - return httpx.Response(202, json={"batch_id": "b-1", "job_ids": ["j-1", "j-2"], "total": 2}) + return httpx2.Response(202, json={"batch_id": "b-1", "job_ids": ["j-1", "j-2"], "total": 2}) with make_client(handler) as client: batch = client.email.find_batch( @@ -217,15 +217,15 @@ def test_find_posts_and_job_wait_polls_jobs_endpoint(make_client: ClientFactory) }, ] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.method == "POST": seen["path"] = request.url.path seen["body"] = json.loads(request.content) - return httpx.Response(202, json={"job_id": "j-9", "status": "queued"}) + return httpx2.Response(202, json={"job_id": "j-9", "status": "queued"}) assert request.url.path == "/v1/email/jobs/j-9" payload = poll_payloads[min(state["i"], len(poll_payloads) - 1)] state["i"] += 1 - return httpx.Response(200, json=payload) + return httpx2.Response(200, json=payload) with make_client(handler) as client: job = client.email.find(first_name="Grace", last_name="Hopper", domain="navy.mil") @@ -244,9 +244,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_find_sends_known_pattern_and_omits_it_when_unset(make_client: ClientFactory) -> None: bodies: list[dict] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: bodies.append(json.loads(request.content)) - return httpx.Response(202, json={"job_id": "j-kp", "status": "queued"}) + return httpx2.Response(202, json={"job_id": "j-kp", "status": "queued"}) with make_client(handler) as client: client.email.find(first_name="Grace", last_name="Hopper", domain="navy.mil", known_pattern="first.last") @@ -262,10 +262,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_find_wait_raises_on_failed_job(make_client: ClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.method == "POST": - return httpx.Response(202, json={"job_id": "j-x", "status": "queued"}) - return httpx.Response(200, json={"job_id": "j-x", "status": "failed", "result": None, "error": "boom"}) + return httpx2.Response(202, json={"job_id": "j-x", "status": "queued"}) + return httpx2.Response(200, json={"job_id": "j-x", "status": "failed", "result": None, "error": "boom"}) with make_client(handler) as client: job = client.email.find(first_name="No", last_name="One", domain="void.dev") @@ -274,7 +274,7 @@ def handler(request: httpx.Request) -> httpx.Response: def test_job_reattaches_without_http_call(make_client: ClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: pytest.fail("job()/batch() must not perform an HTTP request") with make_client(handler) as client: @@ -291,8 +291,8 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_find_batch_async_returns_batch(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(202, json={"batch_id": "b-async", "job_ids": ["j-a"], "total": 1}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(202, json={"batch_id": "b-async", "job_ids": ["j-a"], "total": 1}) async with make_async_client(handler) as client: batch = await client.email.find_batch( @@ -371,12 +371,12 @@ async def test_find_async_job_wait(make_async_client: AsyncClientFactory) -> Non }, ] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.method == "POST": - return httpx.Response(202, json={"job_id": "j-async", "status": "queued"}) + return httpx2.Response(202, json={"job_id": "j-async", "status": "queued"}) payload = poll_payloads[min(state["i"], len(poll_payloads) - 1)] state["i"] += 1 - return httpx.Response(200, json=payload) + return httpx2.Response(200, json=payload) async with make_async_client(handler) as client: job = await client.email.find(first_name="Ada", last_name="Lovelace", domain="acme.com") diff --git a/packages/discolike/tests/test_enrich.py b/packages/discolike/tests/test_enrich.py index acb0aeb..8bdf041 100644 --- a/packages/discolike/tests/test_enrich.py +++ b/packages/discolike/tests/test_enrich.py @@ -2,7 +2,7 @@ import io -import httpx +import httpx2 import pytest from discolike._jobs import FAMILY_SEGMENT @@ -13,12 +13,12 @@ def test_append_json_response_parses_result_list(make_client: ClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.path == "/v1/append" - params = dict(httpx.QueryParams(request.url.query)) + params = dict(httpx2.QueryParams(request.url.query)) assert params["domain_column"] == "website" assert b"Acme" in request.content - return httpx.Response(200, json=[{"domain": "acme.com", "name": "Acme", "extra_field": "kept"}]) + return httpx2.Response(200, json=[{"domain": "acme.com", "name": "Acme", "extra_field": "kept"}]) with make_client(handler) as client: result = client._enrich.append(file=io.BytesIO(b"website\nAcme\n"), domain_column="website") @@ -29,8 +29,8 @@ def handler(request: httpx.Request) -> httpx.Response: def test_append_csv_response_returns_raw_bytes(make_client: ClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, content=b"col1,col2\n", headers={"Content-Type": "text/csv"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, content=b"col1,col2\n", headers={"Content-Type": "text/csv"}) with make_client(handler) as client: result = client.append(file=io.BytesIO(b"domain\nacme.com\n"), csv=True) @@ -43,9 +43,9 @@ def test_append_sends_dataset_query_params(tmp_path, make_client: ClientFactory) csv_path.write_text("domain\nacme.com\n") seen = {} - def handler(request: httpx.Request) -> httpx.Response: - seen["params"] = httpx.QueryParams(request.url.query) - return httpx.Response(200, json=[]) + def handler(request: httpx2.Request) -> httpx2.Response: + seen["params"] = httpx2.QueryParams(request.url.query) + return httpx2.Response(200, json=[]) with make_client(handler) as client: client.append(file=csv_path, dataset=["bizdata", "growth"]) @@ -54,8 +54,8 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_append_async_json_response(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json=[{"domain": "acme.com"}]) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json=[{"domain": "acme.com"}]) async with make_async_client(handler) as client: result = await client.append(file=io.BytesIO(b"domain\nacme.com\n")) @@ -67,10 +67,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_segment_domains_branch_comma_joins_and_returns_job(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path - seen["params"] = httpx.QueryParams(request.url.query) - return httpx.Response(200, json={"task_id": "seg-1"}) + seen["params"] = httpx2.QueryParams(request.url.query) + return httpx2.Response(200, json={"task_id": "seg-1"}) with make_client(handler) as client: job = client.segment(domains=["acme.com", "beta.com"], max_segments=5) @@ -88,11 +88,11 @@ def test_segment_file_branch_returns_job(tmp_path, make_client: ClientFactory) - csv_path.write_text("domain\nacme.com\n") seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method seen["content"] = request.content - return httpx.Response(200, json={"task_id": "seg-2"}) + return httpx2.Response(200, json={"task_id": "seg-2"}) with make_client(handler) as client: job = client.segment(file=csv_path, domain_column="domain") @@ -106,7 +106,7 @@ def handler(request: httpx.Request) -> httpx.Response: def test_segment_raises_when_neither_domains_nor_file_given(make_client: ClientFactory) -> None: with ( - make_client(lambda request: httpx.Response(200, json={})) as client, + make_client(lambda request: httpx2.Response(200, json={})) as client, pytest.raises(ValueError, match="one of domains, query_id, or file is required"), ): client.segment() @@ -117,7 +117,7 @@ def test_segment_raises_when_both_domains_and_file_given(tmp_path, make_client: csv_path.write_text("domain\nacme.com\n") with ( - make_client(lambda request: httpx.Response(200, json={})) as client, + make_client(lambda request: httpx2.Response(200, json={})) as client, pytest.raises(ValueError, match="file cannot be combined with domains or query_id"), ): client.segment(domains=["acme.com"], file=csv_path) @@ -125,15 +125,15 @@ def test_segment_raises_when_both_domains_and_file_given(tmp_path, make_client: def test_segment_raises_when_domain_column_given_with_domains(make_client: ClientFactory) -> None: with ( - make_client(lambda request: httpx.Response(200, json={})) as client, + make_client(lambda request: httpx2.Response(200, json={})) as client, pytest.raises(ValueError, match="domain_column only applies to file uploads"), ): client.segment(domains=["acme.com"], domain_column="domain") async def test_segment_async_domains_branch(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"task_id": "seg-3"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"task_id": "seg-3"}) async with make_async_client(handler) as client: job = await client.segment(domains=["acme.com"]) diff --git a/packages/discolike/tests/test_exceptions.py b/packages/discolike/tests/test_exceptions.py index fde6a4f..3faf913 100644 --- a/packages/discolike/tests/test_exceptions.py +++ b/packages/discolike/tests/test_exceptions.py @@ -1,6 +1,6 @@ from typing import cast -import httpx +import httpx2 import pytest from discolike import AuthenticationError @@ -13,12 +13,12 @@ from discolike._exceptions import raise_for_status -def _response(status: int, json_body: dict | None = None, headers: dict | None = None) -> httpx.Response: - return httpx.Response( +def _response(status: int, json_body: dict | None = None, headers: dict | None = None) -> httpx2.Response: + return httpx2.Response( status_code=status, json=json_body if json_body is not None else {"detail": "boom"}, headers=headers or {}, - request=httpx.Request("GET", "https://api.test/v1/x"), + request=httpx2.Request("GET", "https://api.test/v1/x"), ) diff --git a/packages/discolike/tests/test_jobs.py b/packages/discolike/tests/test_jobs.py index d76486f..5dd44c4 100644 --- a/packages/discolike/tests/test_jobs.py +++ b/packages/discolike/tests/test_jobs.py @@ -1,4 +1,4 @@ -import httpx +import httpx2 import pytest import discolike._jobs as jobs_module @@ -24,7 +24,7 @@ async def fake_sleep(seconds: float) -> None: def make_job(handler) -> Job: - http = httpx.Client(transport=httpx.MockTransport(handler), base_url=BASE) + http = httpx2.Client(transport=httpx2.MockTransport(handler), base_url=BASE) transport = Transport("k", base_url=BASE, timeout=5.0, max_retries=0, http_client=http) return Job(transport, task_family=FAMILY_DISCOGEN, task_id="t-1") @@ -32,11 +32,11 @@ def make_job(handler) -> Job: def _status_sequence(payloads): state = {"i": 0} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.path == "/v1/discogen/status/t-1" payload = payloads[min(state["i"], len(payloads) - 1)] state["i"] += 1 - return httpx.Response(200, json=payload) + return httpx2.Response(200, json=payload) return handler @@ -76,10 +76,10 @@ def mock_monotonic(): def test_cancel_issues_delete() -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["method"] = request.method seen["path"] = request.url.path - return httpx.Response(200, json={"status": "cancelling"}) + return httpx2.Response(200, json={"status": "cancelling"}) make_job(handler).cancel() assert (seen["method"], seen["path"]) == ("DELETE", "/v1/discogen/cancel/t-1") @@ -89,7 +89,7 @@ async def test_async_job_wait() -> None: handler = _status_sequence( [{"status": "in_progress", "progress": 5}, {"status": "completed", "progress": 100, "results": []}] ) - http = httpx.AsyncClient(transport=httpx.MockTransport(handler), base_url=BASE) + http = httpx2.AsyncClient(transport=httpx2.MockTransport(handler), base_url=BASE) transport = AsyncTransport("k", base_url=BASE, timeout=5.0, max_retries=0, http_client=http) final = await AsyncJob(transport, task_family=FAMILY_DISCOGEN, task_id="t-1").wait(timeout=60.0) assert final.status == "completed" diff --git a/packages/discolike/tests/test_match.py b/packages/discolike/tests/test_match.py index aa381b7..df1dcd0 100644 --- a/packages/discolike/tests/test_match.py +++ b/packages/discolike/tests/test_match.py @@ -1,6 +1,6 @@ import io -import httpx +import httpx2 from discolike._jobs import FAMILY_BULKMATCH from discolike._jobs import AsyncJob @@ -12,10 +12,10 @@ def test_company_sends_params_and_parses_response(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path - seen["params"] = dict(httpx.QueryParams(request.url.query)) - return httpx.Response(200, json={"domain": "acme.com", "anything": 1}) + seen["params"] = dict(httpx2.QueryParams(request.url.query)) + return httpx2.Response(200, json={"domain": "acme.com", "anything": 1}) with make_client(handler) as client: result = client.match.company(name="Acme Inc", city="Austin", strict=True, min_match_confidence=80) @@ -31,9 +31,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_company_omits_min_match_confidence_when_not_set(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: - seen["params"] = dict(httpx.QueryParams(request.url.query)) - return httpx.Response(200, json={"domain": "acme.com"}) + def handler(request: httpx2.Request) -> httpx2.Response: + seen["params"] = dict(httpx2.QueryParams(request.url.query)) + return httpx2.Response(200, json={"domain": "acme.com"}) with make_client(handler) as client: client.match.company(name="Acme Inc") @@ -46,11 +46,11 @@ def test_bulk_posts_multipart_with_path(tmp_path, make_client: ClientFactory) -> csv_path.write_text("company\nAcme\n") seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path - seen["params"] = dict(httpx.QueryParams(request.url.query)) + seen["params"] = dict(httpx2.QueryParams(request.url.query)) seen["content"] = request.content - return httpx.Response(200, json={"task_id": "bm-1"}) + return httpx2.Response(200, json={"task_id": "bm-1"}) with make_client(handler) as client: job = client.match.bulk(file=csv_path, name_column="company", min_match_confidence=80) @@ -68,8 +68,8 @@ def test_bulk_accepts_open_handle_without_closing_it(make_client: ClientFactory) handle = io.BytesIO(b"company\nAcme\n") handle.name = "companies.csv" - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"task_id": "bm-2"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"task_id": "bm-2"}) with make_client(handler) as client: job = client.match.bulk(file=handle, name_column="company") @@ -83,11 +83,11 @@ async def test_async_bulk_posts_multipart(tmp_path, make_async_client: AsyncClie csv_path.write_text("company\nAcme\n") seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path - seen["params"] = dict(httpx.QueryParams(request.url.query)) + seen["params"] = dict(httpx2.QueryParams(request.url.query)) seen["content"] = request.content - return httpx.Response(200, json={"task_id": "bm-3"}) + return httpx2.Response(200, json={"task_id": "bm-3"}) async with make_async_client(handler) as client: job = await client.match.bulk(file=csv_path, name_column="company") diff --git a/packages/discolike/tests/test_package.py b/packages/discolike/tests/test_package.py index 3246033..107096e 100644 --- a/packages/discolike/tests/test_package.py +++ b/packages/discolike/tests/test_package.py @@ -2,4 +2,4 @@ def test_version() -> None: - assert discolike.__version__ == "0.1.2" + assert discolike.__version__ == "0.2.0" diff --git a/packages/discolike/tests/test_providers.py b/packages/discolike/tests/test_providers.py index 179a49a..184e699 100644 --- a/packages/discolike/tests/test_providers.py +++ b/packages/discolike/tests/test_providers.py @@ -2,7 +2,7 @@ import json -import httpx +import httpx2 import pytest from discolike_testkit import AsyncClientFactory @@ -12,10 +12,10 @@ def test_search_providers_list_hits_collection(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method - return httpx.Response(200, json={"providers": [{"integration_id": "sp1"}]}) + return httpx2.Response(200, json={"providers": [{"integration_id": "sp1"}]}) with make_client(handler) as client: result = client.search_providers.list() @@ -28,11 +28,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_search_providers_create_posts_body_and_drops_unset(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"integration_id": "sp2", "integration_name": "Tavily"}) + return httpx2.Response(200, json={"integration_id": "sp2", "integration_name": "Tavily"}) with make_client(handler) as client: result = client.search_providers.create( @@ -56,11 +56,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_search_providers_update_puts_path_and_body(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"integration_id": "sp3", "integration_name": "Serper"}) + return httpx2.Response(200, json={"integration_id": "sp3", "integration_name": "Serper"}) with make_client(handler) as client: result = client.search_providers.update( @@ -83,10 +83,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_search_providers_delete_returns_none(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method - return httpx.Response(200, json={"message": "Integration deleted successfully"}) + return httpx2.Response(200, json={"message": "Integration deleted successfully"}) with make_client(handler) as client: result = client.search_providers.delete(integration_id="sp4") @@ -99,10 +99,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_search_providers_set_default_puts_default_subroute(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method - return httpx.Response(200, json={"message": "ok", "integration_id": "sp5"}) + return httpx2.Response(200, json={"message": "ok", "integration_id": "sp5"}) with make_client(handler) as client: result = client.search_providers.set_default(integration_id="sp5") @@ -115,10 +115,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_search_providers_clear_default_deletes_default_subroute(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method - return httpx.Response(200, json={"message": "Default search provider cleared successfully"}) + return httpx2.Response(200, json={"message": "Default search provider cleared successfully"}) with make_client(handler) as client: result = client.search_providers.clear_default(integration_id="sp6") @@ -131,10 +131,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_search_providers_models_hits_models_route(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method - return httpx.Response(200, json={"models": {"tavily": [{"name": "tavily/search", "cost_per_query": 0.008}]}}) + return httpx2.Response(200, json={"models": {"tavily": [{"name": "tavily/search", "cost_per_query": 0.008}]}}) with make_client(handler) as client: result = client.search_providers.models() @@ -148,10 +148,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_llm_providers_list_hits_config(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method - return httpx.Response(200, json={"providers": [], "mtime": None}) + return httpx2.Response(200, json={"providers": [], "mtime": None}) with make_client(handler) as client: result = client.llm_providers.list() @@ -164,11 +164,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_llm_providers_create_posts_body_and_drops_unset(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"message": "created", "integration_id": "llm1"}) + return httpx2.Response(200, json={"message": "created", "integration_id": "llm1"}) with make_client(handler) as client: result = client.llm_providers.create( @@ -192,10 +192,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_llm_providers_get_hits_config_item(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method - return httpx.Response(200, json={"integration_id": "llm2", "integration_name": "Anthropic"}) + return httpx2.Response(200, json={"integration_id": "llm2", "integration_name": "Anthropic"}) with make_client(handler) as client: result = client.llm_providers.get(integration_id="llm2") @@ -208,11 +208,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_llm_providers_update_keeps_null_api_key_in_body(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"message": "updated"}) + return httpx2.Response(200, json={"message": "updated"}) with make_client(handler) as client: result = client.llm_providers.update( @@ -236,9 +236,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_llm_providers_update_sends_new_api_key(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"message": "updated"}) + return httpx2.Response(200, json={"message": "updated"}) with make_client(handler) as client: client.llm_providers.update( @@ -255,10 +255,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_llm_providers_delete_returns_none(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method - return httpx.Response(200, json={"message": "Integration deleted successfully"}) + return httpx2.Response(200, json={"message": "Integration deleted successfully"}) with make_client(handler) as client: result = client.llm_providers.delete(integration_id="llm4") @@ -271,10 +271,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_llm_providers_set_default_posts_subroute(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method - return httpx.Response(200, json={"message": "ok", "integration_id": "llm5"}) + return httpx2.Response(200, json={"message": "ok", "integration_id": "llm5"}) with make_client(handler) as client: result = client.llm_providers.set_default(integration_id="llm5") @@ -287,11 +287,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_llm_providers_test_connection_posts_body(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"status": "success", "message": "Connection to openai successful"}) + return httpx2.Response(200, json={"status": "success", "message": "Connection to openai successful"}) with make_client(handler) as client: result = client.llm_providers.test_connection( @@ -316,8 +316,8 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.asyncio async def test_search_providers_list_async(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"providers": []}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"providers": []}) async with make_async_client(handler) as client: result = await client.search_providers.list() @@ -329,10 +329,10 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_search_providers_set_default_async(make_async_client: AsyncClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method - return httpx.Response(200, json={"message": "ok", "integration_id": "sp7"}) + return httpx2.Response(200, json={"message": "ok", "integration_id": "sp7"}) async with make_async_client(handler) as client: result = await client.search_providers.set_default(integration_id="sp7") @@ -346,10 +346,10 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_llm_providers_create_async(make_async_client: AsyncClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"message": "created", "integration_id": "llm6"}) + return httpx2.Response(200, json={"message": "created", "integration_id": "llm6"}) async with make_async_client(handler) as client: result = await client.llm_providers.create( @@ -368,9 +368,9 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_llm_providers_update_async_keeps_null_api_key(make_async_client: AsyncClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"message": "updated"}) + return httpx2.Response(200, json={"message": "updated"}) async with make_async_client(handler) as client: await client.llm_providers.update( diff --git a/packages/discolike/tests/test_queries.py b/packages/discolike/tests/test_queries.py index ef8df50..4711653 100644 --- a/packages/discolike/tests/test_queries.py +++ b/packages/discolike/tests/test_queries.py @@ -2,7 +2,7 @@ import json -import httpx +import httpx2 from discolike_testkit import AsyncClientFactory from discolike_testkit import ClientFactory @@ -11,10 +11,10 @@ def test_list_sends_params_and_parses_response(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path - seen["params"] = httpx.QueryParams(request.url.query) - return httpx.Response(200, json={"results": [{"query_id": "q1"}], "count": 1}) + seen["params"] = httpx2.QueryParams(request.url.query) + return httpx2.Response(200, json={"results": [{"query_id": "q1"}], "count": 1}) with make_client(handler) as client: result = client.queries.list(max_records=10, offset=5, action="discover", tags=["a", "b"]) @@ -31,11 +31,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_create_exclusion_list_posts_json_body(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"query_id": "q2", "query_name": "My List", "domain_count": 2}) + return httpx2.Response(200, json={"query_id": "q2", "query_name": "My List", "domain_count": 2}) with make_client(handler) as client: result = client.queries.create_exclusion_list(query_name="My List", domains=["a.com", "b.com"]) @@ -49,11 +49,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_update_patches_path_and_body(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"query_id": "q3", "query_name": "New Name"}) + return httpx2.Response(200, json={"query_id": "q3", "query_name": "New Name"}) with make_client(handler) as client: result = client.queries.update(query_id="q3", query_name="New Name") @@ -67,10 +67,10 @@ def handler(request: httpx.Request) -> httpx.Response: def test_delete_sends_delete_and_returns_none(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method - return httpx.Response(200, json={"message": "Query deleted successfully"}) + return httpx2.Response(200, json={"message": "Query deleted successfully"}) with make_client(handler) as client: result = client.queries.delete(query_id="q4") @@ -81,8 +81,8 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_list_async(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"results": [], "count": 0}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"results": [], "count": 0}) async with make_async_client(handler) as client: result = await client.queries.list() @@ -92,8 +92,8 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_delete_async(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"message": "ok"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"message": "ok"}) async with make_async_client(handler) as client: result = await client.queries.delete(query_id="q5") @@ -104,11 +104,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_save_results_posts_json_body(make_client: ClientFactory) -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["path"] = request.url.path seen["method"] = request.method seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"query_id": "q6", "action": "thin_discover", "row_count": 1}) + return httpx2.Response(200, json={"query_id": "q6", "action": "thin_discover", "row_count": 1}) with make_client(handler) as client: result = client.queries.save_results(query_name="R", action="discover", data=[{"domain": "a.com"}], tags=["x"]) @@ -122,8 +122,8 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_save_results_async(make_async_client: AsyncClientFactory) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"query_id": "q7"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(200, json={"query_id": "q7"}) async with make_async_client(handler) as client: result = await client.queries.save_results(query_name="R", action="discover", data=[{"domain": "a.com"}]) diff --git a/packages/discolike/tests/test_transport.py b/packages/discolike/tests/test_transport.py index e115870..69149cc 100644 --- a/packages/discolike/tests/test_transport.py +++ b/packages/discolike/tests/test_transport.py @@ -1,4 +1,4 @@ -import httpx +import httpx2 import pytest import discolike._transport as transport_module @@ -22,7 +22,7 @@ async def fake_async_sleep(seconds: float) -> None: def make_transport(handler) -> Transport: - http = httpx.Client(transport=httpx.MockTransport(handler), base_url="https://api.test/v1") + http = httpx2.Client(transport=httpx2.MockTransport(handler), base_url="https://api.test/v1") return Transport("test-key", base_url="https://api.test/v1", timeout=5.0, max_retries=2, http_client=http) @@ -40,9 +40,9 @@ def test_drop_none() -> None: def test_auth_and_user_agent_headers() -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen.update(request.headers) - return httpx.Response(200, json={"ok": True}) + return httpx2.Response(200, json={"ok": True}) make_transport(handler).request("GET", "/usage") assert seen["x-discolike-key"] == "test-key" @@ -52,9 +52,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_retries_on_503_then_succeeds(no_sleep) -> None: calls = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: calls.append(1) - return httpx.Response(503) if len(calls) < 3 else httpx.Response(200, json={"ok": True}) + return httpx2.Response(503) if len(calls) < 3 else httpx2.Response(200, json={"ok": True}) response = make_transport(handler).request("GET", "/usage") assert response.json() == {"ok": True} @@ -63,16 +63,16 @@ def handler(request: httpx.Request) -> httpx.Response: def test_429_exhausts_retries_then_raises(no_sleep) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(429, json={"detail": "slow down"}, headers={"Retry-After": "1"}) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(429, json={"detail": "slow down"}, headers={"Retry-After": "1"}) with pytest.raises(RateLimitError): make_transport(handler).request("GET", "/usage") def test_network_error_exhausts_then_raises(no_sleep) -> None: - def handler(request: httpx.Request) -> httpx.Response: - raise httpx.ConnectError("refused") + def handler(request: httpx2.Request) -> httpx2.Response: + raise httpx2.ConnectError("refused") with pytest.raises(APIConnectionError): make_transport(handler).request("GET", "/usage") @@ -81,9 +81,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_400_does_not_retry() -> None: calls = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: calls.append(1) - return httpx.Response(400, json={"detail": "bad"}) + return httpx2.Response(400, json={"detail": "bad"}) from discolike import ValidationError @@ -95,13 +95,13 @@ def handler(request: httpx.Request) -> httpx.Response: def test_max_retries_zero_does_not_retry(no_sleep) -> None: calls = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: calls.append(1) - return httpx.Response(503, json={"detail": "unavailable"}) + return httpx2.Response(503, json={"detail": "unavailable"}) from discolike import ServerError - http = httpx.Client(transport=httpx.MockTransport(handler), base_url="https://api.test/v1") + http = httpx2.Client(transport=httpx2.MockTransport(handler), base_url="https://api.test/v1") transport = Transport("test-key", base_url="https://api.test/v1", timeout=5.0, max_retries=0, http_client=http) with pytest.raises(ServerError): transport.request("GET", "/usage") @@ -112,11 +112,11 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_async_transport_retries(no_sleep) -> None: calls = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: calls.append(1) - return httpx.Response(502) if len(calls) < 2 else httpx.Response(200, json={"ok": True}) + return httpx2.Response(502) if len(calls) < 2 else httpx2.Response(200, json={"ok": True}) - http = httpx.AsyncClient(transport=httpx.MockTransport(handler), base_url="https://api.test/v1") + http = httpx2.AsyncClient(transport=httpx2.MockTransport(handler), base_url="https://api.test/v1") transport = AsyncTransport("test-key", base_url="https://api.test/v1", timeout=5.0, max_retries=2, http_client=http) response = await transport.request("GET", "/usage") assert response.json() == {"ok": True} @@ -126,9 +126,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_post_502_does_not_retry_and_raises_server_error(no_sleep) -> None: calls = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: calls.append(1) - return httpx.Response(502) + return httpx2.Response(502) from discolike import ServerError @@ -141,9 +141,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_post_429_is_still_retried(no_sleep) -> None: calls = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: calls.append(1) - return httpx.Response(429) if len(calls) < 2 else httpx.Response(200, json={"ok": True}) + return httpx2.Response(429) if len(calls) < 2 else httpx2.Response(200, json={"ok": True}) response = make_transport(handler).request("POST", "/discogen/process") assert response.json() == {"ok": True} @@ -154,11 +154,11 @@ def handler(request: httpx.Request) -> httpx.Response: def test_post_connect_error_is_retried_then_succeeds(no_sleep) -> None: calls = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: calls.append(1) if len(calls) < 2: - raise httpx.ConnectError("refused") - return httpx.Response(200, json={"ok": True}) + raise httpx2.ConnectError("refused") + return httpx2.Response(200, json={"ok": True}) response = make_transport(handler).request("POST", "/discogen/process") assert response.json() == {"ok": True} @@ -169,9 +169,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_post_read_timeout_raises_immediately_without_retry(no_sleep) -> None: calls = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: calls.append(1) - raise httpx.ReadTimeout("timed out") + raise httpx2.ReadTimeout("timed out") with pytest.raises(APIConnectionError): make_transport(handler).request("POST", "/discogen/process") @@ -182,9 +182,9 @@ def handler(request: httpx.Request) -> httpx.Response: def test_delete_502_is_still_retried(no_sleep) -> None: calls = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: calls.append(1) - return httpx.Response(502) if len(calls) < 2 else httpx.Response(200, json={"ok": True}) + return httpx2.Response(502) if len(calls) < 2 else httpx2.Response(200, json={"ok": True}) response = make_transport(handler).request("DELETE", "/discogen/cancel/abc") assert response.json() == {"ok": True} @@ -194,13 +194,13 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_async_post_502_does_not_retry_and_raises_server_error(no_sleep) -> None: calls = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: calls.append(1) - return httpx.Response(502) + return httpx2.Response(502) from discolike import ServerError - http = httpx.AsyncClient(transport=httpx.MockTransport(handler), base_url="https://api.test/v1") + http = httpx2.AsyncClient(transport=httpx2.MockTransport(handler), base_url="https://api.test/v1") transport = AsyncTransport("test-key", base_url="https://api.test/v1", timeout=5.0, max_retries=2, http_client=http) with pytest.raises(ServerError): await transport.request("POST", "/discogen/process") @@ -211,13 +211,13 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_async_post_connect_error_is_retried_then_succeeds(no_sleep) -> None: calls = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: calls.append(1) if len(calls) < 2: - raise httpx.ConnectError("refused") - return httpx.Response(200, json={"ok": True}) + raise httpx2.ConnectError("refused") + return httpx2.Response(200, json={"ok": True}) - http = httpx.AsyncClient(transport=httpx.MockTransport(handler), base_url="https://api.test/v1") + http = httpx2.AsyncClient(transport=httpx2.MockTransport(handler), base_url="https://api.test/v1") transport = AsyncTransport("test-key", base_url="https://api.test/v1", timeout=5.0, max_retries=2, http_client=http) response = await transport.request("POST", "/discogen/process") assert response.json() == {"ok": True} @@ -228,18 +228,18 @@ def handler(request: httpx.Request) -> httpx.Response: def test_byo_http_client_without_base_url_gets_base_url_set() -> None: seen = {} - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen["url"] = str(request.url) - return httpx.Response(200, json={"ok": True}) + return httpx2.Response(200, json={"ok": True}) - http = httpx.Client(transport=httpx.MockTransport(handler)) + http = httpx2.Client(transport=httpx2.MockTransport(handler)) transport = Transport("test-key", base_url="https://api.test/v1", timeout=5.0, max_retries=0, http_client=http) transport.request("GET", "/usage") assert seen["url"] == "https://api.test/v1/usage" def test_byo_http_client_with_base_url_is_left_alone() -> None: - http = httpx.Client(base_url="https://custom.example/v2") + http = httpx2.Client(base_url="https://custom.example/v2") Transport("test-key", base_url="https://api.test/v1", timeout=5.0, max_retries=0, http_client=http) assert str(http.base_url) == "https://custom.example/v2/" @@ -247,11 +247,11 @@ def test_byo_http_client_with_base_url_is_left_alone() -> None: def test_with_timeout_overrides_request_timeout_and_leaves_base_alone() -> None: seen = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: seen.append(request.extensions["timeout"]) - return httpx.Response(200, json={"ok": True}) + return httpx2.Response(200, json={"ok": True}) - http = httpx.Client(transport=httpx.MockTransport(handler), base_url="https://api.test/v1", timeout=5.0) + http = httpx2.Client(transport=httpx2.MockTransport(handler), base_url="https://api.test/v1", timeout=5.0) transport = Transport("test-key", base_url="https://api.test/v1", timeout=5.0, max_retries=0, http_client=http) transport.with_timeout(120.0).request("GET", "/usage") transport.request("GET", "/usage") diff --git a/scripts/check_contract.py b/scripts/check_contract.py index 4655195..a518417 100644 --- a/scripts/check_contract.py +++ b/scripts/check_contract.py @@ -10,7 +10,7 @@ from dataclasses import dataclass from types import ModuleType -import httpx +import httpx2 import discolike.resources from discolike._models import DiscolikeModel @@ -158,7 +158,7 @@ def check_models(spec: dict, mirrored: dict[str, type[DiscolikeModel]] | None = def load_spec(*, spec_path: str | None, spec_url: str) -> dict: if spec_path is not None: return json.loads(pathlib.Path(spec_path).read_text()) - response = httpx.get(spec_url, timeout=REQUEST_TIMEOUT_SECONDS) + response = httpx2.get(spec_url, timeout=REQUEST_TIMEOUT_SECONDS) response.raise_for_status() return response.json() diff --git a/uv.lock b/uv.lock index aa3d26d..dd2e735 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' or sys_platform != 'emscripten'", +] [manifest] members = [ @@ -50,15 +54,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, ] -[[package]] -name = "certifi" -version = "2026.6.17" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -72,7 +67,7 @@ wheels = [ name = "discolike" source = { editable = "packages/discolike" } dependencies = [ - { name = "httpx" }, + { name = "httpx2" }, { name = "pydantic" }, { name = "typing-extensions" }, ] @@ -94,7 +89,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "discolike-cli", marker = "extra == 'cli'", editable = "packages/discolike-cli" }, - { name = "httpx", specifier = ">=0.27" }, + { name = "httpx2", specifier = ">=2.9" }, { name = "pydantic", specifier = ">=2.7" }, { name = "typing-extensions", specifier = ">=4.1" }, ] @@ -111,7 +106,7 @@ dev = [ [[package]] name = "discolike-cli" -version = "0.1.2" +version = "0.2.0" source = { editable = "packages/discolike-cli" } dependencies = [ { name = "discolike" }, @@ -150,14 +145,14 @@ version = "0.0.0" source = { editable = "packages/discolike-testkit" } dependencies = [ { name = "discolike" }, - { name = "httpx" }, + { name = "httpx2" }, { name = "pytest" }, ] [package.metadata] requires-dist = [ { name = "discolike", editable = "packages/discolike" }, - { name = "httpx", specifier = ">=0.27" }, + { name = "httpx2", specifier = ">=2.9" }, { name = "pytest", specifier = ">=8" }, ] @@ -183,31 +178,42 @@ wheels = [ ] [[package]] -name = "httpcore" -version = "1.0.9" +name = "httpcore2" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, { name = "h11" }, + { name = "truststore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, + { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, ] [[package]] -name = "httpx" -version = "0.28.1" +name = "httpx2" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, ] [[package]] @@ -540,6 +546,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "ty" version = "0.0.59"