From ad8d53b2564f3dac052c04c1078f3e578db15958 Mon Sep 17 00:00:00 2001 From: cjumel Date: Wed, 3 Jun 2026 12:03:28 +0200 Subject: [PATCH 1/5] chore: upgrade deps with vulnerabilities This should fix some issues detected by Dependabot on GitHub. --- pyproject.toml | 2 +- uv.lock | 56 +++++++++++++++++++++++++------------------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2303030..ba3391d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,7 +136,7 @@ parse_squash_commits = false [tool.uv] exclude-newer = "2 weeks" # Reduce risks of supply chain attacks -required-version = ">=0.11.6,<0.12.0" +required-version = ">=0.11.15,<0.12.0" [build-system] build-backend = "hatchling.build" diff --git a/uv.lock b/uv.lock index 7d5183b..c09eb53 100644 --- a/uv.lock +++ b/uv.lock @@ -1044,11 +1044,11 @@ wheels = [ [[package]] name = "idna" -version = "3.13" +version = "3.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] [[package]] @@ -1966,37 +1966,37 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] name = "uv" -version = "0.11.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/02/69a3b06fd8a91f95b79e95e14f5ccdd4df0f124c381aefe9d1e2784d5a65/uv-0.11.11.tar.gz", hash = "sha256:2ba46a912a1775957c579a1a42c8c8b480418502326b72427b1cad972c8f659f", size = 4112827, upload-time = "2026-05-06T20:04:47.982Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/54/39d3c58de992767834120fe3735b85cc60dd00a69b377c3d947ca6f172a1/uv-0.11.11-py3-none-linux_armv6l.whl", hash = "sha256:4977a1193e5dc9c2934b9f97d6cf787382f80deae17646640ee583cfc61486c0", size = 23537936, upload-time = "2026-05-06T20:04:58.626Z" }, - { url = "https://files.pythonhosted.org/packages/de/c9/d2d7ca30abf4c2d5ae0d9360a1e154115af176308ef1ecdc8bf7af724cf8/uv-0.11.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:92817f276758e41b4160fcb6d457ebd9f228f0473efe3808891164f326fdea38", size = 23068282, upload-time = "2026-05-06T20:05:01.466Z" }, - { url = "https://files.pythonhosted.org/packages/fa/37/f64decba47d7afaace3f238aa4a416dca947bd0a1a9b534c3a0f179e1016/uv-0.11.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6eec6ad051e6e5d922cd547b9f7b09a7f821597ae01900a6f01b0a01317e5fd0", size = 21671522, upload-time = "2026-05-06T20:05:04.382Z" }, - { url = "https://files.pythonhosted.org/packages/93/a6/c129878d7c2a66ffdaa12dc253d3135c5e10fc5b5e15812791e188c6dbec/uv-0.11.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:1d227bb53b701e533f0aa074dd145a6fa31492dc7d6d57a6e72a700b9a4a1991", size = 23283200, upload-time = "2026-05-06T20:04:39.879Z" }, - { url = "https://files.pythonhosted.org/packages/8f/c2/cff1f9ab7eda3d863e9866fca0e14df37c0fd734b66ebb77d751258b2fae/uv-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:05ee9f18701692fcb22db98085c041a3be7a35b88c710dea4487c293f42a4b95", size = 23081561, upload-time = "2026-05-06T20:05:07.149Z" }, - { url = "https://files.pythonhosted.org/packages/ca/44/ebd02ca8fae5961d1bcbcee11019dd170dd0d42517afad753281335700cc/uv-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0632af539d6a1ee00f58da9e7db32fd99e12187aa67426cb90d871154ab5debb", size = 23105780, upload-time = "2026-05-06T20:04:50.107Z" }, - { url = "https://files.pythonhosted.org/packages/86/f7/0741abcd70591a65f85fc4e8fecd3fb3fb4bdfe50042cccf016714955fd9/uv-0.11.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb3f2715551d2fc9ef44b6cf0918fcc556cd99e9bf6caa1d8a870a4657d2b180", size = 24542681, upload-time = "2026-05-06T20:04:53.014Z" }, - { url = "https://files.pythonhosted.org/packages/b1/42/46e7e35f1f39e39d4bf0f712479768cf8d33eb7f35b67fceaea43e975dfd/uv-0.11.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c86bd6460579857d7e359bdbfe6f688076c654481ae933151d1449f9ea672fb6", size = 25459284, upload-time = "2026-05-06T20:04:34.168Z" }, - { url = "https://files.pythonhosted.org/packages/e8/fc/efdb16e1a6c619b021259ac8d8e4b6afd97efb446054ea28761eb2e1a177/uv-0.11.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f69f4df007c7506db8d7f77ccabd466a886ac21e9b04a479dd0cd22e26d2262", size = 24560769, upload-time = "2026-05-06T20:04:42.648Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f8/a5d5bac297b1379719050788c6b852c6b3eefcb1e82d8465ed22c10cede7/uv-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5b9f31dab557b5ee4257d8c6ba2608a63c7278537cb0cd102cf6fc518e3fb5c", size = 24639659, upload-time = "2026-05-06T20:04:31.491Z" }, - { url = "https://files.pythonhosted.org/packages/ee/d5/f3be167a43192062f1409fd6b857a612665d331174293b4ffc73218872e1/uv-0.11.11-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:8e8faf2e5b3517155fd18e509b19b21135247d43b7fb9a8d61a44a53118d5ab7", size = 23388445, upload-time = "2026-05-06T20:04:25.199Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cd/ef1f573ee8edd2beab9fcd2449121483829621b3b57f7ba3f35c56ef373b/uv-0.11.11-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:3f8c9a1bea743a3fe39e956455686f4d0dd25ef58e8d70dc11a45381fd7c50e5", size = 24114301, upload-time = "2026-05-06T20:04:28.586Z" }, - { url = "https://files.pythonhosted.org/packages/9d/be/9181158465719e875a6995c10af24e00cdefba3fe6c9c8cbb02d34b2ade7/uv-0.11.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f68dc7b62050a26ac6b1491398aebbbf0fa5485627e73b1d626666a097dbab07", size = 24155126, upload-time = "2026-05-06T20:04:55.98Z" }, - { url = "https://files.pythonhosted.org/packages/71/9c/bb306f9964870847f02a931d1fff896726f8bafcf9ce917122ac1bfef14c/uv-0.11.11-py3-none-musllinux_1_1_i686.whl", hash = "sha256:29ddb0d9b24a30ff4360b94e3cb704e82cd5fda86dc224032251f33ab5ceb79e", size = 23824684, upload-time = "2026-05-06T20:05:10.305Z" }, - { url = "https://files.pythonhosted.org/packages/56/48/434a1cf4798ca200e0dcb36411ba38013edb6d3e1aeb4cd85e8a2d7db9ca/uv-0.11.11-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:505a31f2c30fa9e83b1853cab06c5b92e66341c914c6f20f3878903aa09a6f34", size = 24862560, upload-time = "2026-05-06T20:04:37.287Z" }, - { url = "https://files.pythonhosted.org/packages/63/3a/997cddf82917f084d486e1c268c7e94836190fd928c93aa3fb92caee9a7f/uv-0.11.11-py3-none-win32.whl", hash = "sha256:c1e0e3e18cc94680642eac3c3f19f2635c17dd058edcb41b78cbdc459f574eb4", size = 22573619, upload-time = "2026-05-06T20:04:45.35Z" }, - { url = "https://files.pythonhosted.org/packages/30/5f/db34b840f8d86833ef810de8150fc9ce01a03c779393e08eadbcc4c010d5/uv-0.11.11-py3-none-win_amd64.whl", hash = "sha256:36412b13f6287304789abdf40122d268cee548fce3573e07d148a29370181421", size = 25170135, upload-time = "2026-05-06T20:05:13.001Z" }, - { url = "https://files.pythonhosted.org/packages/2d/3e/f3ba2557b437ec5b1fde1e0d5248b723432dc90f09b0050f52695596fd2e/uv-0.11.11-py3-none-win_arm64.whl", hash = "sha256:011f42faf5d267a6681ea77e3f236f275cb4490efeecb9599de74dc7ad7df8f6", size = 23597162, upload-time = "2026-05-06T20:05:16.095Z" }, +version = "0.11.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/609d5d01ba21dc8f0974610ca7802fbb2c946a0c38665cfe5c5aeddbefb5/uv-0.11.15.tar.gz", hash = "sha256:755f959ec6a2fd8ccb6ee76ad90ab759d2eb1f4797444078645dd1ee4bca92d6", size = 4159545, upload-time = "2026-05-18T19:57:48.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/7c/dcc230c5911884d8848145dabcac8fb95a5ed6f9fe1c57fae8242618f28a/uv-0.11.15-py3-none-linux_armv6l.whl", hash = "sha256:83b04ab49514a0a761ffedb36a748ee81f87746671e72088e5f32c9585e5f1a9", size = 23110183, upload-time = "2026-05-18T19:57:23.051Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/efd4e044b60eb9c3c12ee386be098d56c335538ccec7caa49349cfba9344/uv-0.11.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6cae61f737be075b90be9e3f07d961072aed7019f4c9b8ed5c5d41c4d6cade3", size = 22637941, upload-time = "2026-05-18T19:57:26.752Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b8/48627f895a1569e576822e0a8416aa4797eb4a4551de21a4ad97b9b5819d/uv-0.11.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9accae33619a9166e5c48531deb455d672cfb89f9357a00975e669c76b0bd49f", size = 21258803, upload-time = "2026-05-18T19:57:05.473Z" }, + { url = "https://files.pythonhosted.org/packages/af/50/4bc8a148274feabee2d9c9f1fa15009e10c0228dfe57981ee3ea2ef1d481/uv-0.11.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:c0cf52cd6d50bb9e05e2d968f45f80761107e4cbc8d4a26d9758f9d8274aaec1", size = 23066178, upload-time = "2026-05-18T19:57:33.058Z" }, + { url = "https://files.pythonhosted.org/packages/a9/56/139fc3bec9a8b0a25bfe2196123adb9f16124da437bf4fbcf0d21cfcafb2/uv-0.11.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:49dc6ed70bff00937384f96cdc4b1a4742d18e5504ec2c4a1214dba2dee5687a", size = 22705332, upload-time = "2026-05-18T19:57:36.714Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b0/b18b3dd204f8c213236a1ebd148e009861637129a8cce34df0e9aa22ed40/uv-0.11.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:adb9a89352539fdd8f7cd5f9966cf9f94fc5b98e0ccdf5003a04123dc6423bec", size = 22707534, upload-time = "2026-05-18T19:58:04.117Z" }, + { url = "https://files.pythonhosted.org/packages/76/36/3ca09f95572df99d361b49c96b1297149e96e120d8d1ecf074095a4b6da4/uv-0.11.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40ff67e3f8e8a7533781a2e892a534975a93acb83ea35460e64e7b2bf2111774", size = 24096607, upload-time = "2026-05-18T19:58:11.625Z" }, + { url = "https://files.pythonhosted.org/packages/64/be/3bdee21a296bbf5336a526e3613d0e7d4538dacc39c62d7fcba55d15f6b0/uv-0.11.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6463a299ed7e6b5a800ed6f108af8e1588352629424133ddef7572b0e1e1118", size = 25082562, upload-time = "2026-05-18T19:57:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/cd/73/f371f3689ffe741066468d001d85f739fc4b5574de83b639ef19b5e8a7f4/uv-0.11.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68c1e62d4b78578b90b833553286b65d6a7e327537716441068583ba652ec4f5", size = 24253391, upload-time = "2026-05-18T19:57:18.47Z" }, + { url = "https://files.pythonhosted.org/packages/d3/16/fe392d618af6b00c064b3e718d585dcf791546a77c5123a5bec07ce53a0a/uv-0.11.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98edf1bdaf82447014852051d93e3ee95012509c567bf057fd117e6bdbd9a807", size = 24415871, upload-time = "2026-05-18T19:58:19.651Z" }, + { url = "https://files.pythonhosted.org/packages/6e/24/2e92a052fb6334fcd746d1c7cb57847c204b118c84f5da53c0f9e129f7b7/uv-0.11.15-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:be8f76d25bcf4c92bb384240ac1bf9aa7f51063d0bdeca4c9cf0ec3ed8b145e0", size = 23159007, upload-time = "2026-05-18T19:57:10.653Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2e/6923d0658d164bb2c435ed1868aa2d49b3074594679917a001ff92dc95bb/uv-0.11.15-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:f9f4fbbf4fe485522054f3c7496c6e8e932d6436e4200ff3daf718db0b7c7bd5", size = 23769385, upload-time = "2026-05-18T19:58:15.856Z" }, + { url = "https://files.pythonhosted.org/packages/a4/99/7e34cd949e57360814e8064cc9fb7104df445d0f6a663504e5f7473480aa/uv-0.11.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0ed920e896b2fd13a35031707e307e42fbb2681458b967440a17272d86d49137", size = 23860973, upload-time = "2026-05-18T19:57:55.575Z" }, + { url = "https://files.pythonhosted.org/packages/28/98/8fe1f5f9d816e94569a0298dd8e0936801097625fa1952162951f0d628b6/uv-0.11.15-py3-none-musllinux_1_1_i686.whl", hash = "sha256:41d907611f3e6a13262807fd7f0a17849f76285ca80f536f6b3943732bdc6656", size = 23431392, upload-time = "2026-05-18T19:57:59.814Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6b/76a1ce2fa860026913a5941700cdc7d715fce9c3277a3fa3489cf2523ca0/uv-0.11.15-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:e3b68f8bf1a4568710f77e5bda9182ce7682811d89a8e7468c22460e032b234d", size = 24519478, upload-time = "2026-05-18T19:57:51.165Z" }, + { url = "https://files.pythonhosted.org/packages/43/60/1d58e8a05718cb50494763115710b73846cacb651fd735d285233fd72c59/uv-0.11.15-py3-none-win32.whl", hash = "sha256:8e2da3076761086a5b76869c3f38ef0509c836046ef41ddd19485dfd7271dca9", size = 22020178, upload-time = "2026-05-18T19:58:07.64Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/40fcefcb348af660488597ed3c01363df7344e60611f8883750dc596f5c6/uv-0.11.15-py3-none-win_amd64.whl", hash = "sha256:cc3915ab291a1ecaf31de05f5d8bd70d09c66fe9911a53f70d9efa62ff0dbd8a", size = 24668779, upload-time = "2026-05-18T19:57:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7d/fa3a9960c95af9bbe2a629048760d0b9b4fead8ccd4f2235af747ec7cdf0/uv-0.11.15-py3-none-win_arm64.whl", hash = "sha256:4f39426a13dee24897aed60c4b98058c66f18bd983885ac5f4a54a04b24fbddf", size = 23198178, upload-time = "2026-05-18T19:57:14.68Z" }, ] [[package]] From 042896d4a0727e70ac1ba80d31aaa577649a7318 Mon Sep 17 00:00:00 2001 From: cjumel Date: Wed, 3 Jun 2026 12:14:28 +0200 Subject: [PATCH 2/5] chore: simplify setup of datetime module This will make the datetime import follow the google style convention, and this removes a useless test mock. --- src/linkup/_client.py | 67 ++++++++++++++++++++------------------- tests/unit/client_test.py | 2 -- 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/src/linkup/_client.py b/src/linkup/_client.py index 6a885a3..835c1e5 100644 --- a/src/linkup/_client.py +++ b/src/linkup/_client.py @@ -4,7 +4,6 @@ import json import os -from datetime import date # noqa: TC003 (`date` is used in test mocks) from typing import TYPE_CHECKING, Any, Literal, cast, overload import httpx @@ -47,6 +46,8 @@ from ._version import __version__ if TYPE_CHECKING: + import datetime + from .x402 import LinkupX402Signer @@ -107,8 +108,8 @@ def search( output_type: Literal["searchResults"], structured_output_schema: None = None, include_images: bool | None = None, - from_date: date | str | None = None, - to_date: date | str | None = None, + from_date: datetime.date | str | None = None, + to_date: datetime.date | str | None = None, exclude_domains: list[str] | None = None, include_domains: list[str] | None = None, max_results: int | None = None, @@ -126,8 +127,8 @@ def search( output_type: Literal["sourcedAnswer"], structured_output_schema: None = None, include_images: bool | None = None, - from_date: date | str | None = None, - to_date: date | str | None = None, + from_date: datetime.date | str | None = None, + to_date: datetime.date | str | None = None, exclude_domains: list[str] | None = None, include_domains: list[str] | None = None, max_results: int | None = None, @@ -145,8 +146,8 @@ def search( output_type: Literal["structured"], structured_output_schema: type[pydantic.BaseModel] | dict[str, Any] | str, include_images: bool | None = None, - from_date: date | str | None = None, - to_date: date | str | None = None, + from_date: datetime.date | str | None = None, + to_date: datetime.date | str | None = None, exclude_domains: list[str] | None = None, include_domains: list[str] | None = None, max_results: int | None = None, @@ -164,8 +165,8 @@ def search( output_type: Literal["structured"], structured_output_schema: type[pydantic.BaseModel] | dict[str, Any] | str, include_images: bool | None = None, - from_date: date | str | None = None, - to_date: date | str | None = None, + from_date: datetime.date | str | None = None, + to_date: datetime.date | str | None = None, exclude_domains: list[str] | None = None, include_domains: list[str] | None = None, max_results: int | None = None, @@ -183,8 +184,8 @@ def search( output_type: Literal["searchResults", "sourcedAnswer", "structured"], structured_output_schema: type[pydantic.BaseModel] | dict[str, Any] | str | None = None, include_images: bool | None = None, - from_date: date | str | None = None, - to_date: date | str | None = None, + from_date: datetime.date | str | None = None, + to_date: datetime.date | str | None = None, exclude_domains: list[str] | None = None, include_domains: list[str] | None = None, max_results: int | None = None, @@ -203,8 +204,8 @@ def search( output_type: Literal["searchResults", "sourcedAnswer", "structured"], structured_output_schema: type[pydantic.BaseModel] | dict[str, Any] | str | None = None, include_images: bool | None = None, - from_date: date | str | None = None, - to_date: date | str | None = None, + from_date: datetime.date | str | None = None, + to_date: datetime.date | str | None = None, exclude_domains: list[str] | None = None, include_domains: list[str] | None = None, max_results: int | None = None, @@ -304,8 +305,8 @@ async def async_search( output_type: Literal["searchResults"], structured_output_schema: None = None, include_images: bool | None = None, - from_date: date | str | None = None, - to_date: date | str | None = None, + from_date: datetime.date | str | None = None, + to_date: datetime.date | str | None = None, exclude_domains: list[str] | None = None, include_domains: list[str] | None = None, max_results: int | None = None, @@ -323,8 +324,8 @@ async def async_search( output_type: Literal["sourcedAnswer"], structured_output_schema: None = None, include_images: bool | None = None, - from_date: date | str | None = None, - to_date: date | str | None = None, + from_date: datetime.date | str | None = None, + to_date: datetime.date | str | None = None, exclude_domains: list[str] | None = None, include_domains: list[str] | None = None, max_results: int | None = None, @@ -342,8 +343,8 @@ async def async_search( output_type: Literal["structured"], structured_output_schema: type[pydantic.BaseModel] | dict[str, Any] | str, include_images: bool | None = None, - from_date: date | str | None = None, - to_date: date | str | None = None, + from_date: datetime.date | str | None = None, + to_date: datetime.date | str | None = None, exclude_domains: list[str] | None = None, include_domains: list[str] | None = None, max_results: int | None = None, @@ -361,8 +362,8 @@ async def async_search( output_type: Literal["structured"], structured_output_schema: type[pydantic.BaseModel] | dict[str, Any] | str, include_images: bool | None = None, - from_date: date | str | None = None, - to_date: date | str | None = None, + from_date: datetime.date | str | None = None, + to_date: datetime.date | str | None = None, exclude_domains: list[str] | None = None, include_domains: list[str] | None = None, max_results: int | None = None, @@ -380,8 +381,8 @@ async def async_search( output_type: Literal["searchResults", "sourcedAnswer", "structured"], structured_output_schema: type[pydantic.BaseModel] | dict[str, Any] | str | None = None, include_images: bool | None = None, - from_date: date | str | None = None, - to_date: date | str | None = None, + from_date: datetime.date | str | None = None, + to_date: datetime.date | str | None = None, exclude_domains: list[str] | None = None, include_domains: list[str] | None = None, max_results: int | None = None, @@ -400,8 +401,8 @@ async def async_search( output_type: Literal["searchResults", "sourcedAnswer", "structured"], structured_output_schema: type[pydantic.BaseModel] | dict[str, Any] | str | None = None, include_images: bool | None = None, - from_date: date | str | None = None, - to_date: date | str | None = None, + from_date: datetime.date | str | None = None, + to_date: datetime.date | str | None = None, exclude_domains: list[str] | None = None, include_domains: list[str] | None = None, max_results: int | None = None, @@ -499,8 +500,8 @@ def research( reasoning_depth: Literal["S", "M", "L", "XL"] | None = None, mode: Literal["answer", "auto", "investigate", "research"] | None = None, structured_output_schema: type[pydantic.BaseModel] | dict[str, Any] | str | None = None, - from_date: date | str | None = None, - to_date: date | str | None = None, + from_date: datetime.date | str | None = None, + to_date: datetime.date | str | None = None, exclude_domains: list[str] | None = None, include_domains: list[str] | None = None, timeout: float | None = None, @@ -572,8 +573,8 @@ async def async_research( reasoning_depth: Literal["S", "M", "L", "XL"] | None = None, mode: Literal["answer", "auto", "investigate", "research"] | None = None, structured_output_schema: type[pydantic.BaseModel] | dict[str, Any] | str | None = None, - from_date: date | str | None = None, - to_date: date | str | None = None, + from_date: datetime.date | str | None = None, + to_date: datetime.date | str | None = None, exclude_domains: list[str] | None = None, include_domains: list[str] | None = None, timeout: float | None = None, @@ -1401,8 +1402,8 @@ def _get_search_params( output_type: Literal["searchResults", "sourcedAnswer", "structured"], structured_output_schema: type[pydantic.BaseModel] | str | dict[str, Any] | None, include_images: bool | None, - from_date: date | str | None, - to_date: date | str | None, + from_date: datetime.date | str | None, + to_date: datetime.date | str | None, exclude_domains: list[str] | None, include_domains: list[str] | None, max_results: int | None, @@ -1458,8 +1459,8 @@ def _get_research_params( reasoning_depth: Literal["S", "M", "L", "XL"] | None, mode: Literal["answer", "auto", "investigate", "research"] | None, structured_output_schema: type[pydantic.BaseModel] | str | dict[str, Any] | None, - from_date: date | str | None, - to_date: date | str | None, + from_date: datetime.date | str | None, + to_date: datetime.date | str | None, exclude_domains: list[str] | None, include_domains: list[str] | None, ) -> dict[str, str | bool | list[str]]: diff --git a/tests/unit/client_test.py b/tests/unit/client_test.py index 39d2405..483539f 100644 --- a/tests/unit/client_test.py +++ b/tests/unit/client_test.py @@ -329,7 +329,6 @@ def test_search( mock_request_response_content: bytes, expected_search_response: Any, # noqa: ANN401 ) -> None: - mocker.patch("linkup._client.date").today.return_value = date(2000, 1, 1) request_mock = mocker.patch( "httpx.Client.request", return_value=Response( @@ -416,7 +415,6 @@ async def test_async_search( mock_request_response_content: bytes, expected_search_response: Any, # noqa: ANN401 ) -> None: - mocker.patch("linkup._client.date").today.return_value = date(2000, 1, 1) request_mock = mocker.patch( "httpx.AsyncClient.request", return_value=Response( From 66b6e26d06d9d8242de47008b1bde656861628ae Mon Sep 17 00:00:00 2001 From: cjumel Date: Wed, 3 Jun 2026 12:18:24 +0200 Subject: [PATCH 3/5] chore: remove inconsistent backticks in docstrings Use of backsticks was not consistent, we don't need them for now so I removed them all. --- src/linkup/_client.py | 67 +++++++++++++++++++++---------------------- src/linkup/_types.py | 4 +-- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/linkup/_client.py b/src/linkup/_client.py index 835c1e5..f42e55c 100644 --- a/src/linkup/_client.py +++ b/src/linkup/_client.py @@ -56,15 +56,14 @@ class LinkupClient: Args: api_key: The API key for the Linkup API. If None, the API key will be read from the - environment variable `LINKUP_API_KEY`. + environment variable LINKUP_API_KEY. base_url: The base URL for the Linkup API, for development purposes. x402_signer: An optional x402 signer for payment-gated endpoints. If provided, the client will attempt to handle 402 responses automatically. Cannot be used together with api_key. - auth_header: Custom header name to use for the API key (e.g. - ``"Ocp-Apim-Subscription-Key"``). When set, the API key value is sent as - ``: `` instead of the default - ``Authorization: Bearer ``. + auth_header: Custom header name to use for the API key (e.g. "Ocp-Apim-Subscription-Key"). + When set, the API key value is sent as : instead of the default + Authorization: Bearer . Raises: ValueError: If the API key is not provided and not found in the environment variable. @@ -213,7 +212,7 @@ def search( include_sources: bool | None = None, timeout: float | None = None, ) -> LinkupSearchResults | LinkupSourcedAnswer | JSONObject | LinkupSearchStructuredResponse: - """Perform a web search using the Linkup API `search` endpoint. + """Perform a web search using the Linkup API /search endpoint. All optional parameters will default to the Linkup API defaults when not provided. The Linkup API defaults are available in the @@ -230,14 +229,14 @@ def search( supporting it, and "structured" will base the output on the format provided in structured_output_schema. structured_output_schema: If output_type is "structured", specify the schema of the - output. Supported formats are a `pydantic.BaseModel`, a Python dictionary containing + output. Supported formats are a pydantic.BaseModel, a Python dictionary containing a valid object JSON schema, or a string representing a valid object JSON schema. include_images: Indicate whether images should be included during the search. from_date: The date from which the search results should be considered. Accepts a - `datetime.date`, `YYYY-MM-DD`, or full ISO datetime string. If None, the search + datetime.date, YYYY-MM-DD, or full ISO datetime string. If None, the search results will not be filtered by date. to_date: The date until which the search results should be considered. Accepts a - `datetime.date`, `YYYY-MM-DD`, or full ISO datetime string. If None, the search + datetime.date, YYYY-MM-DD, or full ISO datetime string. If None, the search results will not be filtered by date. exclude_domains: If you want to exclude specific domains from your search. include_domains: If you want the search to only return results from certain domains. @@ -410,7 +409,7 @@ async def async_search( include_sources: bool | None = None, timeout: float | None = None, ) -> LinkupSearchResults | LinkupSourcedAnswer | JSONObject | LinkupSearchStructuredResponse: - """Asynchronously perform a web search using the Linkup API `search` endpoint. + """Asynchronously perform a web search using the Linkup API /search endpoint. All optional parameters will default to the Linkup API defaults when not provided. The Linkup API defaults are available in the @@ -427,14 +426,14 @@ async def async_search( supporting it, and "structured" will base the output on the format provided in structured_output_schema. structured_output_schema: If output_type is "structured", specify the schema of the - output. Supported formats are a `pydantic.BaseModel`, a Python dictionary containing + output. Supported formats are a pydantic.BaseModel, a Python dictionary containing a valid object JSON schema, or a string representing a valid object JSON schema. include_images: Indicate whether images should be included during the search. from_date: The date from which the search results should be considered. Accepts a - `datetime.date`, `YYYY-MM-DD`, or full ISO datetime string. If None, the search + datetime.date, YYYY-MM-DD, or full ISO datetime string. If None, the search results will not be filtered by date. to_date: The date until which the search results should be considered. Accepts a - `datetime.date`, `YYYY-MM-DD`, or full ISO datetime string. If None, the search + datetime.date, YYYY-MM-DD, or full ISO datetime string. If None, the search results will not be filtered by date. exclude_domains: If you want to exclude specific domains from your search. include_domains: If you want the search to only return results from certain domains. @@ -506,26 +505,26 @@ def research( include_domains: list[str] | None = None, timeout: float | None = None, ) -> LinkupResearchTask: - """Create an asynchronous research task using the Linkup API `research` endpoint. + """Create an asynchronous research task using the Linkup API /research endpoint. - The returned task can be inspected later with `get_research`, `list_research`, `get_task`, - or `list_tasks`. + The returned task can be inspected later with get_research, list_research, get_task, + or list_tasks. Args: query: The research query to investigate. output_type: The expected research output type. Use "sourcedAnswer" for an answer with - supporting sources, or "structured" for output matching `structured_output_schema`. + supporting sources, or "structured" for output matching structured_output_schema. reasoning_depth: The amount of reasoning effort to use. If None, the Linkup API default is used. mode: The research mode to use. If None, the Linkup API default is used. structured_output_schema: If output_type is "structured", specify the output schema. - Supported formats are a `pydantic.BaseModel`, a Python dictionary containing a valid + Supported formats are a pydantic.BaseModel, a Python dictionary containing a valid object JSON schema, or a string representing a valid object JSON schema. from_date: The date from which the research sources should be considered. Accepts a - `datetime.date`, `YYYY-MM-DD`, or full ISO datetime string. If None, sources will + datetime.date, YYYY-MM-DD, or full ISO datetime string. If None, sources will not be filtered by a start date. to_date: The date until which the research sources should be considered. Accepts a - `datetime.date`, `YYYY-MM-DD`, or full ISO datetime string. If None, sources will + datetime.date, YYYY-MM-DD, or full ISO datetime string. If None, sources will not be filtered by an end date. exclude_domains: Domains to exclude from the research sources. include_domains: Domains to restrict the research sources to. @@ -579,26 +578,26 @@ async def async_research( include_domains: list[str] | None = None, timeout: float | None = None, ) -> LinkupResearchTask: - """Asynchronously create a research task using the Linkup API `research` endpoint. + """Asynchronously create a research task using the Linkup API /research endpoint. - The returned task can be inspected later with `async_get_research`, `async_list_research`, - `async_get_task`, or `async_list_tasks`. + The returned task can be inspected later with async_get_research, async_list_research, + async_get_task, or async_list_tasks. Args: query: The research query to investigate. output_type: The expected research output type. Use "sourcedAnswer" for an answer with - supporting sources, or "structured" for output matching `structured_output_schema`. + supporting sources, or "structured" for output matching structured_output_schema. reasoning_depth: The amount of reasoning effort to use. If None, the Linkup API default is used. mode: The research mode to use. If None, the Linkup API default is used. structured_output_schema: If output_type is "structured", specify the output schema. - Supported formats are a `pydantic.BaseModel`, a Python dictionary containing a valid + Supported formats are a pydantic.BaseModel, a Python dictionary containing a valid object JSON schema, or a string representing a valid object JSON schema. from_date: The date from which the research sources should be considered. Accepts a - `datetime.date`, `YYYY-MM-DD`, or full ISO datetime string. If None, sources will + datetime.date, YYYY-MM-DD, or full ISO datetime string. If None, sources will not be filtered by a start date. to_date: The date until which the research sources should be considered. Accepts a - `datetime.date`, `YYYY-MM-DD`, or full ISO datetime string. If None, sources will + datetime.date, YYYY-MM-DD, or full ISO datetime string. If None, sources will not be filtered by an end date. exclude_domains: Domains to exclude from the research sources. include_domains: Domains to restrict the research sources to. @@ -779,8 +778,8 @@ def create_tasks( """Create a mixed batch of search, fetch, and research tasks. Args: - tasks: The tasks to create. Supported task input models are `LinkupSearchTaskInput`, - `LinkupFetchTaskInput`, and `LinkupResearchTaskInput`. + tasks: The tasks to create. Supported task input models are LinkupSearchTaskInput, + LinkupFetchTaskInput, and LinkupResearchTaskInput. timeout: The timeout for the HTTP request, in seconds. If None, the request will have no timeout. @@ -812,8 +811,8 @@ async def async_create_tasks( """Asynchronously create a mixed batch of search, fetch, and research tasks. Args: - tasks: The tasks to create. Supported task input models are `LinkupSearchTaskInput`, - `LinkupFetchTaskInput`, and `LinkupResearchTaskInput`. + tasks: The tasks to create. Supported task input models are LinkupSearchTaskInput, + LinkupFetchTaskInput, and LinkupResearchTaskInput. timeout: The timeout for the HTTP request, in seconds. If None, the request will have no timeout. @@ -1001,7 +1000,7 @@ def fetch( extract_images: bool | None = None, timeout: float | None = None, ) -> LinkupFetchResponse: - """Fetch the content of a web page using the Linkup API `fetch` endpoint. + """Fetch the content of a web page using the Linkup API /fetch endpoint. All optional parameters will default to the Linkup API defaults when not provided. The Linkup API defaults are available in the @@ -1051,7 +1050,7 @@ async def async_fetch( extract_images: bool | None = None, timeout: float | None = None, ) -> LinkupFetchResponse: - """Asynchronously fetch the content of a web page using the Linkup API `fetch` endpoint. + """Asynchronously fetch the content of a web page using the Linkup API /fetch endpoint. All optional parameters will default to the Linkup API defaults when not provided. The Linkup API defaults are available in the @@ -1637,7 +1636,7 @@ def _parse_search_response( if output_type == "sourcedAnswer": return LinkupSourcedAnswer.model_validate(response_data) if output_type == "structured": - # HACK: we assume that `include_sources` will default to False, since the API output can + # HACK: we assume that include_sources will default to False, since the API output can # be arbitrary so we can't guess if it includes sources or not if include_sources: return LinkupSearchStructuredResponse.model_validate(response_data) diff --git a/src/linkup/_types.py b/src/linkup/_types.py index b7cb2da..c4b619b 100644 --- a/src/linkup/_types.py +++ b/src/linkup/_types.py @@ -86,7 +86,7 @@ class LinkupSourcedAnswer(_LinkupBaseModel): class LinkupSearchStructuredResponse(_LinkupBaseModel): - """A Linkup `search` structured response, with the sources supporting it. + """A Linkup /search structured response, with the sources supporting it. Attributes: data: The raw structured output dictionary. @@ -237,7 +237,7 @@ class LinkupTaskMetadata(_LinkupBaseModel): class LinkupTaskQuota(_LinkupBaseModel): - """Task quota information returned by `list_tasks`. + """Task quota information returned by list_tasks. Attributes: in_flight: The number of tasks currently in flight. From 7e554127ba38576b65a0132ba447395296e83ac5 Mon Sep 17 00:00:00 2001 From: cjumel Date: Wed, 3 Jun 2026 14:09:48 +0200 Subject: [PATCH 4/5] fix: more validation in models Add structured output schema validation on the relevant pydantic.BaseModel. --- src/linkup/_types.py | 16 ++++++++++++++++ tests/unit/client_test.py | 15 +++++++++++---- tests/unit/types_test.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 tests/unit/types_test.py diff --git a/src/linkup/_types.py b/src/linkup/_types.py index c4b619b..1736d47 100644 --- a/src/linkup/_types.py +++ b/src/linkup/_types.py @@ -164,6 +164,14 @@ class LinkupSearchTaskInput(_LinkupBaseModel): pydantic.Field(default=None, validation_alias="structuredOutputSchema") ) + @pydantic.model_validator(mode="after") + def _validate_structured_output_schema(self) -> LinkupSearchTaskInput: + if self.output_type == "structured" and self.structured_output_schema is None: + raise ValueError( + "structured_output_schema must be provided when output_type is 'structured'" + ) + return self + class LinkupResearchTaskInput(_LinkupBaseModel): """Input for creating or retrieving a research task. @@ -200,6 +208,14 @@ class LinkupResearchTaskInput(_LinkupBaseModel): pydantic.Field(default=None, validation_alias="structuredOutputSchema") ) + @pydantic.model_validator(mode="after") + def _validate_structured_output_schema(self) -> LinkupResearchTaskInput: + if self.output_type == "structured" and self.structured_output_schema is None: + raise ValueError( + "structured_output_schema must be provided when output_type is 'structured'" + ) + return self + class LinkupFetchTaskInput(_LinkupBaseModel): """Input for creating or retrieving a fetch task. diff --git a/tests/unit/client_test.py b/tests/unit/client_test.py index 483539f..50ec541 100644 --- a/tests/unit/client_test.py +++ b/tests/unit/client_test.py @@ -741,7 +741,10 @@ def test_get_research_structured_output_keeps_sourced_answer_shape_raw( "id": "bfeb26f5-f4d6-47d2-9818-7f62fbcd0b0c", "input": { "outputType": "structured", - "q": "query" + "q": "query", + "structuredOutputSchema": { + "type": "object" + } }, "output": { "answer": "structured answer field", @@ -761,6 +764,7 @@ def test_get_research_structured_output_keeps_sourced_answer_shape_raw( "answer": "structured answer field", "sources": [], } + assert research_response.input.structured_output_schema == {"type": "object"} @pytest.mark.asyncio @@ -1525,7 +1529,7 @@ def test_get_task_structured_search_output_keeps_search_results_shape_raw( assert task.output == {"results": []} -def test_get_task_structured_search_output_without_schema_raw( +def test_get_task_structured_search_output_raw( mocker: MockerFixture, client: linkup.Client ) -> None: mocker.patch( @@ -1540,7 +1544,10 @@ def test_get_task_structured_search_output_without_schema_raw( "input": { "depth": "standard", "outputType": "structured", - "q": "query" + "q": "query", + "structuredOutputSchema": { + "type": "object" + } }, "output": { "summary": "done" @@ -1556,7 +1563,7 @@ def test_get_task_structured_search_output_without_schema_raw( task = client.get_task("bfeb26f5-f4d6-47d2-9818-7f62fbcd0b0c") assert isinstance(task, linkup.SearchTask) - assert task.input.structured_output_schema is None + assert task.input.structured_output_schema == {"type": "object"} assert task.output == {"summary": "done"} diff --git a/tests/unit/types_test.py b/tests/unit/types_test.py new file mode 100644 index 0000000..7369bd6 --- /dev/null +++ b/tests/unit/types_test.py @@ -0,0 +1,34 @@ +from typing import Any + +import pydantic +import pytest + +import linkup + + +@pytest.mark.parametrize( + "task_input", + [ + pytest.param( + lambda: linkup.SearchTaskInput( + query="query", + depth="standard", + output_type="structured", + ), + id="search", + ), + pytest.param( + lambda: linkup.ResearchTaskInput( + query="query", + output_type="structured", + ), + id="research", + ), + ], +) +def test_structured_task_input_requires_schema(task_input: Any) -> None: # noqa: ANN401 + with pytest.raises( + pydantic.ValidationError, + match="structured_output_schema must be provided", + ): + task_input() From 42fec807c2cfabc48d0256ba2f92d15ac380d28a Mon Sep 17 00:00:00 2001 From: cjumel Date: Wed, 3 Jun 2026 16:06:49 +0200 Subject: [PATCH 5/5] chore: setup scratch file directory Scratch file directory is a place where we can put temporary scripts for development, so it is git-ignored and some lint checks are disabled on it. --- .gitignore | 25 +++++++++++++++++-------- .rumdl.toml | 1 + AGENTS.md | 6 +++++- pyproject.toml | 1 + 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index cf9da06..6db9e46 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,30 @@ -# Python-generated files -.idea/ +# Environment variables +.env + +# Reserved directories +data/ +scratch/ + +# Python artefacts .ipynb_checkpoints/ .venv/ -.vscode/ __pycache__/ build/ dist/ wheels/ .coverage -.env *.egg-info *.py[oc] -# MacOS stuff -.DS_Store +# IDE artefacts +.cursor/ +.idea/ +.vscode/ -# Coding agents stuff +# Coding agents artefacts +.claude/ +.opencode/ .rtk/ -playground.py +# System artefacts +.DS_Store diff --git a/.rumdl.toml b/.rumdl.toml index 6f4772b..d945774 100644 --- a/.rumdl.toml +++ b/.rumdl.toml @@ -1,4 +1,5 @@ [global] +exclude = ["scratch/**"] line-length = 100 [MD013] diff --git a/AGENTS.md b/AGENTS.md index e3d4271..5eb221c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,11 @@ When adding or changing a public API capability, update the relevant pieces toge ## Validation -Before opening a PR, run the CI checks: `make lint typecheck test`. +After having done some changes or before opening a PR, run the CI checks: +`make lint typecheck test`. + +Never run the underlying checks without using these `make` commands (e.g. with `uv run ruff ...`), +as these kind of checks won't take into account some settings (like the `exclude` list). ## Non-Goals diff --git a/pyproject.toml b/pyproject.toml index ba3391d..027ad90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ target-version = "py310" unsafe-fixes = true [tool.ruff.lint] +exclude = ["scratch/"] explicit-preview-rules = true extend-ignore = ["D107"] preview = true