diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..1c0689c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,43 @@ +name: test + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.12"] + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.22" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + pip install pytest pytest-asyncio + + - name: Build and start mock server + working-directory: tests/mockserver + run: | + go build -o mockserver . + ./mockserver & + sleep 2 + curl -sf http://localhost:18080/ || echo "mock server ready" + + - name: Run tests + run: pytest tests/ -v --tb=short -x diff --git a/.speakeasy/gen.lock b/.speakeasy/gen.lock index 32d4c6e..409aa50 100644 --- a/.speakeasy/gen.lock +++ b/.speakeasy/gen.lock @@ -1,41 +1,41 @@ lockVersion: 2.0.0 id: 77dbe0f5-bd71-4808-8102-230d4ffc4dc7 management: - docChecksum: 737fdbe515a4595dd4cb732be4173bc5 + docChecksum: 7d5cd58eeb46bb65394f8d93701de5d3 docVersion: 1.0.0 - speakeasyVersion: 1.700.2 - generationVersion: 2.801.2 - releaseVersion: 2.2.0 - configChecksum: bc4fccaed878f41b87c40a40144d567a + speakeasyVersion: 1.733.4 + generationVersion: 2.845.12 + releaseVersion: 2.3.0 + configChecksum: 8a3f331950b6f7e77a32f7cd1db0b5f8 published: true persistentEdits: - generation_id: bda6d739-828c-46ef-8b2b-7f5d321717b8 - pristine_commit_hash: 38d34a633bf11b39648f78b9bf39c57149232305 - pristine_tree_hash: 080fe89b15b5a14d6dac0d81620a968340a129c4 + generation_id: 137cb5fb-bdb8-4499-aeff-a6321a1bdb1b + pristine_commit_hash: 6b6328c7225337c31677e43d97830359df07c179 + pristine_tree_hash: bf00b109fbef4edf704a638751b1cd9a7c8e666a features: python: acceptHeaders: 3.0.0 additionalDependencies: 1.0.0 - constsAndDefaults: 1.0.5 - core: 5.23.18 + constsAndDefaults: 1.0.7 + core: 6.0.15 defaultEnabledRetries: 0.2.0 enumUnions: 0.1.0 envVarSecurityUsage: 0.3.2 flatRequests: 1.0.1 flattening: 3.1.1 - globalSecurity: 3.0.4 + globalSecurity: 3.0.5 globalSecurityCallbacks: 1.0.0 globalSecurityFlattening: 1.0.0 - globalServerURLs: 3.2.0 + globalServerURLs: 3.2.1 methodArguments: 1.0.2 - methodServerURLs: 3.1.1 - nameOverrides: 3.0.1 + methodServerURLs: 3.1.2 + nameOverrides: 3.0.3 nullables: 1.0.2 - responseFormat: 1.0.1 - retries: 3.0.3 + responseFormat: 1.1.0 + retries: 3.0.4 sdkHooks: 1.2.1 - serverEvents: 1.0.11 - unions: 3.1.2 + serverEvents: 1.0.13 + unions: 3.1.4 trackedFiles: .gitattributes: id: 24139dae6567 @@ -47,8 +47,8 @@ trackedFiles: pristine_git_object: 8d79f0abb72526f1fb34a4c03e5bba612c6ba2ae USAGE.md: id: 3aed33ce6e6f - last_write_checksum: sha1:8596faf77e8a805d411ad8d53ae5e3ad1f46acfc - pristine_git_object: d2feb9083049890c49da397c56a410384ffb99e7 + last_write_checksum: sha1:9c6e80ea166a9a6d5973a69ce2788e6c65f264c6 + pristine_git_object: 9f08d215c86cda516bb2283683def17f64339774 docs/errors/agentruns400responseerror.md: id: 4a255bc34dbc last_write_checksum: sha1:52fd0795019408368d8d17c72c7dafe6267e7799 @@ -63,16 +63,28 @@ trackedFiles: pristine_git_object: de23c318d560e32c2015128e590348d52d84dce5 docs/errors/contentsforbiddenerror.md: id: f29faf0667d2 - last_write_checksum: sha1:34760666dac2379e320fcc6397de368079925caa - pristine_git_object: 2c39593aa27ef9b2b9fe8e5a8f4ca0ed6a8022ac + last_write_checksum: sha1:3a863233031f25949057e9e258e41a55965dfb6e + pristine_git_object: 81d690f0cd0f42cbe2db676a45ee59db84def43f docs/errors/contentsinternalservererror.md: id: 6f136fea0383 - last_write_checksum: sha1:f07d3cd9b4584bef05c1ce3b59b711390d149fc2 - pristine_git_object: e6b4b20686de782a85cf56f05e394af7256431ce + last_write_checksum: sha1:89b8b80416518f036c8825ed773e4b092a7dde00 + pristine_git_object: 8dabd744caf24f9ce90fab0d8bb3f5ecdfeb7e44 docs/errors/contentsunauthorizederror.md: id: c673d1fd50f2 - last_write_checksum: sha1:2ee93d84242a907007aa55200cb8456e095f1ca2 - pristine_git_object: f45e9e4c869798cd9a6ebb00518943f5a622a859 + last_write_checksum: sha1:c3aae77709d87098770406f004c19a27ef930e83 + pristine_git_object: 486b384d58421877ad73fa963a812c8cc83b5f1c + docs/errors/researchforbiddenerror.md: + id: 38294d725501 + last_write_checksum: sha1:6a2a0e75dc6ba751562e90bbb85fda2dda20ff60 + pristine_git_object: 9f18fd00d61656f4db977f5adf3ff2274700f20d + docs/errors/researchinternalservererror.md: + id: e140fbce184f + last_write_checksum: sha1:66f12319d49979b07d18489f5fdefad610ea42f6 + pristine_git_object: 5576cc81dab7ef5b597c8c71d79497b5c7cff741 + docs/errors/researchunauthorizederror.md: + id: 90348ea8ad14 + last_write_checksum: sha1:8948e2a9d19578d31dc9556b3a498208f6eaea1e + pristine_git_object: 9516b1732d6641bf0077ceee9c044ef662d456c8 docs/errors/searchforbiddenerror.md: id: fb63bcabfad1 last_write_checksum: sha1:b7e3a2369be34808acaa576f9384b9daf32b9b2c @@ -85,14 +97,18 @@ trackedFiles: id: bd64fef413da last_write_checksum: sha1:1d70ee061964342b0e48d2f9f726bbc49d492820 pristine_git_object: f799351c432b366215f25cff8710c8b082ceb997 + docs/errors/unprocessableentityerror.md: + id: 49570373f8e1 + last_write_checksum: sha1:6995ccac169fa137740bbbf5136a8fd7ee016e15 + pristine_git_object: 5abff965877a345c70cc64d6138b7e84946622f7 docs/models/advancedagentrunsrequest.md: id: 1b4e175934b4 last_write_checksum: sha1:73a48e950bb5313ee31f36c8bafff77c22078861 pristine_git_object: 596bc50cef57e8402f45539d69327682a047f7e1 docs/models/agentrunsbatchresponse.md: id: 04584f262eed - last_write_checksum: sha1:b99416099fb3ddf30186ef638f2e8d39e2c562c6 - pristine_git_object: 3fe03a1507c1261432e8ec3479debddf57829833 + last_write_checksum: sha1:22c9d779fdccf76ec7b4592630b4b263d6f8e5ce + pristine_git_object: 5922b796b8395ed5f99cc99ce4515bf0c39c1cf3 docs/models/agentrunsresponseoutput.md: id: 628cb50c13b9 last_write_checksum: sha1:b7fe4c0c6d2ea03dd892fc9621b49beb2462cc4d @@ -131,12 +147,16 @@ trackedFiles: pristine_git_object: 6050bc0a757b2ea380ae2b1aaffc789588b44fa0 docs/models/contentsrequest.md: id: 9e0c7be98068 - last_write_checksum: sha1:7b33e15b1f304499b4794f0521118e0d6a56bcf7 - pristine_git_object: 7cae5ed34c809cb4d4f341ed6ffa7253036d090c + last_write_checksum: sha1:ee5a86cac6f99df2c46b6a500490dd69d289e194 + pristine_git_object: fec30b007750d47d0d9cb260a0a984fe1d846ba7 docs/models/contentsresponse.md: id: 3be584b2a8cf last_write_checksum: sha1:357146cd86137bee392eed939594c1630937b377 pristine_git_object: 7e81c7aa44ebb89cd11fc8ed4e75ad0802496589 + docs/models/contenttype.md: + id: 78e9266f4216 + last_write_checksum: sha1:b07de74e6e51de014aac32016de80db12ff83ee3 + pristine_git_object: b043bcf6036d260ace3df072d2dc57d4fdcb4656 docs/models/country.md: id: a9be7df1a5df last_write_checksum: sha1:fe1e2a0d00c9e256751e759eb55ba5e722562be8 @@ -161,10 +181,14 @@ trackedFiles: id: 88acdb1a4cde last_write_checksum: sha1:5a20b012ce095064aaedfc09db5dbdf170ffc27c pristine_git_object: e3b507d2b9e37f660ba541d2d4967ae2db681434 - docs/models/input.md: - id: 5cbc446a3956 - last_write_checksum: sha1:f7672dc07961b1c7816e32b5d13ff699d4ea55ac - pristine_git_object: 54bf616d4a0b7923835d7825e283e911322e2171 + docs/models/input1.md: + id: 13ac5c7b8e41 + last_write_checksum: sha1:cd17fe02727f29475456bd76bfd5dd2ecf61612d + pristine_git_object: 8b6df1b3fe78557e96f42e4c72b02a32d3b7c8e8 + docs/models/input2.md: + id: f914f15b24f1 + last_write_checksum: sha1:4e59087bf04e435ff5ec762ecf80e3a2abd490f6 + pristine_git_object: fefc4e562d59c1e90c2c3886d6c1735be5d88a9d docs/models/language.md: id: 5bac2bb42c7c last_write_checksum: sha1:06df16a4f694c3844bfcd18e3f44e23b3f49efc9 @@ -189,10 +213,38 @@ trackedFiles: id: c070462247dc last_write_checksum: sha1:f5aea8d54b1d4f21cd98d934ae913ecdecd35a6b pristine_git_object: fec0835c5d443cb3675a49ff221152b17b6bb027 + docs/models/output.md: + id: 376633b966cd + last_write_checksum: sha1:45d7d4baf3d5bd47998470a3261d5d842216a895 + pristine_git_object: d7c2771a56efeb0f63d0c388c621748c5ac93572 docs/models/reportverbosity.md: id: d06de274a3c5 last_write_checksum: sha1:a9b998858b5efb701cfee75690c1a0e160ffdce1 pristine_git_object: 1a2aae5cdb01b31eb1590b76e52f08b9ab6d19f0 + docs/models/researchdetail.md: + id: ac4ed946dccb + last_write_checksum: sha1:f43174f11eb01588ded19f3e06c072a3589c4df9 + pristine_git_object: e635556b70c8aff5211a13aab43c6aa27fad4b4e + docs/models/researcheffort.md: + id: 5b67676468b4 + last_write_checksum: sha1:8e3dc42805efa230a574cce116659bff6a550061 + pristine_git_object: e54e2072a76ab8bad9edc76563323a501a761204 + docs/models/researchinput.md: + id: a5099da2ac37 + last_write_checksum: sha1:a2212309beac7493be856f4dcff2c08af5fd3c9e + pristine_git_object: 440a4440591c38081f66c1d9f37395b1b4faf89d + docs/models/researchloc.md: + id: 3aeace91cdd2 + last_write_checksum: sha1:2666811f4486af8652e691adb62b0481f4fb2692 + pristine_git_object: 54741e7b741e96ee1b130afa66523e61215aa21e + docs/models/researchrequest.md: + id: bf0af2378820 + last_write_checksum: sha1:92742ce12e177769db3e738aa27e9cbc7ccb986a + pristine_git_object: be8ad6ef6bd2a278d5e78fbab735437dcd3d6ae4 + docs/models/researchresponse.md: + id: e2ac4b94e0e5 + last_write_checksum: sha1:0c004acb2376d5bd678f1daad46abf0fc5aeef68 + pristine_git_object: 58a1bc31d29cb4d036f52cbeb4bd2ffd95ce88ab docs/models/researchtool.md: id: 68f310f2701f last_write_checksum: sha1:d5f85e978e3fb30fb80a8a362b0f8decf2a020fd @@ -267,8 +319,8 @@ trackedFiles: pristine_git_object: 95bb5b7394fb231b3b9cef813050863140ea65b9 docs/models/searchfreshness.md: id: 11d81669a994 - last_write_checksum: sha1:9fd5f232c47c024ff3ca46e87b1d5f11c4092f17 - pristine_git_object: 1cbf6d3bdde9e6970b63ca045ffc47809b4c400d + last_write_checksum: sha1:fa45060666c3fa42d6b65c2d7ec27b5cb0c9b483 + pristine_git_object: e9478183db391bc49b2f3a0a9a877e358cb4899d docs/models/searchlivecrawl.md: id: "953023757e55" last_write_checksum: sha1:ffbcb9b63322706b9bc9fe1bae22fc6d83efc405 @@ -279,8 +331,8 @@ trackedFiles: pristine_git_object: 690de1f50a63ba69c5a73a5a2f7792b94b968436 docs/models/searchrequest.md: id: bc36c5e5aee1 - last_write_checksum: sha1:074dc535f01cf4a27617325f37dc10b05c013ac8 - pristine_git_object: 9f3b32961a391ac69ea424c277ad07354e18c978 + last_write_checksum: sha1:da9699efe63a7983e7265387fd12334508881ef3 + pristine_git_object: 1adb45e0d501c83f4ecdbb7b84879eefb20a3d52 docs/models/searchresponse.md: id: d5606b4d403f last_write_checksum: sha1:27a70c88fc494aa9dfda622d16da17d1a5c06996 @@ -293,6 +345,10 @@ trackedFiles: id: 452e4d4eb67a last_write_checksum: sha1:71fdd7bbeb4eccce70c9b87e9631d708571e0f17 pristine_git_object: 0346db068b3d37366beee9c844a3643d6a332cee + docs/models/source.md: + id: 6541ef7b41e7 + last_write_checksum: sha1:483698db3a1c935665a55f062761e2250173f813 + pristine_git_object: 174794d3a4732ea91a88d0b9b8d058127fc62f27 docs/models/tool.md: id: 8966139dbeed last_write_checksum: sha1:75d2ca9e2e2b19ceaa89db32c055b290709edc5a @@ -323,28 +379,32 @@ trackedFiles: pristine_git_object: b8a67d378796378511ff5969bc86f6c6406f3576 docs/sdks/contentssdk/README.md: id: 7ca17aeff32d - last_write_checksum: sha1:870eb631cee0e6c143ccad943299a47d9cf58590 - pristine_git_object: 2a10aa787ea9269dccf47dfb4e56955ec944afb3 + last_write_checksum: sha1:ce6d4513d733269954debb41f7f453ba8759723b + pristine_git_object: e4ce470a54ad136244116e5d218ef62f54402da9 docs/sdks/runs/README.md: id: 4598fd39b715 - last_write_checksum: sha1:786ae1c85b29c47a919f31c27de58a484e66f6f1 - pristine_git_object: 759645a33f64a41d928c30fada03a9ecf2dc7adf + last_write_checksum: sha1:a4fa67e2225b766de164057368ea6fea5e9035ed + pristine_git_object: c8d570f44294f1c6d2905701506867ef1ff4576b docs/sdks/search/README.md: id: 5c534716244c - last_write_checksum: sha1:25c9f92183efe902dec6dffc7908061778b0981a - pristine_git_object: 1a33aefa1bf3b03dc3787d8859bc5ad2b307135c + last_write_checksum: sha1:2a9b67f8171c4ff82f2e46d8fef716e645fedfe8 + pristine_git_object: 72bfdc70ed5ced95fbf35318614a49976a823af1 + docs/sdks/you/README.md: + id: 1abb954b0afb + last_write_checksum: sha1:4f5546910a5dfcda054541096cc98b945d41cab6 + pristine_git_object: 6be14705d30d36358fe5e422c8d84c45171b1467 py.typed: id: 258c3ed47ae4 last_write_checksum: sha1:8efc425ffe830805ffcc0f3055871bdcdc542c60 pristine_git_object: 3e38f1a929f7d6b1d6de74604aa87e3d8f010544 pylintrc: id: 7ce8b9f946e6 - last_write_checksum: sha1:f24e0b43d3e3461208ccec7ecb543f79662d01c4 - pristine_git_object: 000d9930f874a1b01cda74f245fc00e1b6eae49e + last_write_checksum: sha1:b08ef8689f6474e400ebbe9c8baf7d7b9fae3a73 + pristine_git_object: f456032107a9387ba6c98afd1c981df2f4b3d636 pyproject.toml: id: 5d07e7d72637 - last_write_checksum: sha1:7e0c682673181d1b26f49b21a2e077210fa3e331 - pristine_git_object: 04d6d744e4a5e4f949a42340fb38ea00983c2768 + last_write_checksum: sha1:52fa68a283dba10ff1069fed74cf81ab03fc1468 + pristine_git_object: 290ebddeeb9306329af4f48500a7fc9a708eff70 scripts/publish.sh: id: fe273b08f514 last_write_checksum: sha1:adc9b741c12ad1591ab4870eabe20f0d0a86cd1a @@ -367,8 +427,8 @@ trackedFiles: pristine_git_object: 13987cea395263dc4b60a5ee4cb9d8c65f95fc9f src/youdotcom/_version.py: id: 5224f82ecc7b - last_write_checksum: sha1:4f1a3e64c755ff5a6b967b45f26c5590dee06006 - pristine_git_object: a4f88d8a6fc02699d89426efc537b4fa827d725b + last_write_checksum: sha1:01235a099c799feb9e36352e6c10fcfd6371552b + pristine_git_object: c998bfdeb29e4fee6ea9edb89caaec14cac7c449 src/youdotcom/agents.py: id: 0ec0f4c4e0d0 last_write_checksum: sha1:4b58c15455f5410f050cfc8be831bfb6401d68ad @@ -379,12 +439,12 @@ trackedFiles: pristine_git_object: ea145dd9a33a92ca4d64ca9ee88d499dcf8adc78 src/youdotcom/contents_sdk.py: id: 0684c08251f6 - last_write_checksum: sha1:5c6f88cbbaf65aaeb1d5a8c79deab5691f88794d - pristine_git_object: 1f0e0eb693f183a8d590aaf8d1f45efd5bb134e2 + last_write_checksum: sha1:df4747161f79c38ccdcfbdf858f361e8d611ca11 + pristine_git_object: a21a07c01058262259ce70c61c5cc04d708e2086 src/youdotcom/errors/__init__.py: id: e7ee44aa2c0f - last_write_checksum: sha1:56c82ad67cfbc471560e903a15a1beec7e1d6dbb - pristine_git_object: 8e9618709bbcb898541e4f782dd3b0940eaf8861 + last_write_checksum: sha1:3cda7690491c92a99b17243d42ffdc41cdb64abd + pristine_git_object: 2193daa39144685fdad94368aae2789a134d11d3 src/youdotcom/errors/agentruns400response_error.py: id: 6e04ce5f87f1 last_write_checksum: sha1:3d8694955e3799f0c762f605074d403b8b2203ab @@ -399,12 +459,16 @@ trackedFiles: pristine_git_object: c86c94af3ec33f7c7c7d167a25e0dd3a13775206 src/youdotcom/errors/contentsop.py: id: 92163cd72b73 - last_write_checksum: sha1:eddc98b1a08e9578ed18fce95180fd89a76e6714 - pristine_git_object: 5f36db15be8adcdd8b7764caaf9ed33bbf670298 + last_write_checksum: sha1:0589479e94a35d68deac9064f098ce0569b3e36d + pristine_git_object: 443fd304ff07e7623911e094281ab2f1661843f1 src/youdotcom/errors/no_response_error.py: id: 9f953dc697cf last_write_checksum: sha1:7f326424a7d5ae1bcd5c89a0d6b3dbda9138942f pristine_git_object: 1deab64bc43e1e65bf3c412d326a4032ce342366 + src/youdotcom/errors/researchop.py: + id: 8143ca635d3f + last_write_checksum: sha1:66e92b01868dda50157ced7254e072293e868283 + pristine_git_object: b1a5836fc9ac80feae48c5bc937f5ebb61dc803c src/youdotcom/errors/responsevalidationerror.py: id: 0ad5034298b3 last_write_checksum: sha1:f95060059297e22c183f9e387df44eb03aac1f5c @@ -427,40 +491,40 @@ trackedFiles: pristine_git_object: 89560b566073785535643e694c112bedbd3db13d src/youdotcom/models/__init__.py: id: ad350e4fd8c1 - last_write_checksum: sha1:ddc46a43ee7fb62d9d538fd0e781a1b755684aee - pristine_git_object: 41218f5cc252adaa186f7943df955b52aecf1b93 + last_write_checksum: sha1:b7ac540f79ae19ad6bc96101a99b3d2df3d29325 + pristine_git_object: 804e5c280c7b35c49fcb7ea9db759c0b5444274d src/youdotcom/models/advancedagentrunsrequest.py: id: 6bb8d5dd67d4 - last_write_checksum: sha1:0752685b5c0a8ca60afeb92f21f8fbbdc0799ea6 - pristine_git_object: a85f6b17504ee07097611a7f4f0174b8f6e03d06 + last_write_checksum: sha1:d1207266cea794cac55224e3bc6aed2f66422949 + pristine_git_object: f271b2bff38f6c55a2a6e3f890ef04bb6b1405c9 src/youdotcom/models/agentruns422response_error.py: id: 6731cdd29afd last_write_checksum: sha1:e9c70fe90257d4f3a93ea7d730e4d7f662dbd7bb pristine_git_object: 41240562660e2a4dd1ee4823ae31e843ce51c056 src/youdotcom/models/agentrunsbatchresponse.py: id: 38fe9f202ccb - last_write_checksum: sha1:e4c5b05246757af6c6a74092724b132b81c056cd - pristine_git_object: 832454728fd13c82a055a1e3833ff457baca4974 + last_write_checksum: sha1:0717ce0715c0cd84de6f21e9822c8029197bcf67 + pristine_git_object: 187616cbe44da1ecaeffe76bb0fc55985f1e242d src/youdotcom/models/agentrunsresponseoutput.py: id: 6ac5478f43e0 last_write_checksum: sha1:8ebe3e761e97a1fcb81a3e180dd9643f705e7367 pristine_git_object: 14726f52263a8f553a12ee6dd24f5451a65adf62 src/youdotcom/models/agentrunsresponsewebsearchresult.py: id: e85bdd982508 - last_write_checksum: sha1:dda492d7b5222936608a2df71acb64ce1de35e78 - pristine_git_object: 74c19ac620c27de81d68cabc4cd2375c1dad9251 + last_write_checksum: sha1:03d003761e73fea392805382b36545792acac7dd + pristine_git_object: a0a105cab1cf1c161370a4579b7ef93cda0ed4c0 src/youdotcom/models/agentrunsstreamingresponse.py: id: 1423c3f03bf9 last_write_checksum: sha1:0fff2d6e0785c7744a50455853c309f7ca609fd5 pristine_git_object: 1a2c119e6ecee3f1116cc98968a9cc86cdde503e src/youdotcom/models/agentsrunsop.py: id: b0d55d74eb29 - last_write_checksum: sha1:7b2b7d6c14bb77b25efc2ddc29e9336d04aed473 - pristine_git_object: 52a57d5de789822fbeb65ad45b7273c6402ea1d2 + last_write_checksum: sha1:3b2198586c6819837835eb8a6a2b5e90722f0136 + pristine_git_object: 2ac90c01209da9babf694e453a9bdd7ba8f7f517 src/youdotcom/models/computetool.py: id: 5353a2de6f97 - last_write_checksum: sha1:329af82f45084613d277c2d05c12577f89f52684 - pristine_git_object: b70495a7134c788a9ee85b9c485222ed2a289605 + last_write_checksum: sha1:4c65a3868a85ac0ca4a08e8ab10f1dc34bf7f364 + pristine_git_object: 12538817aaf1efe50c87ee29995f423b1c89567c src/youdotcom/models/contents.py: id: c1d5af212a4c last_write_checksum: sha1:c3b6a119c617166269004dd2967c5121c36ad98d @@ -475,8 +539,8 @@ trackedFiles: pristine_git_object: 6324e63c7f280e72fd1c0d1711b78b3c37be448b src/youdotcom/models/contentsop.py: id: ca42ef875c5f - last_write_checksum: sha1:eec71df278e9042f4af6045c7d4225d197409e84 - pristine_git_object: b1f47da8a10f88ff29f538f5de875b5564a0502a + last_write_checksum: sha1:281db131fb9b8e4feef7f8c31cea58fc99aa89e0 + pristine_git_object: fc47c7fd77d691d12d06e8cd6a2f9d8ddca8a7a3 src/youdotcom/models/country.py: id: 725d2a57cc07 last_write_checksum: sha1:7827b3e65afd2ee86078e45ac373cedfe5d68766 @@ -487,8 +551,8 @@ trackedFiles: pristine_git_object: 759f45bd1053d6b4f52400434edadb027661958b src/youdotcom/models/expressagentrunsrequest.py: id: 0f698f43a90d - last_write_checksum: sha1:e3cb44616eaaff810d34f96c06a9f785e26455b8 - pristine_git_object: a3e47f8d2183c52d41d65c98144bb4c124ad9d22 + last_write_checksum: sha1:182e79b7735aaa20c2e7203a787267cde8f2c2b7 + pristine_git_object: f4f5de0e9c96adf70854876f0eb06d319e464eba src/youdotcom/models/freshness.py: id: c7c960c20e5e last_write_checksum: sha1:48ae86226706f17277fc58f132b55eef545888a4 @@ -509,38 +573,42 @@ trackedFiles: id: 5a8683f42b91 last_write_checksum: sha1:b7c084407a5584d770deb61970d4953825a3bd2c pristine_git_object: 14a8b432a575bc4c291723e98f2586beae4f5939 + src/youdotcom/models/researchop.py: + id: c1ae2c3f13d9 + last_write_checksum: sha1:fc7c95702b93a5e31edbda349b8acc0199ad084b + pristine_git_object: f8ec45ce17672dc92b004d989d6c677f89b06668 src/youdotcom/models/researchtool.py: id: 4e0236b1b670 - last_write_checksum: sha1:e7aab4a37c5329910b60ec367fd4eb187030fb4b - pristine_git_object: bb9930a2a1f92106ef070e890b0956185d518fe6 + last_write_checksum: sha1:e90b83ae9dbf18da27bbbf2db81fbd18a16cb4d1 + pristine_git_object: 445f60cce6ef58e3c0da474ac55da3e298ae114b src/youdotcom/models/response_created.py: id: ec9dee8aac4e - last_write_checksum: sha1:49ba1a996a840a512c8202ee7f0157d4088f0232 - pristine_git_object: a7e26251381588c9e333d086b02fff1df5b3ebdf + last_write_checksum: sha1:a4b321cd48f8409910efc27a1bd145f1de254dc7 + pristine_git_object: 509085d15b21cb96bebd088785f773555d029759 src/youdotcom/models/response_done.py: id: 436337f20c7e - last_write_checksum: sha1:f0a64a1ed114a70279b43eedfe40f5c697625cb0 - pristine_git_object: fa2d419af9c8a0d797f78079af635d230bc364e1 + last_write_checksum: sha1:93b52e55c42a41453c2047d62d6e783624fb831b + pristine_git_object: 599f8a7d5a7f8a4a917d0b3fa1dc2b18a1f9f199 src/youdotcom/models/response_output_content_full.py: id: 7f5d8f4dd4fb - last_write_checksum: sha1:2818eafbdc6c4c63476204b8332f0624aa391e7b - pristine_git_object: dc6f1b22810a9ec245a1c4841f9aa015491d1d1b + last_write_checksum: sha1:b1cb42414e2f43af2491431da5e513db6c2d1702 + pristine_git_object: b2bd70cecbd90d17b2f90f83cb1e0e83a328f1b7 src/youdotcom/models/response_output_item_added.py: id: 190e4d2047f4 - last_write_checksum: sha1:dd57e970c0febca6850003851f3e4bda9590d01c - pristine_git_object: 7f4af2441324372bfa03442dbb845e70c2ec13e8 + last_write_checksum: sha1:b5e09d2685f2197d1f08717fddd1705e5a291a5c + pristine_git_object: 72949626cb9a3422ae999ea6dcee0cefdc2f205c src/youdotcom/models/response_output_item_done.py: id: 7a086b15ec74 - last_write_checksum: sha1:53282e6b9c5f5bfdd07cac89a98894ef0d6ca4bc - pristine_git_object: bd3f6c4e8a9181b465f2aadd1d413e4ca434784b + last_write_checksum: sha1:088dc34f175c26df4cda6b122148f1de3e3095a5 + pristine_git_object: 54f4cbcfdd897d72c82f926ad627f2200d3530ba src/youdotcom/models/response_output_text_delta.py: id: a77d5296fe54 - last_write_checksum: sha1:23ae45a9aeca687747a8496a28c1e498c8a129b8 - pristine_git_object: 4520ad5693f79ec1a6364d9f1653f04f84bea165 + last_write_checksum: sha1:8010dd2b2a35d13f0174d80a06fb549d469645e6 + pristine_git_object: 36aa5a4f36e4d781a5113b0a339a474f4c4b1a09 src/youdotcom/models/response_starting.py: id: ba22f359a5db - last_write_checksum: sha1:61c7a768bccc976be03ec1332b53d36327deaa6c - pristine_git_object: 481a4810bc90069c1248431db9566193cc5f7325 + last_write_checksum: sha1:282c112ecc2b6343c5b34fc9af6f3ede1b6b8489 + pristine_git_object: 438a1913bc2391b0d972e30461d0361a7134cb66 src/youdotcom/models/safesearch.py: id: ad014425b7f3 last_write_checksum: sha1:bda9731af9142b81be895fca26e2499e30edbc1b @@ -551,8 +619,8 @@ trackedFiles: pristine_git_object: cec092e4f820d3dfd8d09f8b7bc224c418b8fd49 src/youdotcom/models/searchop.py: id: 525c0a4e8872 - last_write_checksum: sha1:733b5c4948d03d49ee658e33d3a2986189017ea6 - pristine_git_object: 9b7de765e1cab75a977abd9b0e10ea22b777712b + last_write_checksum: sha1:ea521b1b3cf6d34aeabe0268b571f4eec698bf23 + pristine_git_object: a06a0fdcef28814a51726bba187cbc82958d7f48 src/youdotcom/models/security.py: id: 3a94d17768c4 last_write_checksum: sha1:8b1c11f19ca3684c0330a4884c0f45b165578a7b @@ -563,28 +631,28 @@ trackedFiles: pristine_git_object: 666d75aaa5f5ab22a259d8fe1aa08f599c79e104 src/youdotcom/models/websearchtool.py: id: 8240ffdc3807 - last_write_checksum: sha1:02a8450d7193b3f1758ec9355bed1cc5cbf97509 - pristine_git_object: 274c2fd80a8aeacaf50c9a1f861a2875911ba95e + last_write_checksum: sha1:44338571c786fbf9377a82ca0e0ff57a26472e66 + pristine_git_object: 7834ec3a54a74c67a575b3c2987299989f07aac7 src/youdotcom/py.typed: id: 90f76277734b last_write_checksum: sha1:8efc425ffe830805ffcc0f3055871bdcdc542c60 pristine_git_object: 3e38f1a929f7d6b1d6de74604aa87e3d8f010544 src/youdotcom/runs.py: id: 8011c1ffa5a1 - last_write_checksum: sha1:c7d1aab6522c5c1f7fa2bb9dc32b11c05aa37921 - pristine_git_object: 02e1733d3d5a03a6710c02a944fa633296dcd1ec + last_write_checksum: sha1:08b88bce4f60cfac1d6744f077e27175a20b1987 + pristine_git_object: 0fc591713191bcd659057fcdc05e2b07139b3457 src/youdotcom/sdk.py: id: 90954e74e7b7 - last_write_checksum: sha1:643a3d7ddcbd81045cd1423654aabe7458687f8d - pristine_git_object: d25ff3ec6877474e673102b6e04814fbcbd35058 + last_write_checksum: sha1:5e14bed0ce6e55976b40bacba13ccd526ec818fe + pristine_git_object: c72bef1f7ba827516449fd4e30e1378189a9542b src/youdotcom/sdkconfiguration.py: id: eb56427350d9 - last_write_checksum: sha1:097096eb430f3eecee2fa55dd5393eafcf85e8a7 - pristine_git_object: 05b634ced959105937834b966e7ded78aa89ee6f + last_write_checksum: sha1:f62ffed1bbe732f18d96076c622fccf3eb8c2f6c + pristine_git_object: ea49a89ef4840777202f1440fbf116e2f63d7a56 src/youdotcom/search.py: id: 5e475f9db47f - last_write_checksum: sha1:7ebcee8ab5b543fa069d41d2671e8e60d206d12d - pristine_git_object: 47c8e0cc21cd5b80fc7f796567aa36b81424badc + last_write_checksum: sha1:fa2b85d71f14cc589fbb190c2475f387aea8f5fa + pristine_git_object: 355ce7774b9722b1f02ab98634f9481831ace609 src/youdotcom/types/__init__.py: id: 5e0774b59bbc last_write_checksum: sha1:140ebdd01a46f92ffc710c52c958c4eba3cf68ed @@ -595,8 +663,8 @@ trackedFiles: pristine_git_object: a9a640a1a7048736383f96c67c6290c86bf536ee src/youdotcom/utils/__init__.py: id: bd4acfd8ce6a - last_write_checksum: sha1:ffaf69e6877a274b08066215f5a31a1b344f5402 - pristine_git_object: f4525b2057c3d54fb3b9eff6248fd172478ce94c + last_write_checksum: sha1:0f93d821f9cb3e061ea125d881bb6f61166738dd + pristine_git_object: aded7597c2bbc4e3fa7f555755d894671ac57741 src/youdotcom/utils/annotations.py: id: bc5c21b7250e last_write_checksum: sha1:a4824ad65f730303e4e1e3ec1febf87b4eb46dbc @@ -605,14 +673,18 @@ trackedFiles: id: 2b6c0dd73070 last_write_checksum: sha1:c721e4123000e7dc61ec52b28a739439d9e17341 pristine_git_object: a6c52cd61bbe2d459046c940ce5e8c469f2f0664 + src/youdotcom/utils/dynamic_imports.py: + id: 09b830ea97a1 + last_write_checksum: sha1:a1940c63feb8eddfd8026de53384baf5056d5dcc + pristine_git_object: 673edf82a97d0fea7295625d3e092ea369a36b79 src/youdotcom/utils/enums.py: id: 6e8be91b2dd2 last_write_checksum: sha1:bc8c3c1285ae09ba8a094ee5c3d9c7f41fa1284d pristine_git_object: 3324e1bc2668c54c4d5f5a1a845675319757a828 src/youdotcom/utils/eventstreaming.py: id: 5f31d9da5a93 - last_write_checksum: sha1:bababae5d54b7efc360db701daa49e18a92c2f3b - pristine_git_object: 0969899bfc491e5e408d05643525f347ea95e4fc + last_write_checksum: sha1:ffa870a25a7e4e2015bfd7a467ccd3aa1de97f0e + pristine_git_object: f2052fc22d9fd6c663ba3dce019fe234ca37108b src/youdotcom/utils/forms.py: id: 1bf9b877c054 last_write_checksum: sha1:0ca31459b99f761fcc6d0557a0a38daac4ad50f4 @@ -639,12 +711,12 @@ trackedFiles: pristine_git_object: 1de32b6d26f46590232f398fdba6ce0072f1659c src/youdotcom/utils/retries.py: id: 384c61cdc8f6 - last_write_checksum: sha1:5b97ac4f59357d70c2529975d50364c88bcad607 - pristine_git_object: 88a91b10cd2076b4a2c6cff2ac6bfaa5e3c5ad13 + last_write_checksum: sha1:471372f5c5d1dd5583239c9cf3c75f1b636e5d87 + pristine_git_object: af07d4e941007af4213c5ec9047ef8a2fca04e5e src/youdotcom/utils/security.py: id: 41e3b3176b50 - last_write_checksum: sha1:acc69159fb3ce552bbaaee640e72e297601a6f04 - pristine_git_object: ee42de6f36f641709604e81cc0461de3adf2b019 + last_write_checksum: sha1:8f41e203536f0ab841ea31498ca73556d5cb92b1 + pristine_git_object: e51915d1bd67d63cb62077f19b1b5cd13f747dd6 src/youdotcom/utils/serializers.py: id: 360f8e2583cc last_write_checksum: sha1:ce1d8d7f500a9ccba0aeca5057cee9c271f4dfd7 @@ -753,6 +825,7 @@ examples: query: query: "Your query" language: "EN" + count: 10 responses: "200": application/json: {"results": {"web": [{"url": "https://you.com", "title": "The World's Greatest Search Engine!", "description": "Search on YDC", "snippets": ["I'm an AI assistant that helps you get more done. What can I help you with?"], "thumbnail_url": "https://www.somethumbnailsite.com/thumbnail.jpg", "page_age": "2025-06-25T11:41:00", "authors": ["John Doe"], "favicon_url": "https://someurl.com/favicon"}], "news": [{"title": "Exclusive | You.com becomes the backbone of the EU's AI strategy", "description": "As the EU's AI strategy is being debated, You.com becomes the backbone of the EU's AI strategy.", "page_age": "2025-06-25T11:41:00", "thumbnail_url": "https://www.somethumbnailsite.com/thumbnail.jpg", "url": "https://www.you.com/news/eu-ai-strategy-youcom"}]}, "metadata": {"search_uuid": "942ccbdd-7705-4d9c-9d37-4ef386658e90", "query": "Your query", "latency": 0.123}} @@ -767,6 +840,7 @@ examples: query: query: "Your query" language: "EN" + count: 10 responses: "401": application/json: {"detail": "API key is required"} @@ -775,6 +849,7 @@ examples: query: query: "Your query" language: "EN" + count: 10 responses: "401": application/json: {"detail": "Invalid or expired API key"} @@ -783,6 +858,7 @@ examples: query: query: "Your query" language: "EN" + count: 10 responses: "401": application/json: {"detail": ""} @@ -791,6 +867,7 @@ examples: query: query: "Your query" language: "EN" + count: 10 responses: "403": application/json: {"detail": "Missing required scopes"} @@ -799,6 +876,7 @@ examples: query: query: "Your query" language: "EN" + count: 10 responses: "500": application/json: {"detail": "Internal authentication error"} @@ -807,6 +885,7 @@ examples: query: query: "Your query" language: "EN" + count: 10 responses: "500": application/json: {"detail": "Internal authorization error"} @@ -859,5 +938,80 @@ examples: application/json: {} "500": application/json: {} + research: + speakeasy-default-research: + requestBody: + application/json: {"input": "", "research_effort": "standard"} + responses: + "200": + application/json: {"output": {"content": "", "content_type": "text", "sources": []}} + "401": + application/json: {} + "403": + application/json: {} + "422": + application/json: {"detail": [{"type": "missing", "loc": ["body", "input"], "msg": "Field required", "input": ""}]} + "500": + application/json: {} + missingApiKey: + requestBody: + application/json: {"input": "", "research_effort": "standard"} + responses: + "401": + application/json: {"detail": "API key is required"} + invalidOrExpired: + requestBody: + application/json: {"input": "", "research_effort": "standard"} + responses: + "401": + application/json: {"detail": "Invalid or expired API key"} + otherAuthParsing: + requestBody: + application/json: {"input": "", "research_effort": "standard"} + responses: + "401": + application/json: {"detail": ""} + missingScopes: + requestBody: + application/json: {"input": "", "research_effort": "standard"} + responses: + "403": + application/json: {"detail": "Missing required scopes"} + missingField: + requestBody: + application/json: {"input": "", "research_effort": "standard"} + responses: + "422": + application/json: {"detail": [{"type": "missing", "loc": ["body", "input"], "msg": "Field required", "input": {}}]} + invalidEnum: + requestBody: + application/json: {"input": "", "research_effort": "standard"} + responses: + "422": + application/json: {"detail": [{"type": "enum", "loc": ["body", "research_effort"], "msg": "Input should be 'lite', 'standard', 'deep' or 'exhaustive'", "input": "invalid_value", "ctx": {"expected": "'lite', 'standard', 'deep' or 'exhaustive'"}}]} + stringTooLong: + requestBody: + application/json: {"input": "", "research_effort": "standard"} + responses: + "422": + application/json: {"detail": [{"type": "string_too_long", "loc": ["body", "input"], "msg": "String should have at most 40000 characters", "input": "", "ctx": {"max_length": 40000}}]} + invalidJson: + requestBody: + application/json: {"input": "", "research_effort": "standard"} + responses: + "422": + application/json: {"detail": [{"type": "json_invalid", "loc": ["body", 115], "msg": "JSON decode error", "input": {}, "ctx": {"error": "Invalid control character at"}}]} + authFailure: + requestBody: + application/json: {"input": "", "research_effort": "standard"} + responses: + "500": + application/json: {"detail": "Internal authentication error"} + authorizationFailure: + requestBody: + application/json: {"input": "", "research_effort": "standard"} + responses: + "500": + application/json: {"detail": "Internal authorization error"} examplesVersion: 1.0.2 generatedTests: {} diff --git a/.speakeasy/gen.yaml b/.speakeasy/gen.yaml index b9d081f..f2ffbc8 100644 --- a/.speakeasy/gen.yaml +++ b/.speakeasy/gen.yaml @@ -14,6 +14,7 @@ generation: securityFeb2025: true sharedErrorComponentsApr2025: true sharedNestedComponentsJan2026: false + nameOverrideFeb2026: false auth: oAuth2ClientCredentialsEnabled: true oAuth2PasswordEnabled: true @@ -23,6 +24,7 @@ generation: schemas: allOfMergeStrategy: shallowMerge requestBodyFieldName: body + versioningStrategy: automatic persistentEdits: {} tests: generateTests: true @@ -32,7 +34,7 @@ generation: examples: - usage.md python: - version: 2.2.0 + version: 2.3.0 additionalDependencies: dev: {} main: {} @@ -53,10 +55,13 @@ python: envVarPrefix: YOU fixFlags: asyncPaginationSep2025: true + conflictResistantModelImportsFeb2026: false responseRequiredSep2024: true flattenGlobalSecurity: true flattenRequests: true flatteningOrder: parameters-first + forwardCompatibleEnumsByDefault: false + forwardCompatibleUnionsByDefault: "false" imports: option: openapi paths: diff --git a/.speakeasy/out.openapi.yaml b/.speakeasy/out.openapi.yaml index 5a9c5e7..85b6237 100644 --- a/.speakeasy/out.openapi.yaml +++ b/.speakeasy/out.openapi.yaml @@ -1,14 +1,19 @@ openapi: 3.1.0 info: title: You.com API - description: | + description: |- + Unified API for Express, Advanced, and Custom Agents from You.com + Get the best search results from web and news sources + Returns the HTML or Markdown of a target webpage + Multi-step reasoning with comprehensive research capabilities Comprehensive API for You.com services: - **Agents API**: Execute queries using Express, Advanced, and Custom AI agents - **Search API**: Get search results from web and news sources - **Contents API**: Retrieve and process web page content version: 1.0.0 servers: - - url: https://ydc-index.io + - url: https://api.you.com + x-fern-server-name: Production paths: /v1/agents/runs: post: @@ -110,8 +115,6 @@ paths: application/json: schema: $ref: "#/components/schemas/AgentRuns422Response" - servers: - - url: https://api.you.com tags: - agents.runs x-speakeasy-name-override: create @@ -119,8 +122,7 @@ paths: get: operationId: search summary: Returns a list of unified search results from web and news sources - servers: - - url: https://ydc-index.io + description: This endpoint is designed to return LLM-ready web results based on a user's query. Based on a classification mechanism, it can return web results and news associated with your query. If you need to feed an LLM with the results of a query that sounds like `What are the latest geopolitical updates from India`, then this endpoint is the right one for you. security: - ApiKeyAuth: [] parameters: @@ -140,9 +142,13 @@ paths: type: integer maximum: 100 minimum: 1 + default: 10 - name: freshness in: query - description: Specifies the freshness of the results to return. + description: |- + Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. + + When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. required: false schema: oneOf: @@ -347,10 +353,13 @@ paths: tags: - search x-speakeasy-name-override: unified + servers: + - url: https://ydc-index.io /v1/contents: post: operationId: contents summary: Returns the content of the web pages + description: Returns the HTML or Markdown of a target webpage. requestBody: content: application/json: @@ -367,10 +376,11 @@ paths: formats: $ref: '#/components/schemas/ContentsFormats' crawl_timeout: - type: number + type: integer maximum: 60 minimum: 1 - description: The timeout in seconds for crawling each URL. Must be between 1 and 60 seconds. + description: Maximum time in seconds to wait for page content. Must be between 1 and 60 seconds. Default is 10 seconds. + default: 10 example: 10 required: true responses: @@ -403,7 +413,7 @@ paths: metadata: $ref: '#/components/schemas/ContentsMetadata' "401": - description: Unauthorized + description: Unauthorized. Problems with API key. content: application/json: schema: @@ -426,7 +436,7 @@ paths: value: detail: "" "403": - description: Forbidden + description: Forbidden. API key lacks scope for this path. content: application/json: schema: @@ -440,7 +450,7 @@ paths: value: detail: "Missing required scopes" "500": - description: Internal Server Error + description: Internal Server Error during authentication/authorization middleware. content: application/json: schema: @@ -460,13 +470,292 @@ paths: tags: - contents x-speakeasy-name-override: generate + servers: + - url: https://ydc-index.io + /v1/research: + post: + operationId: research + summary: Returns comprehensive research-grade answers with multi-step reasoning + description: Research goes beyond a single web search. In response to your question, it runs multiple searches, reads through the sources, and synthesizes everything into a thorough, well-cited answer. Use it when a question is too complex for a simple lookup, and when you need a response you can actually trust and verify. + security: + - ApiKeyAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + input: + type: string + maxLength: 40000 + description: |- + The research question or complex query requiring in-depth investigation and multi-step reasoning. + + Note: The maximum length of the input is 40,000 characters. + research_effort: + type: string + enum: + - lite + - standard + - deep + - exhaustive + description: |- + Controls how much time and effort the Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. + + Available levels: + - `lite`: Returns answers quickly. Good for straightforward questions that just need a fast, reliable answer. + - `standard`: The default. Balances speed and depth, a good fit for most questions. + - `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed. + - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. + default: standard + required: + - input + example: + input: Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed? + research_effort: lite + required: true + responses: + "200": + description: A JSON object containing a comprehensive answer with citations and supporting search results + content: + application/json: + schema: + type: object + properties: + output: + type: object + properties: + content: + type: string + description: The comprehensive response with inline citations. The content is formatted in Markdown and includes numbered citations that reference the items in the sources array. + content_type: + type: string + enum: + - text + description: The format of the content field. + sources: + type: array + items: + type: object + properties: + url: + type: string + description: The URL of the source webpage. + title: + type: string + description: The title of the source webpage. + snippets: + type: array + items: + type: string + description: Relevant excerpts from the source page that were used in generating the answer. + required: + - url + description: A list of web sources used to generate the answer. + required: + - content + - content_type + - sources + description: The research output containing the answer and sources. + required: + - output + example: + output: + content: |- + Over the past decade, some global cities have shown improvements in air quality due to specific actions. Beijing, for example, made significant strides in improving its air quality through coordinated control measures with surrounding areas, collaborative planning, unified standards, joint emergency responses, and information sharing [[1]]. These efforts, including a five-year action plan for air pollution prevention and control, have helped to substantially improve air quality in the Jing-Jin-Ji region [[2]]. + + Paris has also seen improvements, with a 50% reduction in Nitrogen dioxide pollution and a 55% decrease in particulate matter citywide since 2005. This was achieved through its climate strategy, which included adding more bike lanes and increasing cycling networks [[3]]. Wellington, New Zealand, improved air quality in one of its busiest districts by increasing the percentage of electric buses from 5% to over 50% between 2022 and 2023, leading to a 50% reduction in black carbon and a 29% drop in nitrogen dioxide levels [[3]]. Mexico City, once known as the world's most polluted city in the early 1990s, has vastly improved its air quality, with the daily concentration of SO2 declining significantly by 2018 [[2]]. + + While many cities globally have experienced persistently high or even rising levels of air pollution, especially concerning PM2.5 concentrations, NO2 exposures have shown an encouraging trend, with 211 more cities meeting the WHO guideline in 2019 compared to 2010 [[4]]. Local policies have been instrumental in these improvements [[4]]. + content_type: text + sources: + - url: https://sustainablemobility.iclei.org/air-pollution-beijing/ + title: "Clearing the skies: how Beijing tackled air pollution & what lies ..." + snippets: + - >- + However, Beijing has made remarkable strides in recent years to improve its air quality, setting an example for other cities grappling with similar challenges. The root causes Comparing the past 20 years of its development to the 20 before, Beijing's GDP, population, and vehicles sharply increased by 1078%, 74%, and 335% respectively (UNEP, 2019). + - >- + The city actively coordinated air pollution control measures with surrounding areas, such as the Beijing-Tianjin-Hebei region. Collaborative planning, unified standards, joint emergency responses, and information sharing significantly improved the air quality in this broader region. + - >- + While Beijing has made significant strides, challenges remain. The average PM2.5 level is still six times higher than the World Health Organization's (WHO) guideline, and the 2021-22 improvement may be partially attributed to measures taken for the Winter Olympics. + - >- + As China emerged as the world's largest automobile producer and consumer, it grappled with the detrimental impacts of increased oil consumption. Furthermore, the high level of coal consumption, especially during the winter heating season, contributed to the city's deteriorating air quality, reaching an average of 101.56 micrograms of PM2.5 particles per cubic meter in 2013 (Statista, 2023). + - url: https://blogs.worldbank.org/en/voices/tackling-poor-air-quality-lessons-three-cities + title: "Tackling poor air quality: Lessons from three cities" + snippets: + - >- + The latest World Bank report, Clearing the Air: A Tale of Three Cities, chose Beijing, New Delhi and Mexico City to assess how current and past efforts improved air quality. In the early 1990s, Mexico City was known as the world's most polluted city and while there are still challenges, air quality has vastly improved. Daily concentration of SO2 – a contributor to PM2.5 concentrations – declined from 300 µg/m3 in the 1990s to less than 100 µg/m3 in 2018. + - >- + In China, the ministries of Environmental Protection (now the Ministry of Ecology and Environment), Industry and Information Technology, Finance, Housing and Rural Development, along with the National Development and Reform Commission and National Energy Administration, worked together to issue a five-year action plan for air pollution prevention and control for the entire Jing-Jin-Ji region that surrounds Beijing and includes the municipality of Beijing, municipality of Tianjin, the province of Hebei, and small parts of Henan, Shanxi, inner Mongolia, and Shandong. What's encouraging about this new work is that it shows that with the right policies, incentives and information, air quality can be improved substantially, particularly as countries work to grow back cleaner after the pandemic. + - >- + Failure to provide such incentives in India in the late 1990s resulted in the government developing plans but not implementing them. This led to India's Supreme Court stepping in to force the government to implement policy measures. A recent government of India program to provide performance-based grants to cities to reward improvements in air quality is a step in the right direction. + - >- + The cost associated with health impacts of outdoor PM2.5 air pollution is estimated to be US$5.7 trillion, equivalent to 4.8 percent of global GDP, according to World Bank research. The COVID-19 pandemic further highlights why addressing air pollution is so important, with early research pointing to links between air pollution, illness and death due to the virus. On the flip side, the economic lockdowns caused by the pandemic, while devastating for communities, did result in some noticeable improvements in air quality but these improvements were inconsistent, particularly when it came to PM2.5. + - url: https://www.weforum.org/stories/2025/06/urban-mobility-improving-cities-air-quality/ + title: "Boosting clean air strategies in cities around the world | World ..." + snippets: + - >- + Comprehensive cycling networks improve air quality while also transforming urban mobility. Paris has added more bike lanes to its cityscape in recent years. Between 2022 and 2023 alone, bike path usage doubled during rush hour and cyclists now outnumber cars on many of the city's streets. The results of Paris' growing cycling network are promising. Alongside other elements of Paris's climate strategy, cycling has contributed to a 50% reduction in Nitrogen dioxide pollution and 55% decrease in particulate matter citywide since 2005. + - >- + In 2025, the alliance and members of the Global New Mobility Coalition will launch a new workstream on Transport and Urbanism that aims to speed up cross-sector collaboration on implementing proven mobility options to improve air quality and drive sustainable growth. + - >- + Air pollution has been estimated to cause 4.2 million premature deaths worldwide per year, according to the World Health Organization, and nearly half of urban airborne contamination comes from city transport. While vehicles are essential to the vitality of cities, without the right policies in place, transport will continue to be a major contributor to harmful air pollution. + - >- + In Wellington, New Zealand, the percentage of electric buses travelling across the city's heavily trafficked Golden Mile corridor rose from 5% to over 50% between 2022 and 2023. This shift led to a 50% reduction in black carbon and a 29% drop in nitrogen dioxide levels throughout the district. This has significantly improved air quality in one of the busiest parts of Wellington, as well as lowering noise pollution. + - url: https://www.stateofglobalair.org/resources/health-in-cities + title: "Air Pollution and Health in Cities | State of Global Air" + snippets: + - >- + Globally, NO2 exposures are heading in an encouraging direction as 211 more cities met the WHO guideline of 10 µg/m3 in 2019 compared to 2010. However, NO2 pollution is worsening in some other regions. Percentage of cities by population-weighted annual average pollutant concentration in 2010 and 2019. However, interventions targeting pollution at the local scale have successfully improved air quality in some cities. + - >- + Local policies have improved air quality in some cities, while pollution has worsened in others. Overall, many cities have seen persistently high — and even rising — levels of air pollution over the past decade. PM2.5 exposures remained stagnant in many cities from 2010 to 2019. + - >- + Cities are often hotspots for poor air quality. As rapid urbanization increases the number of people breathing dangerously polluted air, city-level data can help inform targeted efforts to curb urban air pollution and improve public health. + - >- + Explore air quality and health data for your city using our new interactive app here. Most cities have polluted air, but the type of pollution varies from place to place. Local policies have improved air quality in some cities, while pollution has worsened in others. + "422": + description: Unprocessable Entity. Request validation failed. + content: + application/json: + schema: + type: object + properties: + detail: + type: array + items: + type: object + properties: + type: + type: string + description: The validation error type. + example: missing + loc: + type: array + items: + oneOf: + - type: string + - type: integer + description: The location of the error as a path of segments (strings for field names, integers for byte offsets). + example: ["body", "input"] + msg: + type: string + description: A human-readable description of the error. + example: Field required + input: + oneOf: + - type: string + - type: object + description: The input value that caused the error. + ctx: + type: object + additionalProperties: true + description: Additional context about the error. + required: + - type + - loc + - msg + - input + examples: + missingField: + summary: Required field missing + value: + detail: + - type: missing + loc: ["body", "input"] + msg: Field required + input: {} + invalidEnum: + summary: Invalid enum value for research_effort + value: + detail: + - type: enum + loc: ["body", "research_effort"] + msg: "Input should be 'lite', 'standard', 'deep' or 'exhaustive'" + input: invalid_value + ctx: + expected: "'lite', 'standard', 'deep' or 'exhaustive'" + stringTooLong: + summary: Input exceeds maximum length + value: + detail: + - type: string_too_long + loc: ["body", "input"] + msg: String should have at most 40000 characters + input: + ctx: + max_length: 40000 + invalidJson: + summary: Invalid JSON body + value: + detail: + - type: json_invalid + loc: ["body", 115] + msg: JSON decode error + input: {} + ctx: + error: Invalid control character at + "401": + description: Unauthorized. Problems with API key. + content: + application/json: + schema: + type: object + properties: + detail: + type: string + description: Error detail message. + examples: + missingApiKey: + summary: Missing API key + value: + detail: API key is required + invalidOrExpired: + summary: Invalid/expired API key + value: + detail: Invalid or expired API key + otherAuthParsing: + summary: Other auth parsing errors + value: + detail: + "403": + description: Forbidden. API key lacks scope for this path. + content: + application/json: + schema: + type: object + properties: + detail: + type: string + examples: + missingScopes: + summary: Missing required scopes + value: + detail: Missing required scopes + "500": + description: Internal Server Error during authentication/authorization middleware. + content: + application/json: + schema: + type: object + properties: + detail: + type: string + examples: + authFailure: + summary: Authentication failure + value: + detail: Internal authentication error + authorizationFailure: + summary: Authorization failure + value: + detail: Internal authorization error components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key - description: "The unique API Key required to authorize API access. Learn how to get yours in the [“Get your API key” section of the documentation](https://docs.you.com/get-started/quickstart#get-your-api-key)." + description: "A unique API Key is required to authorize API access. [Get your API Key with free credits](https://you.com/platform/api-keys)." schemas: # REQUEST SCHEMAS ExpressAgentRunsRequest: @@ -1109,7 +1398,7 @@ components: - html - markdown - metadata - description: The formats of the content to be returned. Can include 'html', 'markdown', and/or 'metadata'. + description: Array of content formats to return. All included formats are returned in the response. Include "metadata" to get JSON-LD and OpenGraph information, if available. example: ["html", "markdown"] ContentsMetadata: type: object diff --git a/.speakeasy/workflow.lock b/.speakeasy/workflow.lock index 8d24327..55180ec 100644 --- a/.speakeasy/workflow.lock +++ b/.speakeasy/workflow.lock @@ -1,9 +1,9 @@ -speakeasyVersion: 1.700.2 +speakeasyVersion: 1.733.4 sources: You.com API: sourceNamespace: you-com-search-api - sourceRevisionDigest: sha256:e3c94436fd3002c31990ab21146f126bc6ab86e4b5f0f97449b8cf3dba72c23d - sourceBlobDigest: sha256:0d5b7b460a6e3aae27141ac5678258fadbccb3133081106441de0eed8e629f59 + sourceRevisionDigest: sha256:1d9828035b2b9ec387808b7167bd6f5d833492b9c95a7c236e736cacb24c2e2a + sourceBlobDigest: sha256:b436d65210d9571c37986fbf57326dabb68500ef8c1ac06ae8378768e6c8a254 tags: - latest - 1.0.0 @@ -11,20 +11,21 @@ targets: you: source: You.com API sourceNamespace: you-com-search-api - sourceRevisionDigest: sha256:e3c94436fd3002c31990ab21146f126bc6ab86e4b5f0f97449b8cf3dba72c23d - sourceBlobDigest: sha256:0d5b7b460a6e3aae27141ac5678258fadbccb3133081106441de0eed8e629f59 + sourceRevisionDigest: sha256:1d9828035b2b9ec387808b7167bd6f5d833492b9c95a7c236e736cacb24c2e2a + sourceBlobDigest: sha256:b436d65210d9571c37986fbf57326dabb68500ef8c1ac06ae8378768e6c8a254 codeSamplesNamespace: you-com-search-api-code-samples - codeSamplesRevisionDigest: sha256:19322950f0dbf18cb2174131380b92626cd09e4b7e1f0b87a8769f031a1df0d5 + codeSamplesRevisionDigest: sha256:15160af083a36e6567c9dbd9a52eb1f963895eeb92bc129469febf19f65e74bb workflow: workflowVersion: 1.0.0 speakeasyVersion: latest sources: You.com API: inputs: - - location: https://you.com/specs/openapi_unified_agents.yaml - - location: https://raw.githubusercontent.com/Su-Sea/youdotcom-frontend/8b2343769b0cb30f5a50851979bbe414404d4614/public/specs/openapi_search_v1.yaml?token=GHSAT0AAAAAADSREVEUXQ6IIHAHVE2YZBDY2L3WB3A - - location: https://youdotcom-ceb3k6nx2-susea.vercel.app/specs/openapi_contents.yaml - - location: https://you.com/specs/openapi_base.yaml + - location: https://youdotcom-pr-11819.vercel.app/specs/openapi_unified_agents.yaml + - location: https://youdotcom-pr-11819.vercel.app/specs/openapi_search_v1.yaml + - location: https://youdotcom-pr-11819.vercel.app/specs/openapi_contents.yaml + - location: https://youdotcom-pr-11819.vercel.app/specs/openapi_research.yaml + - location: https://youdotcom-pr-11819.vercel.app/specs/openapi_base.yaml overlays: - location: ./overlays/python_overlay.yaml output: .speakeasy/out.openapi.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 4929696..b0a3a98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,41 @@ All notable changes to the You.com Python SDK will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.3.0] - 2026-02-27 + +### Added + +- **Research API**: New `research()` and `research_async()` methods on the main `You` client for comprehensive, multi-step research answers with citations. The Research API goes beyond a single web search by running multiple searches, reading sources, and synthesizing thorough, well-cited answers. + +```python +from youdotcom import You +from youdotcom.models import ResearchEffort + +you = You() +res = you.research( + input="What are the latest advances in quantum computing?", + research_effort=ResearchEffort.DEEP, +) +print(res.output.content) +for source in res.output.sources: + print(f" - {source.title or 'Untitled'}: {source.url}") +``` + +- **`ResearchEffort` enum**: Controls depth of research (`lite`, `standard`, `deep`, `exhaustive`) +- **Research models**: `ResearchRequest`, `ResearchResponse`, `Output`, `Source`, `ContentType` +- **Research errors**: `ResearchUnauthorizedError`, `ResearchForbiddenError`, `ResearchInternalServerError`, `UnprocessableEntityError` +- **`AgentRuns400ResponseError`**: New error class for 400 Bad Request responses from the Agents API + +### Changed + +- **Default server URL**: Changed from `https://ydc-index.io` to `https://api.you.com`. If you were relying on the default, no action needed as both resolve to the same API. +- **Python version requirement**: Now requires Python >=3.10 (previously >=3.9.2) +- **Search API `count` parameter**: Now defaults to `10` instead of `None` +- **Contents API `crawl_timeout`**: Type changed from `float` to `int`, default is now `10` seconds +- **Speakeasy generator**: Updated from v2.801.2 to v2.845.12 + +--- + ## [2.2.0] - 2026-01-29 ### Changed @@ -302,7 +337,7 @@ Error classes have been renamed for consistency and clarity: | Old Name (1.x) | New Name (2.0) | |----------------|----------------| | `PostV1AgentsRunsUnauthorizedError` | `AgentRuns401ResponseError` | -| `PostV1AgentsRunsForbiddenError` | `AgentRuns422ResponseError` | +| `PostV1AgentsRunsForbiddenError` | Removed (403 now handled by `YouDefaultError`) | | `GetV1SearchUnauthorizedError` | `SearchUnauthorizedError` | | `GetV1SearchForbiddenError` | `SearchForbiddenError` | | `PostV1ContentsUnauthorizedError` | `ContentsUnauthorizedError` | diff --git a/MIGRATION.md b/MIGRATION.md index 29129fd..519d851 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,4 +1,47 @@ -# Migration Guide: 1.x to 2.0 +# Migration Guide + +## 1.x → 2.3.0 (Latest) + +This guide covers breaking changes introduced in 2.3.0. If you are upgrading from 1.x, also read the [1.x → 2.0](#1x-to-20) section below. + +### Breaking Changes in 2.3.0 + +#### Python 3.10 now required + +The minimum supported Python version has been raised from `>=3.9.2` to `>=3.10`. If you are running Python 3.9, you must upgrade before installing this version. + +```bash +python --version # must be 3.10 or later +pip install "youdotcom>=2.3.0" +``` + +#### Search API: `count` default changed + +`you.search.unified()` now defaults `count` to `10` (previously `None`/no default). If your code omits `count` and relies on the API-server default, you will now always receive 10 results. + +```python +# Before (2.x < 2.3.0): count was unset, server decided +res = you.search.unified(query="AI news") + +# After (2.3.0+): equivalent explicit call +res = you.search.unified(query="AI news", count=10) +``` + +#### Contents API: `crawl_timeout` type changed + +`crawl_timeout` has changed from `float` to `int`. Passing a float (e.g., `crawl_timeout=5.5`) will now raise a validation error. + +```python +# Before: float was accepted +res = you.contents.generate(urls=["https://example.com"], crawl_timeout=5.5) + +# After: use int +res = you.contents.generate(urls=["https://example.com"], crawl_timeout=5) +``` + +--- + +## 1.x to 2.0 This guide helps you upgrade your code from You.com Python SDK 1.x to 2.0. @@ -12,7 +55,7 @@ This guide helps you upgrade your code from You.com Python SDK 1.x to 2.0. | Custom agent | `agent="uuid-string"` | `request=CustomAgentRunsRequest(agent="uuid-string", ...)` | | Verbosity enum | `Verbosity` | `ReportVerbosity` | | Format enum | `Format` | `ContentsFormats` | -| Contents format param | `format_=ContentsFormat.X` | `formats=[ContentsFormats.X]` | +| Contents format param | `format_=Format.X` | `formats=[ContentsFormats.X]` | ## Step-by-Step Migration diff --git a/README.md b/README.md index 99b1935..73f9688 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,11 @@ The official developer-friendly & type-safe Python SDK specifically designed to ## Summary -You.com API: Comprehensive API for You.com services: +You.com API: Unified API for Express, Advanced, and Custom Agents from You.com +Get the best search results from web and news sources +Returns the HTML or Markdown of a target webpage +Multi-step reasoning with comprehensive research capabilities +Comprehensive API for You.com services: - **Agents API**: Execute queries using Express, Advanced, and Custom AI agents - **Search API**: Get search results from web and news sources - **Contents API**: Retrieve and process web page content @@ -90,7 +94,7 @@ It's also possible to write a standalone Python script without needing to set up ```python #!/usr/bin/env -S uv run --script # /// script -# requires-python = ">=3.9" +# requires-python = ">=3.10" # dependencies = [ # "youdotcom", # ] @@ -127,23 +131,17 @@ Generally, the SDK will work well with most IDEs out of the box. However, when u ```python # Synchronous Example import os -from youdotcom import You +from youdotcom import You, models with You( api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), ) as you: - res = you.agents.runs.create(request={ - "agent": "express", - "input": "What is the capital of France?", - "stream": False, - }) + res = you.research(input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", research_effort=models.ResearchEffort.LITE) - with res as event_stream: - for event in event_stream: - # handle event - print(event, flush=True) + # Handle response + print(res) ```
@@ -154,7 +152,7 @@ The same SDK client can also be used to make asynchronous requests by importing # Asynchronous Example import asyncio import os -from youdotcom import You +from youdotcom import You, models async def main(): @@ -162,16 +160,10 @@ async def main(): api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), ) as you: - res = await you.agents.runs.create_async(request={ - "agent": "express", - "input": "What is the capital of France?", - "stream": False, - }) + res = await you.research_async(input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", research_effort=models.ResearchEffort.LITE) - async with res as event_stream: - async for event in event_stream: - # handle event - print(event, flush=True) + # Handle response + print(res) asyncio.run(main()) ``` @@ -192,23 +184,17 @@ This SDK supports the following security scheme globally: To authenticate with the API the `api_key_auth` parameter must be set when initializing the SDK client instance. For example: ```python import os -from youdotcom import You +from youdotcom import You, models with You( api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), ) as you: - res = you.agents.runs.create(request={ - "agent": "express", - "input": "What is the capital of France?", - "stream": False, - }) + res = you.research(input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", research_effort=models.ResearchEffort.LITE) - with res as event_stream: - for event in event_stream: - # handle event - print(event, flush=True) + # Handle response + print(res) ``` @@ -219,6 +205,10 @@ with You(
Available methods +### [You SDK](docs/sdks/you/README.md) + +* [research](docs/sdks/you/README.md#research) - Returns comprehensive research-grade answers with multi-step reasoning + ### [Agents.Runs](docs/sdks/runs/README.md) * [create](docs/sdks/runs/README.md#create) - Run an Agent @@ -249,59 +239,71 @@ underlying connection when the context is exited. ```python import os from youdotcom import You +from youdotcom.models import ( + ExpressAgentRunsRequest, + WebSearchTool, + ResponseCreated, + ResponseStarting, + ResponseOutputItemAdded, + ResponseOutputContentFull, + ResponseOutputTextDelta, + ResponseOutputItemDone, + ResponseDone, +) +from youdotcom.utils import eventstreaming with You( api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), ) as you: -response = you.agents.runs.create(request=ExpressAgentRunsRequest( - input="Restaurants in San Francisco", - stream=True, - tools=[ - WebSearchTool() - ] -)) - -# Type narrow to ensure we have a streaming response -assert isinstance(response, eventstreaming.EventStream), "Expected streaming response" -with response as AgentRunsStreamingResponse: - # Iterate through the stream and handle each event type - # Each chunk is an AgentRunsStreamingResponse with a 'data' field - for chunk in response: - # The data field contains the actual event (discriminated by TYPE) - event_data = chunk.data - - # Use isinstance() to narrow the type and handle each event - if isinstance(event_data, ResponseCreated): - print(f"✨ Response created (seq: {event_data.seq_id})") - - elif isinstance(event_data, ResponseStarting): - print(f"🚀 Response starting (seq: {event_data.seq_id})") - - elif isinstance(event_data, ResponseOutputItemAdded): - print(f"➕ Output item added: {event_data.seq_id}") - - elif isinstance(event_data, ResponseOutputContentFull): - print("\n🔍 Web Search Results:") - if event_data.response.full: - for idx, result in enumerate(event_data.response.full, 1): - print(f" {idx}. {result.title} - {result.url}") - - elif isinstance(event_data, ResponseOutputTextDelta): - # Print the delta text as it streams in (without newline) - print(event_data.response.delta, end='', flush=True) - - elif isinstance(event_data, ResponseOutputItemDone): - print(f"\n✅ Output item done (index: {event_data.response.output_index})") - - elif isinstance(event_data, ResponseDone): - print("\n🎉 Response completed!") - print(f" Runtime: {event_data.response.run_time_ms} seconds") - print(f" Finished: {event_data.response.finished}") - - else: - print(f"⚠️ Unknown event type: {type(event_data).__name__}") + response = you.agents.runs.create(request=ExpressAgentRunsRequest( + input="Restaurants in San Francisco", + stream=True, + tools=[ + WebSearchTool() + ] + )) + + # Type narrow to ensure we have a streaming response + assert isinstance(response, eventstreaming.EventStream), "Expected streaming response" + with response as stream: + # Iterate through the stream and handle each event type + # Each chunk is an AgentRunsStreamingResponse with a 'data' field + for chunk in stream: + # The data field contains the actual event (discriminated by TYPE) + event_data = chunk.data + + # Use isinstance() to narrow the type and handle each event + if isinstance(event_data, ResponseCreated): + print(f"✨ Response created (seq: {event_data.seq_id})") + + elif isinstance(event_data, ResponseStarting): + print(f"🚀 Response starting (seq: {event_data.seq_id})") + + elif isinstance(event_data, ResponseOutputItemAdded): + print(f"➕ Output item added: {event_data.seq_id}") + + elif isinstance(event_data, ResponseOutputContentFull): + print("\n🔍 Web Search Results:") + if event_data.response.full: + for idx, result in enumerate(event_data.response.full, 1): + print(f" {idx}. {result.title} - {result.url}") + + elif isinstance(event_data, ResponseOutputTextDelta): + # Print the delta text as it streams in (without newline) + print(event_data.response.delta, end='', flush=True) + + elif isinstance(event_data, ResponseOutputItemDone): + print(f"\n✅ Output item done (index: {event_data.response.output_index})") + + elif isinstance(event_data, ResponseDone): + print("\n🎉 Response completed!") + print(f" Runtime: {event_data.response.run_time_ms} ms") + print(f" Finished: {event_data.response.finished}") + + else: + print(f"⚠️ Unknown event type: {type(event_data).__name__}") ``` [mdn-sse]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events @@ -317,7 +319,7 @@ Some of the endpoints in this SDK support retries. If you use the SDK without an To change the default retry strategy for a single API call, simply provide a `RetryConfig` object to the call: ```python import os -from youdotcom import You +from youdotcom import You, models from youdotcom.utils import BackoffStrategy, RetryConfig @@ -325,24 +327,21 @@ with You( api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), ) as you: - res = you.agents.runs.create(request={ - "agent": "express", - "input": "What is the capital of France?", - "stream": False, - }, - RetryConfig("backoff", BackoffStrategy(1, 50, 1.1, 100), False)) + res = you.research( + input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", + research_effort=models.ResearchEffort.LITE, + retries=RetryConfig("backoff", BackoffStrategy(1, 50, 1.1, 100), False), + ) - with res as event_stream: - for event in event_stream: - # handle event - print(event, flush=True) + # Handle response + print(res) ``` If you'd like to override the default retry strategy for all operations that support retries, you can use the `retry_config` optional parameter when initializing the SDK: ```python import os -from youdotcom import You +from youdotcom import You, models from youdotcom.utils import BackoffStrategy, RetryConfig @@ -351,16 +350,10 @@ with You( api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), ) as you: - res = you.agents.runs.create(request={ - "agent": "express", - "input": "What is the capital of France?", - "stream": False, - }) + res = you.research(input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", research_effort=models.ResearchEffort.LITE) - with res as event_stream: - for event in event_stream: - # handle event - print(event, flush=True) + # Handle response + print(res) ``` @@ -382,7 +375,7 @@ with You( ### Example ```python import os -from youdotcom import You, errors +from youdotcom import You, errors, models with You( @@ -391,16 +384,10 @@ with You( res = None try: - res = you.agents.runs.create(request={ - "agent": "express", - "input": "What is the capital of France?", - "stream": False, - }) + res = you.research(input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", research_effort=models.ResearchEffort.LITE) - with res as event_stream: - for event in event_stream: - # handle event - print(event, flush=True) + # Handle response + print(res) except errors.YouError as e: @@ -412,7 +399,7 @@ with You( print(e.raw_response) # Depending on the method different errors may be thrown - if isinstance(e, errors.AgentRuns400ResponseError): + if isinstance(e, errors.ResearchUnauthorizedError): print(e.data.detail) # Optional[str] ``` @@ -420,7 +407,7 @@ with You( **Primary error:** * [`YouError`](./src/youdotcom/errors/youerror.py): The base class for HTTP error responses. -
Less common errors (14) +
Less common errors (18)
@@ -431,15 +418,19 @@ with You( **Inherit from [`YouError`](./src/youdotcom/errors/youerror.py)**: -* [`AgentRuns400ResponseError`](./src/youdotcom/errors/agentruns400responseerror.py): The message returned by the error. Status code `400`. Applicable to 1 of 3 methods.* -* [`SearchUnauthorizedError`](./src/youdotcom/errors/searchunauthorizederror.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 3 methods.* -* [`ContentsUnauthorizedError`](./src/youdotcom/errors/contentsunauthorizederror.py): Unauthorized. Status code `401`. Applicable to 1 of 3 methods.* -* [`AgentRuns401ResponseError`](./src/youdotcom/errors/agentruns401responseerror.py): The message returned by the error. Status code `401`. Applicable to 1 of 3 methods.* -* [`SearchForbiddenError`](./src/youdotcom/errors/searchforbiddenerror.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 3 methods.* -* [`ContentsForbiddenError`](./src/youdotcom/errors/contentsforbiddenerror.py): Forbidden. Status code `403`. Applicable to 1 of 3 methods.* -* [`AgentRuns422ResponseError`](./src/youdotcom/errors/agentruns422responseerror.py): Unprocessable Entity - Invalid request data. Status code `422`. Applicable to 1 of 3 methods.* -* [`SearchInternalServerError`](./src/youdotcom/errors/searchinternalservererror.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 1 of 3 methods.* -* [`ContentsInternalServerError`](./src/youdotcom/errors/contentsinternalservererror.py): Internal Server Error. Status code `500`. Applicable to 1 of 3 methods.* +* [`AgentRuns400ResponseError`](./src/youdotcom/errors/agentruns400responseerror.py): The message returned by the error. Status code `400`. Applicable to 1 of 4 methods.* +* [`ResearchUnauthorizedError`](./src/youdotcom/errors/researchunauthorizederror.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 4 methods.* +* [`SearchUnauthorizedError`](./src/youdotcom/errors/searchunauthorizederror.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 4 methods.* +* [`ContentsUnauthorizedError`](./src/youdotcom/errors/contentsunauthorizederror.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 4 methods.* +* [`AgentRuns401ResponseError`](./src/youdotcom/errors/agentruns401responseerror.py): The message returned by the error. Status code `401`. Applicable to 1 of 4 methods.* +* [`ResearchForbiddenError`](./src/youdotcom/errors/researchforbiddenerror.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 4 methods.* +* [`SearchForbiddenError`](./src/youdotcom/errors/searchforbiddenerror.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 4 methods.* +* [`ContentsForbiddenError`](./src/youdotcom/errors/contentsforbiddenerror.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 4 methods.* +* [`UnprocessableEntityError`](./src/youdotcom/errors/unprocessableentityerror.py): Unprocessable Entity. Request validation failed. Status code `422`. Applicable to 1 of 4 methods.* +* [`AgentRuns422ResponseError`](./src/youdotcom/errors/agentruns422responseerror.py): Unprocessable Entity - Invalid request data. Status code `422`. Applicable to 1 of 4 methods.* +* [`ResearchInternalServerError`](./src/youdotcom/errors/researchinternalservererror.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 1 of 4 methods.* +* [`SearchInternalServerError`](./src/youdotcom/errors/searchinternalservererror.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 1 of 4 methods.* +* [`ContentsInternalServerError`](./src/youdotcom/errors/contentsinternalservererror.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 1 of 4 methods.* * [`ResponseValidationError`](./src/youdotcom/errors/responsevalidationerror.py): Type mismatch between the response data and the expected Pydantic model. Provides access to the Pydantic validation error via the `cause` attribute.
@@ -455,24 +446,18 @@ with You( The default server can be overridden globally by passing a URL to the `server_url: str` optional parameter when initializing the SDK client instance. For example: ```python import os -from youdotcom import You +from youdotcom import You, models with You( - server_url="https://ydc-index.io", + server_url="https://api.you.com", api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), ) as you: - res = you.agents.runs.create(request={ - "agent": "express", - "input": "What is the capital of France?", - "stream": False, - }) + res = you.research(input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", research_effort=models.ResearchEffort.LITE) - with res as event_stream: - for event in event_stream: - # handle event - print(event, flush=True) + # Handle response + print(res) ``` @@ -481,23 +466,17 @@ with You( The server URL can also be overridden on a per-operation basis, provided a server list was specified for the operation. For example: ```python import os -from youdotcom import You +from youdotcom import You, models with You( api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), ) as you: - res = you.agents.runs.create(request={ - "agent": "express", - "input": "What is the capital of France?", - "stream": False, - }, server_url="https://api.you.com") + res = you.search.unified(query="Your query", count=10, language=models.Language.EN, server_url="https://ydc-index.io") - with res as event_stream: - for event in event_stream: - # handle event - print(event, flush=True) + # Handle response + print(res) ``` diff --git a/USAGE.md b/USAGE.md index d2feb90..9f08d21 100644 --- a/USAGE.md +++ b/USAGE.md @@ -2,23 +2,17 @@ ```python # Synchronous Example import os -from youdotcom import You +from youdotcom import You, models with You( api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), ) as you: - res = you.agents.runs.create(request={ - "agent": "express", - "input": "What is the capital of France?", - "stream": False, - }) + res = you.research(input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", research_effort=models.ResearchEffort.LITE) - with res as event_stream: - for event in event_stream: - # handle event - print(event, flush=True) + # Handle response + print(res) ```
@@ -29,7 +23,7 @@ The same SDK client can also be used to make asynchronous requests by importing # Asynchronous Example import asyncio import os -from youdotcom import You +from youdotcom import You, models async def main(): @@ -37,16 +31,10 @@ async def main(): api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), ) as you: - res = await you.agents.runs.create_async(request={ - "agent": "express", - "input": "What is the capital of France?", - "stream": False, - }) + res = await you.research_async(input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", research_effort=models.ResearchEffort.LITE) - async with res as event_stream: - async for event in event_stream: - # handle event - print(event, flush=True) + # Handle response + print(res) asyncio.run(main()) ``` diff --git a/docs/errors/contentsforbiddenerror.md b/docs/errors/contentsforbiddenerror.md index 2c39593..81d690f 100644 --- a/docs/errors/contentsforbiddenerror.md +++ b/docs/errors/contentsforbiddenerror.md @@ -1,6 +1,6 @@ # ContentsForbiddenError -Forbidden +Forbidden. API key lacks scope for this path. ## Fields diff --git a/docs/errors/contentsinternalservererror.md b/docs/errors/contentsinternalservererror.md index e6b4b20..8dabd74 100644 --- a/docs/errors/contentsinternalservererror.md +++ b/docs/errors/contentsinternalservererror.md @@ -1,6 +1,6 @@ # ContentsInternalServerError -Internal Server Error +Internal Server Error during authentication/authorization middleware. ## Fields diff --git a/docs/errors/contentsunauthorizederror.md b/docs/errors/contentsunauthorizederror.md index f45e9e4..486b384 100644 --- a/docs/errors/contentsunauthorizederror.md +++ b/docs/errors/contentsunauthorizederror.md @@ -1,6 +1,6 @@ # ContentsUnauthorizedError -Unauthorized +Unauthorized. Problems with API key. ## Fields diff --git a/docs/errors/researchforbiddenerror.md b/docs/errors/researchforbiddenerror.md new file mode 100644 index 0000000..9f18fd0 --- /dev/null +++ b/docs/errors/researchforbiddenerror.md @@ -0,0 +1,10 @@ +# ResearchForbiddenError + +Forbidden. API key lacks scope for this path. + + +## Fields + +| Field | Type | Required | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | +| `detail` | *Optional[str]* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/errors/researchinternalservererror.md b/docs/errors/researchinternalservererror.md new file mode 100644 index 0000000..5576cc8 --- /dev/null +++ b/docs/errors/researchinternalservererror.md @@ -0,0 +1,10 @@ +# ResearchInternalServerError + +Internal Server Error during authentication/authorization middleware. + + +## Fields + +| Field | Type | Required | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | +| `detail` | *Optional[str]* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/errors/researchunauthorizederror.md b/docs/errors/researchunauthorizederror.md new file mode 100644 index 0000000..9516b17 --- /dev/null +++ b/docs/errors/researchunauthorizederror.md @@ -0,0 +1,10 @@ +# ResearchUnauthorizedError + +Unauthorized. Problems with API key. + + +## Fields + +| Field | Type | Required | Description | +| --------------------- | --------------------- | --------------------- | --------------------- | +| `detail` | *Optional[str]* | :heavy_minus_sign: | Error detail message. | \ No newline at end of file diff --git a/docs/errors/unprocessableentityerror.md b/docs/errors/unprocessableentityerror.md new file mode 100644 index 0000000..5abff96 --- /dev/null +++ b/docs/errors/unprocessableentityerror.md @@ -0,0 +1,10 @@ +# UnprocessableEntityError + +Unprocessable Entity. Request validation failed. + + +## Fields + +| Field | Type | Required | Description | +| ---------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | +| `detail` | List[[models.ResearchDetail](../models/researchdetail.md)] | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/models/agentrunsbatchresponse.md b/docs/models/agentrunsbatchresponse.md index 3fe03a1..5922b79 100644 --- a/docs/models/agentrunsbatchresponse.md +++ b/docs/models/agentrunsbatchresponse.md @@ -7,5 +7,5 @@ | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | `agent` | *str* | :heavy_check_mark: | The id of the agent populated in the request. | express | | `mode` | *Optional[str]* | :heavy_minus_sign: | The mode of the agent | express | -| `input` | List[[models.Input](../models/input.md)] | :heavy_check_mark: | The users access role and question you asked the agent | | +| `input` | List[[models.Input1](../models/input1.md)] | :heavy_check_mark: | The users access role and question you asked the agent | | | `output` | List[[models.AgentRunsResponseOutput](../models/agentrunsresponseoutput.md)] | :heavy_check_mark: | Array of response outputs from the agent | | \ No newline at end of file diff --git a/docs/models/contentsrequest.md b/docs/models/contentsrequest.md index 7cae5ed..fec30b0 100644 --- a/docs/models/contentsrequest.md +++ b/docs/models/contentsrequest.md @@ -3,8 +3,8 @@ ## Fields -| Field | Type | Required | Description | Example | -| --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -| `urls` | List[*str*] | :heavy_minus_sign: | Array of URLs to fetch the contents from. | | -| `formats` | List[[models.ContentsFormats](../models/contentsformats.md)] | :heavy_minus_sign: | The formats of the content to be returned. Can include 'html', 'markdown', and/or 'metadata'. | [
"html",
"markdown"
] | -| `crawl_timeout` | *Optional[float]* | :heavy_minus_sign: | The timeout in seconds for crawling each URL. Must be between 1 and 60 seconds. | 10 | \ No newline at end of file +| Field | Type | Required | Description | Example | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `urls` | List[*str*] | :heavy_minus_sign: | Array of URLs to fetch the contents from. | | +| `formats` | List[[models.ContentsFormats](../models/contentsformats.md)] | :heavy_minus_sign: | Array of content formats to return. All included formats are returned in the response. Include "metadata" to get JSON-LD and OpenGraph information, if available. | [
"html",
"markdown"
] | +| `crawl_timeout` | *Optional[int]* | :heavy_minus_sign: | Maximum time in seconds to wait for page content. Must be between 1 and 60 seconds. Default is 10 seconds. | 10 | \ No newline at end of file diff --git a/docs/models/contenttype.md b/docs/models/contenttype.md new file mode 100644 index 0000000..b043bcf --- /dev/null +++ b/docs/models/contenttype.md @@ -0,0 +1,10 @@ +# ContentType + +The format of the content field. + + +## Values + +| Name | Value | +| ------ | ------ | +| `TEXT` | text | \ No newline at end of file diff --git a/docs/models/input.md b/docs/models/input1.md similarity index 99% rename from docs/models/input.md rename to docs/models/input1.md index 54bf616..8b6df1b 100644 --- a/docs/models/input.md +++ b/docs/models/input1.md @@ -1,4 +1,4 @@ -# Input +# Input1 ## Fields diff --git a/docs/models/input2.md b/docs/models/input2.md new file mode 100644 index 0000000..fefc4e5 --- /dev/null +++ b/docs/models/input2.md @@ -0,0 +1,19 @@ +# Input2 + +The input value that caused the error. + + +## Supported Types + +### `str` + +```python +value: str = /* values here */ +``` + +### `models.ResearchInput` + +```python +value: models.ResearchInput = /* values here */ +``` + diff --git a/docs/models/output.md b/docs/models/output.md new file mode 100644 index 0000000..d7c2771 --- /dev/null +++ b/docs/models/output.md @@ -0,0 +1,12 @@ +# Output + +The research output containing the answer and sources. + + +## Fields + +| Field | Type | Required | Description | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `content` | *str* | :heavy_check_mark: | The comprehensive response with inline citations. The content is formatted in Markdown and includes numbered citations that reference the items in the sources array. | +| `content_type` | [models.ContentType](../models/contenttype.md) | :heavy_check_mark: | The format of the content field. | +| `sources` | List[[models.Source](../models/source.md)] | :heavy_check_mark: | A list of web sources used to generate the answer. | \ No newline at end of file diff --git a/docs/models/researchdetail.md b/docs/models/researchdetail.md new file mode 100644 index 0000000..e635556 --- /dev/null +++ b/docs/models/researchdetail.md @@ -0,0 +1,12 @@ +# ResearchDetail + + +## Fields + +| Field | Type | Required | Description | Example | +| ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `type` | *str* | :heavy_check_mark: | The validation error type. | missing | +| `loc` | List[[models.ResearchLoc](../models/researchloc.md)] | :heavy_check_mark: | The location of the error as a path of segments (strings for field names, integers for byte offsets). | [
"body",
"input"
] | +| `msg` | *str* | :heavy_check_mark: | A human-readable description of the error. | Field required | +| `input` | [models.Input2](../models/input2.md) | :heavy_check_mark: | The input value that caused the error. | | +| `ctx` | Dict[str, *Any*] | :heavy_minus_sign: | Additional context about the error. | | \ No newline at end of file diff --git a/docs/models/researcheffort.md b/docs/models/researcheffort.md new file mode 100644 index 0000000..e54e207 --- /dev/null +++ b/docs/models/researcheffort.md @@ -0,0 +1,19 @@ +# ResearchEffort + +Controls how much time and effort the Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. + +Available levels: +- `lite`: Returns answers quickly. Good for straightforward questions that just need a fast, reliable answer. +- `standard`: The default. Balances speed and depth, a good fit for most questions. +- `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed. +- `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. + + +## Values + +| Name | Value | +| ------------ | ------------ | +| `LITE` | lite | +| `STANDARD` | standard | +| `DEEP` | deep | +| `EXHAUSTIVE` | exhaustive | \ No newline at end of file diff --git a/docs/models/researchinput.md b/docs/models/researchinput.md new file mode 100644 index 0000000..440a444 --- /dev/null +++ b/docs/models/researchinput.md @@ -0,0 +1,7 @@ +# ResearchInput + + +## Fields + +| Field | Type | Required | Description | +| ----------- | ----------- | ----------- | ----------- | \ No newline at end of file diff --git a/docs/models/researchloc.md b/docs/models/researchloc.md new file mode 100644 index 0000000..54741e7 --- /dev/null +++ b/docs/models/researchloc.md @@ -0,0 +1,17 @@ +# ResearchLoc + + +## Supported Types + +### `str` + +```python +value: str = /* values here */ +``` + +### `int` + +```python +value: int = /* values here */ +``` + diff --git a/docs/models/researchrequest.md b/docs/models/researchrequest.md new file mode 100644 index 0000000..be8ad6e --- /dev/null +++ b/docs/models/researchrequest.md @@ -0,0 +1,9 @@ +# ResearchRequest + + +## Fields + +| Field | Type | Required | Description | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `input` | *str* | :heavy_check_mark: | The research question or complex query requiring in-depth investigation and multi-step reasoning.

Note: The maximum length of the input is 40,000 characters. | +| `research_effort` | [Optional[models.ResearchEffort]](../models/researcheffort.md) | :heavy_minus_sign: | Controls how much time and effort the Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time.

Available levels:
- `lite`: Returns answers quickly. Good for straightforward questions that just need a fast, reliable answer.
- `standard`: The default. Balances speed and depth, a good fit for most questions.
- `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed.
- `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. | \ No newline at end of file diff --git a/docs/models/researchresponse.md b/docs/models/researchresponse.md new file mode 100644 index 0000000..58a1bc3 --- /dev/null +++ b/docs/models/researchresponse.md @@ -0,0 +1,10 @@ +# ResearchResponse + +A JSON object containing a comprehensive answer with citations and supporting search results + + +## Fields + +| Field | Type | Required | Description | +| ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ | +| `output` | [models.Output](../models/output.md) | :heavy_check_mark: | The research output containing the answer and sources. | \ No newline at end of file diff --git a/docs/models/searchfreshness.md b/docs/models/searchfreshness.md index 1cbf6d3..e947818 100644 --- a/docs/models/searchfreshness.md +++ b/docs/models/searchfreshness.md @@ -1,6 +1,8 @@ # SearchFreshness -Specifies the freshness of the results to return. +Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. + +When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. ## Supported Types diff --git a/docs/models/searchrequest.md b/docs/models/searchrequest.md index 9f3b329..1adb45e 100644 --- a/docs/models/searchrequest.md +++ b/docs/models/searchrequest.md @@ -3,14 +3,14 @@ ## Fields -| Field | Type | Required | Description | Example | -| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `query` | *str* | :heavy_check_mark: | The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. | Your query | -| `count` | *Optional[int]* | :heavy_minus_sign: | Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). | | -| `freshness` | [Optional[models.SearchFreshness]](../models/searchfreshness.md) | :heavy_minus_sign: | Specifies the freshness of the results to return. | | -| `offset` | *Optional[int]* | :heavy_minus_sign: | Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. | | -| `country` | [Optional[models.SearchCountry]](../models/searchcountry.md) | :heavy_minus_sign: | The country code that determines the geographical focus of the web results. | | -| `language` | [Optional[models.Language]](../models/language.md) | :heavy_minus_sign: | The language of the web results that will be returned (BCP 47 format). | | -| `safesearch` | [Optional[models.SearchSafesearch]](../models/searchsafesearch.md) | :heavy_minus_sign: | Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not. | | -| `livecrawl` | [Optional[models.SearchLivecrawl]](../models/searchlivecrawl.md) | :heavy_minus_sign: | Indicates which section(s) of search results to livecrawl and return full page content. | | -| `livecrawl_formats` | [Optional[models.SearchLivecrawlFormats]](../models/searchlivecrawlformats.md) | :heavy_minus_sign: | Indicates the format of the livecrawled content. | | \ No newline at end of file +| Field | Type | Required | Description | Example | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `query` | *str* | :heavy_check_mark: | The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. | Your query | +| `count` | *Optional[int]* | :heavy_minus_sign: | Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). | | +| `freshness` | [Optional[models.SearchFreshness]](../models/searchfreshness.md) | :heavy_minus_sign: | Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`.

When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. | | +| `offset` | *Optional[int]* | :heavy_minus_sign: | Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. | | +| `country` | [Optional[models.SearchCountry]](../models/searchcountry.md) | :heavy_minus_sign: | The country code that determines the geographical focus of the web results. | | +| `language` | [Optional[models.Language]](../models/language.md) | :heavy_minus_sign: | The language of the web results that will be returned (BCP 47 format). | | +| `safesearch` | [Optional[models.SearchSafesearch]](../models/searchsafesearch.md) | :heavy_minus_sign: | Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not. | | +| `livecrawl` | [Optional[models.SearchLivecrawl]](../models/searchlivecrawl.md) | :heavy_minus_sign: | Indicates which section(s) of search results to livecrawl and return full page content. | | +| `livecrawl_formats` | [Optional[models.SearchLivecrawlFormats]](../models/searchlivecrawlformats.md) | :heavy_minus_sign: | Indicates the format of the livecrawled content. | | \ No newline at end of file diff --git a/docs/models/source.md b/docs/models/source.md new file mode 100644 index 0000000..174794d --- /dev/null +++ b/docs/models/source.md @@ -0,0 +1,10 @@ +# Source + + +## Fields + +| Field | Type | Required | Description | +| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| `url` | *str* | :heavy_check_mark: | The URL of the source webpage. | +| `title` | *Optional[str]* | :heavy_minus_sign: | The title of the source webpage. | +| `snippets` | List[*str*] | :heavy_minus_sign: | Relevant excerpts from the source page that were used in generating the answer. | \ No newline at end of file diff --git a/docs/sdks/contentssdk/README.md b/docs/sdks/contentssdk/README.md index 2a10aa7..e4ce470 100644 --- a/docs/sdks/contentssdk/README.md +++ b/docs/sdks/contentssdk/README.md @@ -8,11 +8,126 @@ ## generate -Returns the content of the web pages +Returns the HTML or Markdown of a target webpage. -### Example Usage +### Example Usage: authFailure - + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.contents.generate(urls=[ + "https://www.you.com", + ], formats=[ + models.ContentsFormats.HTML, + models.ContentsFormats.MARKDOWN, + ], crawl_timeout=10) + + # Handle response + print(res) + +``` +### Example Usage: authorizationFailure + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.contents.generate(urls=[ + "https://www.you.com", + ], formats=[ + models.ContentsFormats.HTML, + models.ContentsFormats.MARKDOWN, + ], crawl_timeout=10) + + # Handle response + print(res) + +``` +### Example Usage: invalidOrExpired + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.contents.generate(urls=[ + "https://www.you.com", + ], formats=[ + models.ContentsFormats.HTML, + models.ContentsFormats.MARKDOWN, + ], crawl_timeout=10) + + # Handle response + print(res) + +``` +### Example Usage: missingApiKey + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.contents.generate(urls=[ + "https://www.you.com", + ], formats=[ + models.ContentsFormats.HTML, + models.ContentsFormats.MARKDOWN, + ], crawl_timeout=10) + + # Handle response + print(res) + +``` +### Example Usage: missingScopes + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.contents.generate(urls=[ + "https://www.you.com", + ], formats=[ + models.ContentsFormats.HTML, + models.ContentsFormats.MARKDOWN, + ], crawl_timeout=10) + + # Handle response + print(res) + +``` +### Example Usage: otherAuthParsing + + ```python import os from youdotcom import You, models @@ -36,12 +151,13 @@ with You( ### Parameters -| Parameter | Type | Required | Description | Example | -| --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -| `urls` | List[*str*] | :heavy_minus_sign: | Array of URLs to fetch the contents from. | | -| `formats` | List[[models.ContentsFormats](../../models/contentsformats.md)] | :heavy_minus_sign: | The formats of the content to be returned. Can include 'html', 'markdown', and/or 'metadata'. | [
"html",
"markdown"
] | -| `crawl_timeout` | *Optional[float]* | :heavy_minus_sign: | The timeout in seconds for crawling each URL. Must be between 1 and 60 seconds. | 10 | -| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | | +| Parameter | Type | Required | Description | Example | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `urls` | List[*str*] | :heavy_minus_sign: | Array of URLs to fetch the contents from. | | +| `formats` | List[[models.ContentsFormats](../../models/contentsformats.md)] | :heavy_minus_sign: | Array of content formats to return. All included formats are returned in the response. Include "metadata" to get JSON-LD and OpenGraph information, if available. | [
"html",
"markdown"
] | +| `crawl_timeout` | *Optional[int]* | :heavy_minus_sign: | Maximum time in seconds to wait for page content. Must be between 1 and 60 seconds. Default is 10 seconds. | 10 | +| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | | +| `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | http://localhost:8080 | ### Response diff --git a/docs/sdks/runs/README.md b/docs/sdks/runs/README.md index 759645a..c8d570f 100644 --- a/docs/sdks/runs/README.md +++ b/docs/sdks/runs/README.md @@ -17,9 +17,40 @@ Execute queries using You.com's AI agents. This endpoint supports three agent ty The response format depends on the `stream` parameter - either a complete JSON payload or Server-Sent Events (SSE). -### Example Usage +### Example Usage: advanced_batch - + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.agents.runs.create(request={ + "agent": "advanced", + "input": "You are a biologist studying the impacts of microplastics. Explain what microplastics are to a group of engineers, explain the impacts of microplastics on the body, and what the common sources and dosages of microplastics are. Highlight what a safe dosage might be and how to achieve it", + "stream": False, + "tools": [ + { + "type": "research", + "search_effort": models.SearchEffort.AUTO, + "report_verbosity": models.ReportVerbosity.MEDIUM, + }, + ], + }) + + with res as event_stream: + for event in event_stream: + # handle event + print(event, flush=True) + +``` +### Example Usage: advanced_stream + + ```python import os from youdotcom import You @@ -31,6 +62,35 @@ with You( res = you.agents.runs.create(request={ "agent": "express", + "input": "Analyze the economic impact of renewable energy adoption", + "stream": True, + "tools": [ + { + "type": "web_search", + }, + ], + }) + + with res as event_stream: + for event in event_stream: + # handle event + print(event, flush=True) + +``` +### Example Usage: custom_batch + + +```python +import os +from youdotcom import You + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.agents.runs.create(request={ + "agent": "63773261-b4de-4d8f-9dfd-cff206a5cb51", "input": "What is the capital of France?", "stream": False, }) @@ -40,6 +100,83 @@ with You( # handle event print(event, flush=True) +``` +### Example Usage: custom_stream + + +```python +import os +from youdotcom import You + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.agents.runs.create(request={ + "agent": "63773261-b4de-4d8f-9dfd-cff206a5cb51", + "input": "Tell me about the history of Paris", + "stream": True, + }) + + with res as event_stream: + for event in event_stream: + # handle event + print(event, flush=True) + +``` +### Example Usage: express_batch + + +```python +import os +from youdotcom import You + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.agents.runs.create(request={ + "agent": "express", + "input": "What is the capital of France?", + "stream": False, + }) + + with res as event_stream: + for event in event_stream: + # handle event + print(event, flush=True) + +``` +### Example Usage: express_stream + + +```python +import os +from youdotcom import You + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.agents.runs.create(request={ + "agent": "express", + "input": "What are some great recipes I can make in under half an hour", + "stream": True, + "tools": [ + { + "type": "web_search", + }, + ], + }) + + with res as event_stream: + for event in event_stream: + # handle event + print(event, flush=True) + ``` ### Parameters @@ -48,7 +185,6 @@ with You( | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | | `request` | [models.AgentsRunsRequest](../../models/agentsrunsrequest.md) | :heavy_check_mark: | The request object to use for the request. | | `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | -| `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | ### Response diff --git a/docs/sdks/search/README.md b/docs/sdks/search/README.md index 1a33aef..72bfdc7 100644 --- a/docs/sdks/search/README.md +++ b/docs/sdks/search/README.md @@ -8,7 +8,7 @@ ## unified -Returns a list of unified search results from web and news sources +This endpoint is designed to return LLM-ready web results based on a user's query. Based on a classification mechanism, it can return web results and news associated with your query. If you need to feed an LLM with the results of a query that sounds like `What are the latest geopolitical updates from India`, then this endpoint is the right one for you. ### Example Usage @@ -22,7 +22,7 @@ with You( api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), ) as you: - res = you.search.unified(query="Your query", language=models.Language.EN) + res = you.search.unified(query="Your query", count=10, language=models.Language.EN) # Handle response print(res) @@ -31,19 +31,19 @@ with You( ### Parameters -| Parameter | Type | Required | Description | Example | -| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `query` | *str* | :heavy_check_mark: | The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. | Your query | -| `count` | *Optional[int]* | :heavy_minus_sign: | Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). | | -| `freshness` | [Optional[models.SearchFreshness]](../../models/searchfreshness.md) | :heavy_minus_sign: | Specifies the freshness of the results to return. | | -| `offset` | *Optional[int]* | :heavy_minus_sign: | Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. | | -| `country` | [Optional[models.SearchCountry]](../../models/searchcountry.md) | :heavy_minus_sign: | The country code that determines the geographical focus of the web results. | | -| `language` | [Optional[models.Language]](../../models/language.md) | :heavy_minus_sign: | The language of the web results that will be returned (BCP 47 format). | | -| `safesearch` | [Optional[models.SearchSafesearch]](../../models/searchsafesearch.md) | :heavy_minus_sign: | Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not. | | -| `livecrawl` | [Optional[models.SearchLivecrawl]](../../models/searchlivecrawl.md) | :heavy_minus_sign: | Indicates which section(s) of search results to livecrawl and return full page content. | | -| `livecrawl_formats` | [Optional[models.SearchLivecrawlFormats]](../../models/searchlivecrawlformats.md) | :heavy_minus_sign: | Indicates the format of the livecrawled content. | | -| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | | -| `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | http://localhost:8080 | +| Parameter | Type | Required | Description | Example | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `query` | *str* | :heavy_check_mark: | The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. | Your query | +| `count` | *Optional[int]* | :heavy_minus_sign: | Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). | | +| `freshness` | [Optional[models.SearchFreshness]](../../models/searchfreshness.md) | :heavy_minus_sign: | Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`.

When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. | | +| `offset` | *Optional[int]* | :heavy_minus_sign: | Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. | | +| `country` | [Optional[models.SearchCountry]](../../models/searchcountry.md) | :heavy_minus_sign: | The country code that determines the geographical focus of the web results. | | +| `language` | [Optional[models.Language]](../../models/language.md) | :heavy_minus_sign: | The language of the web results that will be returned (BCP 47 format). | | +| `safesearch` | [Optional[models.SearchSafesearch]](../../models/searchsafesearch.md) | :heavy_minus_sign: | Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not. | | +| `livecrawl` | [Optional[models.SearchLivecrawl]](../../models/searchlivecrawl.md) | :heavy_minus_sign: | Indicates which section(s) of search results to livecrawl and return full page content. | | +| `livecrawl_formats` | [Optional[models.SearchLivecrawlFormats]](../../models/searchlivecrawlformats.md) | :heavy_minus_sign: | Indicates the format of the livecrawled content. | | +| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | | +| `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | http://localhost:8080 | ### Response diff --git a/docs/sdks/you/README.md b/docs/sdks/you/README.md new file mode 100644 index 0000000..6be1470 --- /dev/null +++ b/docs/sdks/you/README.md @@ -0,0 +1,223 @@ +# You SDK + +## Overview + +You.com API: Unified API for Express, Advanced, and Custom Agents from You.com +Get the best search results from web and news sources +Returns the HTML or Markdown of a target webpage +Multi-step reasoning with comprehensive research capabilities +Comprehensive API for You.com services: +- **Agents API**: Execute queries using Express, Advanced, and Custom AI agents +- **Search API**: Get search results from web and news sources +- **Contents API**: Retrieve and process web page content + +### Available Operations + +* [research](#research) - Returns comprehensive research-grade answers with multi-step reasoning + +## research + +Research goes beyond a single web search. In response to your question, it runs multiple searches, reads through the sources, and synthesizes everything into a thorough, well-cited answer. Use it when a question is too complex for a simple lookup, and when you need a response you can actually trust and verify. + +### Example Usage: authFailure + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + + # Handle response + print(res) + +``` +### Example Usage: authorizationFailure + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + + # Handle response + print(res) + +``` +### Example Usage: invalidEnum + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + + # Handle response + print(res) + +``` +### Example Usage: invalidJson + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + + # Handle response + print(res) + +``` +### Example Usage: invalidOrExpired + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + + # Handle response + print(res) + +``` +### Example Usage: missingApiKey + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + + # Handle response + print(res) + +``` +### Example Usage: missingField + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + + # Handle response + print(res) + +``` +### Example Usage: missingScopes + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + + # Handle response + print(res) + +``` +### Example Usage: otherAuthParsing + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + + # Handle response + print(res) + +``` +### Example Usage: stringTooLong + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), +) as you: + + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + + # Handle response + print(res) + +``` + +### Parameters + +| Parameter | Type | Required | Description | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `input` | *str* | :heavy_check_mark: | The research question or complex query requiring in-depth investigation and multi-step reasoning.

Note: The maximum length of the input is 40,000 characters. | +| `research_effort` | [Optional[models.ResearchEffort]](../../models/researcheffort.md) | :heavy_minus_sign: | Controls how much time and effort the Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time.

Available levels:
- `lite`: Returns answers quickly. Good for straightforward questions that just need a fast, reliable answer.
- `standard`: The default. Balances speed and depth, a good fit for most questions.
- `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed.
- `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. | +| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | + +### Response + +**[models.ResearchResponse](../../models/researchresponse.md)** + +### Errors + +| Error Type | Status Code | Content Type | +| ---------------------------------- | ---------------------------------- | ---------------------------------- | +| errors.ResearchUnauthorizedError | 401 | application/json | +| errors.ResearchForbiddenError | 403 | application/json | +| errors.UnprocessableEntityError | 422 | application/json | +| errors.ResearchInternalServerError | 500 | application/json | +| errors.YouDefaultError | 4XX, 5XX | \*/\* | \ No newline at end of file diff --git a/examples/api-example-calls.py b/examples/api-example-calls.py index e407b37..e5e0807 100755 --- a/examples/api-example-calls.py +++ b/examples/api-example-calls.py @@ -39,7 +39,9 @@ AgentRunsBatchResponse, AgentRunsStreamingResponse, ContentsFormats, - WebSearchTool + WebSearchTool, + ResearchEffort, + ResearchResponse, ) from youdotcom.utils import eventstreaming @@ -94,10 +96,10 @@ def express_streaming_request(): # Type narrow to ensure we have a streaming response assert isinstance(response, eventstreaming.EventStream), "Expected streaming response" - with response as AgentRunsStreamingResponse: + with response as stream: # Iterate through the stream and handle each event type # Each chunk is an AgentRunsStreamingResponse with a 'data' field - for chunk in response: + for chunk in stream: # The data field contains the actual event (discriminated by TYPE) event_data = chunk.data @@ -267,6 +269,29 @@ def content_request(): print() +def research_request(): + """ + Research API endpoint for comprehensive, multi-step research answers + """ + print("\n🚀 Running Research Request...\n") + + assert you is not None, "SDK client not initialized" + + res = you.research( + input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", + research_effort=ResearchEffort.STANDARD, + ) + + assert isinstance(res, ResearchResponse) + print("Research Answer:") + print(res.output.content[:500] + "..." if len(res.output.content) > 500 else res.output.content) + + if res.output.sources: + print(f"\nSources ({len(res.output.sources)}):") + for source in res.output.sources: + print(f" - {source.title or 'Untitled'}: {source.url}") + + # Available functions menu FUNCTIONS = [ {"name": "Express Batch Request", "fn": express_batch_request}, @@ -275,6 +300,7 @@ def content_request(): {"name": "Custom Batch Request", "fn": custom_batch_request}, {"name": "Search Request", "fn": search_request}, {"name": "Content Request", "fn": content_request}, + {"name": "Research Request", "fn": research_request}, ] diff --git a/pylintrc b/pylintrc index 000d993..f456032 100644 --- a/pylintrc +++ b/pylintrc @@ -89,7 +89,7 @@ persistent=yes # Minimum Python version to use for version dependent checks. Will default to # the version used to run pylint. -py-version=3.9 +py-version=3.10 # Discover python modules and packages in the file system subtree. recursive=no @@ -458,7 +458,8 @@ disable=raw-checker-failed, consider-using-with, wildcard-import, unused-wildcard-import, - too-many-return-statements + too-many-return-statements, + redefined-builtin # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/pyproject.toml b/pyproject.toml index ebf7dd2..f0e24df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [project] name = "youdotcom" -version = "2.2.0" +version = "2.3.0" description = "The official You.com Python SDK." authors = [{ name = "You.com" },] readme = "README.md" -requires-python = ">=3.9.2" +requires-python = ">=3.10" dependencies = [ "httpcore >=1.0.9", "httpx >=0.28.1", @@ -17,6 +17,8 @@ dev = [ "mypy ==1.15.0", "pylint ==3.2.3", "pyright ==1.1.398", + "pytest >=8.0.0", + "pytest-asyncio >=0.24.0", ] [tool.setuptools.packages.find] diff --git a/src/youdotcom/_version.py b/src/youdotcom/_version.py index 5d708ac..c998bfd 100644 --- a/src/youdotcom/_version.py +++ b/src/youdotcom/_version.py @@ -3,10 +3,10 @@ import importlib.metadata __title__: str = "youdotcom" -__version__: str = "2.2.0" +__version__: str = "2.3.0" __openapi_doc_version__: str = "1.0.0" -__gen_version__: str = "2.801.2" -__user_agent__: str = "speakeasy-sdk/python 2.2.0 2.801.2 1.0.0 youdotcom" +__gen_version__: str = "2.845.12" +__user_agent__: str = "speakeasy-sdk/python 2.3.0 2.845.12 1.0.0 youdotcom" try: if __package__ is not None: diff --git a/src/youdotcom/contents_sdk.py b/src/youdotcom/contents_sdk.py index 1f0e0eb..a21a07c 100644 --- a/src/youdotcom/contents_sdk.py +++ b/src/youdotcom/contents_sdk.py @@ -15,7 +15,7 @@ def generate( *, urls: Optional[List[str]] = None, formats: Optional[List[models.ContentsFormats]] = None, - crawl_timeout: Optional[float] = None, + crawl_timeout: Optional[int] = 10, retries: OptionalNullable[utils.RetryConfig] = UNSET, server_url: Optional[str] = None, timeout_ms: Optional[int] = None, @@ -23,9 +23,11 @@ def generate( ) -> List[models.ContentsResponse]: r"""Returns the content of the web pages + Returns the HTML or Markdown of a target webpage. + :param urls: Array of URLs to fetch the contents from. - :param formats: The formats of the content to be returned. Can include 'html', 'markdown', and/or 'metadata'. - :param crawl_timeout: The timeout in seconds for crawling each URL. Must be between 1 and 60 seconds. + :param formats: Array of content formats to return. All included formats are returned in the response. Include \"metadata\" to get JSON-LD and OpenGraph information, if available. + :param crawl_timeout: Maximum time in seconds to wait for page content. Must be between 1 and 60 seconds. Default is 10 seconds. :param retries: Override the default retry configuration for this method :param server_url: Override the default server URL for this method :param timeout_ms: Override the default request timeout configuration for this method in milliseconds @@ -39,7 +41,7 @@ def generate( if server_url is not None: base_url = server_url else: - base_url = self._get_url(base_url, url_variables) + base_url = models.CONTENTS_OP_SERVERS[0] request = models.ContentsRequest( urls=urls, @@ -122,7 +124,7 @@ async def generate_async( *, urls: Optional[List[str]] = None, formats: Optional[List[models.ContentsFormats]] = None, - crawl_timeout: Optional[float] = None, + crawl_timeout: Optional[int] = 10, retries: OptionalNullable[utils.RetryConfig] = UNSET, server_url: Optional[str] = None, timeout_ms: Optional[int] = None, @@ -130,9 +132,11 @@ async def generate_async( ) -> List[models.ContentsResponse]: r"""Returns the content of the web pages + Returns the HTML or Markdown of a target webpage. + :param urls: Array of URLs to fetch the contents from. - :param formats: The formats of the content to be returned. Can include 'html', 'markdown', and/or 'metadata'. - :param crawl_timeout: The timeout in seconds for crawling each URL. Must be between 1 and 60 seconds. + :param formats: Array of content formats to return. All included formats are returned in the response. Include \"metadata\" to get JSON-LD and OpenGraph information, if available. + :param crawl_timeout: Maximum time in seconds to wait for page content. Must be between 1 and 60 seconds. Default is 10 seconds. :param retries: Override the default retry configuration for this method :param server_url: Override the default server URL for this method :param timeout_ms: Override the default request timeout configuration for this method in milliseconds @@ -146,7 +150,7 @@ async def generate_async( if server_url is not None: base_url = server_url else: - base_url = self._get_url(base_url, url_variables) + base_url = models.CONTENTS_OP_SERVERS[0] request = models.ContentsRequest( urls=urls, diff --git a/src/youdotcom/errors/__init__.py b/src/youdotcom/errors/__init__.py index 8e96187..2193daa 100644 --- a/src/youdotcom/errors/__init__.py +++ b/src/youdotcom/errors/__init__.py @@ -1,10 +1,9 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" from .youerror import YouError -from typing import TYPE_CHECKING -from importlib import import_module -import builtins -import sys +from typing import Any, TYPE_CHECKING + +from youdotcom.utils.dynamic_imports import lazy_getattr, lazy_dir if TYPE_CHECKING: from .agentruns400response_error import ( @@ -28,6 +27,16 @@ ContentsUnauthorizedErrorData, ) from .no_response_error import NoResponseError + from .researchop import ( + ResearchForbiddenError, + ResearchForbiddenErrorData, + ResearchInternalServerError, + ResearchInternalServerErrorData, + ResearchUnauthorizedError, + ResearchUnauthorizedErrorData, + UnprocessableEntityError, + UnprocessableEntityErrorData, + ) from .responsevalidationerror import ResponseValidationError from .searchop import ( SearchForbiddenError, @@ -53,6 +62,12 @@ "ContentsUnauthorizedError", "ContentsUnauthorizedErrorData", "NoResponseError", + "ResearchForbiddenError", + "ResearchForbiddenErrorData", + "ResearchInternalServerError", + "ResearchInternalServerErrorData", + "ResearchUnauthorizedError", + "ResearchUnauthorizedErrorData", "ResponseValidationError", "SearchForbiddenError", "SearchForbiddenErrorData", @@ -60,6 +75,8 @@ "SearchInternalServerErrorData", "SearchUnauthorizedError", "SearchUnauthorizedErrorData", + "UnprocessableEntityError", + "UnprocessableEntityErrorData", "YouDefaultError", "YouError", ] @@ -78,6 +95,14 @@ "ContentsUnauthorizedError": ".contentsop", "ContentsUnauthorizedErrorData": ".contentsop", "NoResponseError": ".no_response_error", + "ResearchForbiddenError": ".researchop", + "ResearchForbiddenErrorData": ".researchop", + "ResearchInternalServerError": ".researchop", + "ResearchInternalServerErrorData": ".researchop", + "ResearchUnauthorizedError": ".researchop", + "ResearchUnauthorizedErrorData": ".researchop", + "UnprocessableEntityError": ".researchop", + "UnprocessableEntityErrorData": ".researchop", "ResponseValidationError": ".responsevalidationerror", "SearchForbiddenError": ".searchop", "SearchForbiddenErrorData": ".searchop", @@ -89,39 +114,11 @@ } -def dynamic_import(modname, retries=3): - for attempt in range(retries): - try: - return import_module(modname, __package__) - except KeyError: - # Clear any half-initialized module and retry - sys.modules.pop(modname, None) - if attempt == retries - 1: - break - raise KeyError(f"Failed to import module '{modname}' after {retries} attempts") - - -def __getattr__(attr_name: str) -> object: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError( - f"No {attr_name} found in _dynamic_imports for module name -> {__name__} " - ) - - try: - module = dynamic_import(module_name) - result = getattr(module, attr_name) - return result - except ImportError as e: - raise ImportError( - f"Failed to import {attr_name} from {module_name}: {e}" - ) from e - except AttributeError as e: - raise AttributeError( - f"Failed to get {attr_name} from {module_name}: {e}" - ) from e +def __getattr__(attr_name: str) -> Any: + return lazy_getattr( + attr_name, package=__package__, dynamic_imports=_dynamic_imports + ) def __dir__(): - lazy_attrs = builtins.list(_dynamic_imports.keys()) - return builtins.sorted(lazy_attrs) + return lazy_dir(dynamic_imports=_dynamic_imports) diff --git a/src/youdotcom/errors/contentsop.py b/src/youdotcom/errors/contentsop.py index 5f36db1..443fd30 100644 --- a/src/youdotcom/errors/contentsop.py +++ b/src/youdotcom/errors/contentsop.py @@ -14,7 +14,7 @@ class ContentsInternalServerErrorData(BaseModel): @dataclass(unsafe_hash=True) class ContentsInternalServerError(YouError): - r"""Internal Server Error""" + r"""Internal Server Error during authentication/authorization middleware.""" data: ContentsInternalServerErrorData = field(hash=False) @@ -35,7 +35,7 @@ class ContentsForbiddenErrorData(BaseModel): @dataclass(unsafe_hash=True) class ContentsForbiddenError(YouError): - r"""Forbidden""" + r"""Forbidden. API key lacks scope for this path.""" data: ContentsForbiddenErrorData = field(hash=False) @@ -57,7 +57,7 @@ class ContentsUnauthorizedErrorData(BaseModel): @dataclass(unsafe_hash=True) class ContentsUnauthorizedError(YouError): - r"""Unauthorized""" + r"""Unauthorized. Problems with API key.""" data: ContentsUnauthorizedErrorData = field(hash=False) diff --git a/src/youdotcom/errors/researchop.py b/src/youdotcom/errors/researchop.py new file mode 100644 index 0000000..b1a5836 --- /dev/null +++ b/src/youdotcom/errors/researchop.py @@ -0,0 +1,94 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from dataclasses import dataclass, field +import httpx +from typing import List, Optional +from youdotcom.errors import YouError +from youdotcom.models import researchop as models_researchop +from youdotcom.types import BaseModel + + +class ResearchInternalServerErrorData(BaseModel): + detail: Optional[str] = None + + +@dataclass(unsafe_hash=True) +class ResearchInternalServerError(YouError): + r"""Internal Server Error during authentication/authorization middleware.""" + + data: ResearchInternalServerErrorData = field(hash=False) + + def __init__( + self, + data: ResearchInternalServerErrorData, + raw_response: httpx.Response, + body: Optional[str] = None, + ): + message = body or raw_response.text + super().__init__(message, raw_response, body) + object.__setattr__(self, "data", data) + + +class UnprocessableEntityErrorData(BaseModel): + detail: Optional[List[models_researchop.ResearchDetail]] = None + + +@dataclass(unsafe_hash=True) +class UnprocessableEntityError(YouError): + r"""Unprocessable Entity. Request validation failed.""" + + data: UnprocessableEntityErrorData = field(hash=False) + + def __init__( + self, + data: UnprocessableEntityErrorData, + raw_response: httpx.Response, + body: Optional[str] = None, + ): + message = body or raw_response.text + super().__init__(message, raw_response, body) + object.__setattr__(self, "data", data) + + +class ResearchForbiddenErrorData(BaseModel): + detail: Optional[str] = None + + +@dataclass(unsafe_hash=True) +class ResearchForbiddenError(YouError): + r"""Forbidden. API key lacks scope for this path.""" + + data: ResearchForbiddenErrorData = field(hash=False) + + def __init__( + self, + data: ResearchForbiddenErrorData, + raw_response: httpx.Response, + body: Optional[str] = None, + ): + message = body or raw_response.text + super().__init__(message, raw_response, body) + object.__setattr__(self, "data", data) + + +class ResearchUnauthorizedErrorData(BaseModel): + detail: Optional[str] = None + r"""Error detail message.""" + + +@dataclass(unsafe_hash=True) +class ResearchUnauthorizedError(YouError): + r"""Unauthorized. Problems with API key.""" + + data: ResearchUnauthorizedErrorData = field(hash=False) + + def __init__( + self, + data: ResearchUnauthorizedErrorData, + raw_response: httpx.Response, + body: Optional[str] = None, + ): + message = body or raw_response.text + super().__init__(message, raw_response, body) + object.__setattr__(self, "data", data) diff --git a/src/youdotcom/models/__init__.py b/src/youdotcom/models/__init__.py index 41218f5..804e5c2 100644 --- a/src/youdotcom/models/__init__.py +++ b/src/youdotcom/models/__init__.py @@ -1,9 +1,8 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" -from typing import TYPE_CHECKING -from importlib import import_module -import builtins -import sys +from typing import Any, TYPE_CHECKING + +from youdotcom.utils.dynamic_imports import lazy_getattr, lazy_dir if TYPE_CHECKING: from .advancedagentrunsrequest import ( @@ -18,8 +17,8 @@ from .agentrunsbatchresponse import ( AgentRunsBatchResponse, AgentRunsBatchResponseTypedDict, - Input, - InputTypedDict, + Input1, + Input1TypedDict, Role, ) from .agentrunsresponseoutput import ( @@ -38,7 +37,6 @@ DataTypedDict, ) from .agentsrunsop import ( - AGENTS_RUNS_OP_SERVERS, AgentsRunsRequest, AgentsRunsRequestTypedDict, AgentsRunsResponse, @@ -49,6 +47,7 @@ from .contentsformats import ContentsFormats from .contentsmetadata import ContentsMetadata, ContentsMetadataTypedDict from .contentsop import ( + CONTENTS_OP_SERVERS, ContentsRequest, ContentsRequestTypedDict, ContentsResponse, @@ -68,6 +67,26 @@ from .livecrawl import LiveCrawl from .livecrawlformats import LiveCrawlFormats from .reportverbosity import ReportVerbosity + from .researchop import ( + ContentType, + Input2, + Input2TypedDict, + Output, + OutputTypedDict, + ResearchDetail, + ResearchDetailTypedDict, + ResearchEffort, + ResearchInput, + ResearchInputTypedDict, + ResearchLoc, + ResearchLocTypedDict, + ResearchRequest, + ResearchRequestTypedDict, + ResearchResponse, + ResearchResponseTypedDict, + Source, + SourceTypedDict, + ) from .researchtool import ResearchTool, ResearchToolTypedDict from .response_created import ResponseCreated, ResponseCreatedTypedDict from .response_done import ( @@ -133,7 +152,6 @@ from .websearchtool import WebSearchTool, WebSearchToolTypedDict __all__ = [ - "AGENTS_RUNS_OP_SERVERS", "AdvancedAgentRunsRequest", "AdvancedAgentRunsRequestTypedDict", "AgentRunsBatchResponse", @@ -148,8 +166,10 @@ "AgentsRunsRequestTypedDict", "AgentsRunsResponse", "AgentsRunsResponseTypedDict", + "CONTENTS_OP_SERVERS", "ComputeTool", "ComputeToolTypedDict", + "ContentType", "Contents", "ContentsFormats", "ContentsMetadata", @@ -169,8 +189,10 @@ "ExpressAgentRunsRequest", "ExpressAgentRunsRequestTypedDict", "Freshness", - "Input", - "InputTypedDict", + "Input1", + "Input1TypedDict", + "Input2", + "Input2TypedDict", "Language", "LiveCrawl", "LiveCrawlFormats", @@ -180,7 +202,20 @@ "MetadataTypedDict", "News", "NewsTypedDict", + "Output", + "OutputTypedDict", "ReportVerbosity", + "ResearchDetail", + "ResearchDetailTypedDict", + "ResearchEffort", + "ResearchInput", + "ResearchInputTypedDict", + "ResearchLoc", + "ResearchLocTypedDict", + "ResearchRequest", + "ResearchRequestTypedDict", + "ResearchResponse", + "ResearchResponseTypedDict", "ResearchTool", "ResearchToolTypedDict", "ResponseCreated", @@ -229,6 +264,8 @@ "SearchSafesearchTypedDict", "Security", "SecurityTypedDict", + "Source", + "SourceTypedDict", "Tool", "ToolTypedDict", "Type", @@ -254,8 +291,8 @@ "LocTypedDict": ".agentruns422response_error", "AgentRunsBatchResponse": ".agentrunsbatchresponse", "AgentRunsBatchResponseTypedDict": ".agentrunsbatchresponse", - "Input": ".agentrunsbatchresponse", - "InputTypedDict": ".agentrunsbatchresponse", + "Input1": ".agentrunsbatchresponse", + "Input1TypedDict": ".agentrunsbatchresponse", "Role": ".agentrunsbatchresponse", "AgentRunsResponseOutput": ".agentrunsresponseoutput", "AgentRunsResponseOutputTypedDict": ".agentrunsresponseoutput", @@ -266,7 +303,6 @@ "AgentRunsStreamingResponseTypedDict": ".agentrunsstreamingresponse", "Data": ".agentrunsstreamingresponse", "DataTypedDict": ".agentrunsstreamingresponse", - "AGENTS_RUNS_OP_SERVERS": ".agentsrunsop", "AgentsRunsRequest": ".agentsrunsop", "AgentsRunsRequestTypedDict": ".agentsrunsop", "AgentsRunsResponse": ".agentsrunsop", @@ -278,6 +314,7 @@ "ContentsFormats": ".contentsformats", "ContentsMetadata": ".contentsmetadata", "ContentsMetadataTypedDict": ".contentsmetadata", + "CONTENTS_OP_SERVERS": ".contentsop", "ContentsRequest": ".contentsop", "ContentsRequestTypedDict": ".contentsop", "ContentsResponse": ".contentsop", @@ -292,6 +329,24 @@ "LiveCrawl": ".livecrawl", "LiveCrawlFormats": ".livecrawlformats", "ReportVerbosity": ".reportverbosity", + "ContentType": ".researchop", + "Input2": ".researchop", + "Input2TypedDict": ".researchop", + "Output": ".researchop", + "OutputTypedDict": ".researchop", + "ResearchDetail": ".researchop", + "ResearchDetailTypedDict": ".researchop", + "ResearchEffort": ".researchop", + "ResearchInput": ".researchop", + "ResearchInputTypedDict": ".researchop", + "ResearchLoc": ".researchop", + "ResearchLocTypedDict": ".researchop", + "ResearchRequest": ".researchop", + "ResearchRequestTypedDict": ".researchop", + "ResearchResponse": ".researchop", + "ResearchResponseTypedDict": ".researchop", + "Source": ".researchop", + "SourceTypedDict": ".researchop", "ResearchTool": ".researchtool", "ResearchToolTypedDict": ".researchtool", "ResponseCreated": ".response_created", @@ -351,39 +406,11 @@ } -def dynamic_import(modname, retries=3): - for attempt in range(retries): - try: - return import_module(modname, __package__) - except KeyError: - # Clear any half-initialized module and retry - sys.modules.pop(modname, None) - if attempt == retries - 1: - break - raise KeyError(f"Failed to import module '{modname}' after {retries} attempts") - - -def __getattr__(attr_name: str) -> object: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError( - f"No {attr_name} found in _dynamic_imports for module name -> {__name__} " - ) - - try: - module = dynamic_import(module_name) - result = getattr(module, attr_name) - return result - except ImportError as e: - raise ImportError( - f"Failed to import {attr_name} from {module_name}: {e}" - ) from e - except AttributeError as e: - raise AttributeError( - f"Failed to get {attr_name} from {module_name}: {e}" - ) from e +def __getattr__(attr_name: str) -> Any: + return lazy_getattr( + attr_name, package=__package__, dynamic_imports=_dynamic_imports + ) def __dir__(): - lazy_attrs = builtins.list(_dynamic_imports.keys()) - return builtins.sorted(lazy_attrs) + return lazy_dir(dynamic_imports=_dynamic_imports) diff --git a/src/youdotcom/models/advancedagentrunsrequest.py b/src/youdotcom/models/advancedagentrunsrequest.py index a85f6b1..f271b2b 100644 --- a/src/youdotcom/models/advancedagentrunsrequest.py +++ b/src/youdotcom/models/advancedagentrunsrequest.py @@ -101,3 +101,9 @@ def serialize_model(self, handler): m[k] = val return m + + +try: + AdvancedAgentRunsRequest.model_rebuild() +except NameError: + pass diff --git a/src/youdotcom/models/agentrunsbatchresponse.py b/src/youdotcom/models/agentrunsbatchresponse.py index 8324547..187616c 100644 --- a/src/youdotcom/models/agentrunsbatchresponse.py +++ b/src/youdotcom/models/agentrunsbatchresponse.py @@ -18,14 +18,14 @@ class Role(str, Enum): USER = "user" -class InputTypedDict(TypedDict): +class Input1TypedDict(TypedDict): role: Role r"""The access based role of the user""" content: str r"""The question populated in the request payload""" -class Input(BaseModel): +class Input1(BaseModel): role: Role r"""The access based role of the user""" @@ -36,7 +36,7 @@ class Input(BaseModel): class AgentRunsBatchResponseTypedDict(TypedDict): agent: str r"""The id of the agent populated in the request.""" - input: List[InputTypedDict] + input: List[Input1TypedDict] r"""The users access role and question you asked the agent""" output: List[AgentRunsResponseOutputTypedDict] r"""Array of response outputs from the agent""" @@ -48,7 +48,7 @@ class AgentRunsBatchResponse(BaseModel): agent: str r"""The id of the agent populated in the request.""" - input: List[Input] + input: List[Input1] r"""The users access role and question you asked the agent""" output: List[AgentRunsResponseOutput] diff --git a/src/youdotcom/models/agentrunsresponsewebsearchresult.py b/src/youdotcom/models/agentrunsresponsewebsearchresult.py index 74c19ac..a0a105c 100644 --- a/src/youdotcom/models/agentrunsresponsewebsearchresult.py +++ b/src/youdotcom/models/agentrunsresponsewebsearchresult.py @@ -71,3 +71,9 @@ def serialize_model(self, handler): m[k] = val return m + + +try: + AgentRunsResponseWebSearchResult.model_rebuild() +except NameError: + pass diff --git a/src/youdotcom/models/agentsrunsop.py b/src/youdotcom/models/agentsrunsop.py index 52a57d5..2ac90c0 100644 --- a/src/youdotcom/models/agentsrunsop.py +++ b/src/youdotcom/models/agentsrunsop.py @@ -26,11 +26,6 @@ from youdotcom.utils import eventstreaming -AGENTS_RUNS_OP_SERVERS = [ - "https://api.you.com", -] - - AgentsRunsRequestTypedDict = TypeAliasType( "AgentsRunsRequestTypedDict", Union[ diff --git a/src/youdotcom/models/computetool.py b/src/youdotcom/models/computetool.py index b70495a..1253881 100644 --- a/src/youdotcom/models/computetool.py +++ b/src/youdotcom/models/computetool.py @@ -20,3 +20,9 @@ class ComputeTool(BaseModel): pydantic.Field(alias="type"), ] = "compute" r"""Setting this value to \"compute\" is mandatory to use the compute agent.""" + + +try: + ComputeTool.model_rebuild() +except NameError: + pass diff --git a/src/youdotcom/models/contentsop.py b/src/youdotcom/models/contentsop.py index b1f47da..fc47c7f 100644 --- a/src/youdotcom/models/contentsop.py +++ b/src/youdotcom/models/contentsop.py @@ -9,13 +9,18 @@ from youdotcom.types import BaseModel, Nullable, OptionalNullable, UNSET, UNSET_SENTINEL +CONTENTS_OP_SERVERS = [ + "https://ydc-index.io", +] + + class ContentsRequestTypedDict(TypedDict): urls: NotRequired[List[str]] r"""Array of URLs to fetch the contents from.""" formats: NotRequired[List[ContentsFormats]] - r"""The formats of the content to be returned. Can include 'html', 'markdown', and/or 'metadata'.""" - crawl_timeout: NotRequired[float] - r"""The timeout in seconds for crawling each URL. Must be between 1 and 60 seconds.""" + r"""Array of content formats to return. All included formats are returned in the response. Include \"metadata\" to get JSON-LD and OpenGraph information, if available.""" + crawl_timeout: NotRequired[int] + r"""Maximum time in seconds to wait for page content. Must be between 1 and 60 seconds. Default is 10 seconds.""" class ContentsRequest(BaseModel): @@ -23,10 +28,10 @@ class ContentsRequest(BaseModel): r"""Array of URLs to fetch the contents from.""" formats: Optional[List[ContentsFormats]] = None - r"""The formats of the content to be returned. Can include 'html', 'markdown', and/or 'metadata'.""" + r"""Array of content formats to return. All included formats are returned in the response. Include \"metadata\" to get JSON-LD and OpenGraph information, if available.""" - crawl_timeout: Optional[float] = None - r"""The timeout in seconds for crawling each URL. Must be between 1 and 60 seconds.""" + crawl_timeout: Optional[int] = 10 + r"""Maximum time in seconds to wait for page content. Must be between 1 and 60 seconds. Default is 10 seconds.""" @model_serializer(mode="wrap") def serialize_model(self, handler): diff --git a/src/youdotcom/models/expressagentrunsrequest.py b/src/youdotcom/models/expressagentrunsrequest.py index a3e47f8..f4f5de0 100644 --- a/src/youdotcom/models/expressagentrunsrequest.py +++ b/src/youdotcom/models/expressagentrunsrequest.py @@ -53,3 +53,9 @@ def serialize_model(self, handler): m[k] = val return m + + +try: + ExpressAgentRunsRequest.model_rebuild() +except NameError: + pass diff --git a/src/youdotcom/models/researchop.py b/src/youdotcom/models/researchop.py new file mode 100644 index 0000000..f8ec45c --- /dev/null +++ b/src/youdotcom/models/researchop.py @@ -0,0 +1,223 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from enum import Enum +from pydantic import model_serializer +from typing import Any, Dict, List, Optional, Union +from typing_extensions import NotRequired, TypeAliasType, TypedDict +from youdotcom.types import BaseModel, UNSET_SENTINEL + + +class ResearchEffort(str, Enum): + r"""Controls how much time and effort the Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. + + Available levels: + - `lite`: Returns answers quickly. Good for straightforward questions that just need a fast, reliable answer. + - `standard`: The default. Balances speed and depth, a good fit for most questions. + - `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed. + - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. + """ + + LITE = "lite" + STANDARD = "standard" + DEEP = "deep" + EXHAUSTIVE = "exhaustive" + + +class ResearchRequestTypedDict(TypedDict): + input: str + r"""The research question or complex query requiring in-depth investigation and multi-step reasoning. + + Note: The maximum length of the input is 40,000 characters. + """ + research_effort: NotRequired[ResearchEffort] + r"""Controls how much time and effort the Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. + + Available levels: + - `lite`: Returns answers quickly. Good for straightforward questions that just need a fast, reliable answer. + - `standard`: The default. Balances speed and depth, a good fit for most questions. + - `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed. + - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. + """ + + +class ResearchRequest(BaseModel): + input: str + r"""The research question or complex query requiring in-depth investigation and multi-step reasoning. + + Note: The maximum length of the input is 40,000 characters. + """ + + research_effort: Optional[ResearchEffort] = ResearchEffort.STANDARD + r"""Controls how much time and effort the Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. + + Available levels: + - `lite`: Returns answers quickly. Good for straightforward questions that just need a fast, reliable answer. + - `standard`: The default. Balances speed and depth, a good fit for most questions. + - `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed. + - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. + """ + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["research_effort"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m + + +ResearchLocTypedDict = TypeAliasType("ResearchLocTypedDict", Union[str, int]) + + +ResearchLoc = TypeAliasType("ResearchLoc", Union[str, int]) + + +class ResearchInputTypedDict(TypedDict): + pass + + +class ResearchInput(BaseModel): + pass + + +Input2TypedDict = TypeAliasType("Input2TypedDict", Union[ResearchInputTypedDict, str]) +r"""The input value that caused the error.""" + + +Input2 = TypeAliasType("Input2", Union[ResearchInput, str]) +r"""The input value that caused the error.""" + + +class ResearchDetailTypedDict(TypedDict): + type: str + r"""The validation error type.""" + loc: List[ResearchLocTypedDict] + r"""The location of the error as a path of segments (strings for field names, integers for byte offsets).""" + msg: str + r"""A human-readable description of the error.""" + input: Input2TypedDict + r"""The input value that caused the error.""" + ctx: NotRequired[Dict[str, Any]] + r"""Additional context about the error.""" + + +class ResearchDetail(BaseModel): + type: str + r"""The validation error type.""" + + loc: List[ResearchLoc] + r"""The location of the error as a path of segments (strings for field names, integers for byte offsets).""" + + msg: str + r"""A human-readable description of the error.""" + + input: Input2 + r"""The input value that caused the error.""" + + ctx: Optional[Dict[str, Any]] = None + r"""Additional context about the error.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["ctx"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m + + +class ContentType(str, Enum): + r"""The format of the content field.""" + + TEXT = "text" + + +class SourceTypedDict(TypedDict): + url: str + r"""The URL of the source webpage.""" + title: NotRequired[str] + r"""The title of the source webpage.""" + snippets: NotRequired[List[str]] + r"""Relevant excerpts from the source page that were used in generating the answer.""" + + +class Source(BaseModel): + url: str + r"""The URL of the source webpage.""" + + title: Optional[str] = None + r"""The title of the source webpage.""" + + snippets: Optional[List[str]] = None + r"""Relevant excerpts from the source page that were used in generating the answer.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["title", "snippets"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m + + +class OutputTypedDict(TypedDict): + r"""The research output containing the answer and sources.""" + + content: str + r"""The comprehensive response with inline citations. The content is formatted in Markdown and includes numbered citations that reference the items in the sources array.""" + content_type: ContentType + r"""The format of the content field.""" + sources: List[SourceTypedDict] + r"""A list of web sources used to generate the answer.""" + + +class Output(BaseModel): + r"""The research output containing the answer and sources.""" + + content: str + r"""The comprehensive response with inline citations. The content is formatted in Markdown and includes numbered citations that reference the items in the sources array.""" + + content_type: ContentType + r"""The format of the content field.""" + + sources: List[Source] + r"""A list of web sources used to generate the answer.""" + + +class ResearchResponseTypedDict(TypedDict): + r"""A JSON object containing a comprehensive answer with citations and supporting search results""" + + output: OutputTypedDict + r"""The research output containing the answer and sources.""" + + +class ResearchResponse(BaseModel): + r"""A JSON object containing a comprehensive answer with citations and supporting search results""" + + output: Output + r"""The research output containing the answer and sources.""" diff --git a/src/youdotcom/models/researchtool.py b/src/youdotcom/models/researchtool.py index bb9930a..445f60c 100644 --- a/src/youdotcom/models/researchtool.py +++ b/src/youdotcom/models/researchtool.py @@ -38,3 +38,9 @@ class ResearchTool(BaseModel): pydantic.Field(alias="type"), ] = "research" r"""Setting this value to \"research\" is mandatory to use the research agent.""" + + +try: + ResearchTool.model_rebuild() +except NameError: + pass diff --git a/src/youdotcom/models/response_created.py b/src/youdotcom/models/response_created.py index a7e2625..509085d 100644 --- a/src/youdotcom/models/response_created.py +++ b/src/youdotcom/models/response_created.py @@ -28,3 +28,9 @@ class ResponseCreated(BaseModel): ], pydantic.Field(alias="type"), ] = "response.created" + + +try: + ResponseCreated.model_rebuild() +except NameError: + pass diff --git a/src/youdotcom/models/response_done.py b/src/youdotcom/models/response_done.py index fa2d419..599f8a7 100644 --- a/src/youdotcom/models/response_done.py +++ b/src/youdotcom/models/response_done.py @@ -41,3 +41,9 @@ class ResponseDone(BaseModel): ], pydantic.Field(alias="type"), ] = "response.done" + + +try: + ResponseDone.model_rebuild() +except NameError: + pass diff --git a/src/youdotcom/models/response_output_content_full.py b/src/youdotcom/models/response_output_content_full.py index dc6f1b2..b2bd70c 100644 --- a/src/youdotcom/models/response_output_content_full.py +++ b/src/youdotcom/models/response_output_content_full.py @@ -53,3 +53,13 @@ class ResponseOutputContentFull(BaseModel): ], pydantic.Field(alias="type"), ] = "response.output_content.full" + + +try: + ResponseOutputContentFullResponse.model_rebuild() +except NameError: + pass +try: + ResponseOutputContentFull.model_rebuild() +except NameError: + pass diff --git a/src/youdotcom/models/response_output_item_added.py b/src/youdotcom/models/response_output_item_added.py index 7f4af24..7294962 100644 --- a/src/youdotcom/models/response_output_item_added.py +++ b/src/youdotcom/models/response_output_item_added.py @@ -41,3 +41,9 @@ class ResponseOutputItemAdded(BaseModel): ], pydantic.Field(alias="type"), ] = "response.output_item.added" + + +try: + ResponseOutputItemAdded.model_rebuild() +except NameError: + pass diff --git a/src/youdotcom/models/response_output_item_done.py b/src/youdotcom/models/response_output_item_done.py index bd3f6c4..54f4cbc 100644 --- a/src/youdotcom/models/response_output_item_done.py +++ b/src/youdotcom/models/response_output_item_done.py @@ -35,3 +35,9 @@ class ResponseOutputItemDone(BaseModel): ], pydantic.Field(alias="type"), ] = "response.output_item.done" + + +try: + ResponseOutputItemDone.model_rebuild() +except NameError: + pass diff --git a/src/youdotcom/models/response_output_text_delta.py b/src/youdotcom/models/response_output_text_delta.py index 4520ad5..36aa5a4 100644 --- a/src/youdotcom/models/response_output_text_delta.py +++ b/src/youdotcom/models/response_output_text_delta.py @@ -48,3 +48,13 @@ class ResponseOutputTextDelta(BaseModel): ], pydantic.Field(alias="type"), ] = "response.output_text.delta" + + +try: + ResponseOutputTextDeltaResponse.model_rebuild() +except NameError: + pass +try: + ResponseOutputTextDelta.model_rebuild() +except NameError: + pass diff --git a/src/youdotcom/models/response_starting.py b/src/youdotcom/models/response_starting.py index 481a481..438a191 100644 --- a/src/youdotcom/models/response_starting.py +++ b/src/youdotcom/models/response_starting.py @@ -28,3 +28,9 @@ class ResponseStarting(BaseModel): ], pydantic.Field(alias="type"), ] = "response.starting" + + +try: + ResponseStarting.model_rebuild() +except NameError: + pass diff --git a/src/youdotcom/models/searchop.py b/src/youdotcom/models/searchop.py index 9b7de76..a06a0fd 100644 --- a/src/youdotcom/models/searchop.py +++ b/src/youdotcom/models/searchop.py @@ -24,11 +24,17 @@ SearchFreshnessTypedDict = TypeAliasType( "SearchFreshnessTypedDict", Union[Freshness, str] ) -r"""Specifies the freshness of the results to return.""" +r"""Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. + +When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. +""" SearchFreshness = TypeAliasType("SearchFreshness", Union[Freshness, str]) -r"""Specifies the freshness of the results to return.""" +r"""Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. + +When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. +""" SearchCountryTypedDict = TypeAliasType("SearchCountryTypedDict", Union[Country, str]) @@ -77,7 +83,10 @@ class SearchRequestTypedDict(TypedDict): count: NotRequired[int] r"""Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them).""" freshness: NotRequired[SearchFreshnessTypedDict] - r"""Specifies the freshness of the results to return.""" + r"""Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. + + When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. + """ offset: NotRequired[int] r"""Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`.""" country: NotRequired[SearchCountryTypedDict] @@ -101,14 +110,17 @@ class SearchRequest(BaseModel): count: Annotated[ Optional[int], FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), - ] = None + ] = 10 r"""Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them).""" freshness: Annotated[ Optional[SearchFreshness], FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), ] = None - r"""Specifies the freshness of the results to return.""" + r"""Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. + + When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. + """ offset: Annotated[ Optional[int], diff --git a/src/youdotcom/models/websearchtool.py b/src/youdotcom/models/websearchtool.py index 274c2fd..7834ec3 100644 --- a/src/youdotcom/models/websearchtool.py +++ b/src/youdotcom/models/websearchtool.py @@ -20,3 +20,9 @@ class WebSearchTool(BaseModel): pydantic.Field(alias="type"), ] = "web_search" r"""Setting this value to \"web_search\" is mandatory to use the web_search tool.""" + + +try: + WebSearchTool.model_rebuild() +except NameError: + pass diff --git a/src/youdotcom/runs.py b/src/youdotcom/runs.py index 02e1733..0fc5917 100644 --- a/src/youdotcom/runs.py +++ b/src/youdotcom/runs.py @@ -52,7 +52,7 @@ def create( if server_url is not None: base_url = server_url else: - base_url = models.AGENTS_RUNS_OP_SERVERS[0] + base_url = self._get_url(base_url, url_variables) if not isinstance(request, BaseModel): request = utils.unmarshal(request, models.AgentsRunsRequest) @@ -190,7 +190,7 @@ async def create_async( if server_url is not None: base_url = server_url else: - base_url = models.AGENTS_RUNS_OP_SERVERS[0] + base_url = self._get_url(base_url, url_variables) if not isinstance(request, BaseModel): request = utils.unmarshal(request, models.AgentsRunsRequest) diff --git a/src/youdotcom/sdk.py b/src/youdotcom/sdk.py index d25ff3e..c72bef1 100644 --- a/src/youdotcom/sdk.py +++ b/src/youdotcom/sdk.py @@ -8,11 +8,13 @@ import httpx import importlib import sys -from typing import Any, Callable, Dict, Optional, TYPE_CHECKING, Union, cast +from typing import Any, Callable, Dict, Mapping, Optional, TYPE_CHECKING, Union, cast import weakref -from youdotcom import models, utils -from youdotcom._hooks import SDKHooks +from youdotcom import errors, models, utils +from youdotcom._hooks import HookContext, SDKHooks from youdotcom.types import OptionalNullable, UNSET +from youdotcom.utils import get_security_from_env +from youdotcom.utils.unmarshal_json_response import unmarshal_json_response if TYPE_CHECKING: from youdotcom.agents import Agents @@ -21,11 +23,14 @@ class You(BaseSDK): - r"""You.com API: Comprehensive API for You.com services: + r"""You.com API: Unified API for Express, Advanced, and Custom Agents from You.com + Get the best search results from web and news sources + Returns the HTML or Markdown of a target webpage + Multi-step reasoning with comprehensive research capabilities + Comprehensive API for You.com services: - **Agents API**: Execute queries using Express, Advanced, and Custom AI agents - **Search API**: Get search results from web and news sources - **Contents API**: Retrieve and process web page content - """ agents: "Agents" @@ -43,8 +48,8 @@ def __init__( Union[Optional[str], Callable[[], Optional[str]]] ] = None, server_idx: Optional[int] = None, - server_url: Optional[str] = None, url_params: Optional[Dict[str, str]] = None, + server_url: Optional[str] = None, client: Optional[HttpClient] = None, async_client: Optional[AsyncHttpClient] = None, retry_config: OptionalNullable[RetryConfig] = UNSET, @@ -187,3 +192,245 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): ): await self.sdk_configuration.async_client.aclose() self.sdk_configuration.async_client = None + + def research( + self, + *, + input: str, + research_effort: Optional[ + models.ResearchEffort + ] = models.ResearchEffort.STANDARD, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> models.ResearchResponse: + r"""Returns comprehensive research-grade answers with multi-step reasoning + + Research goes beyond a single web search. In response to your question, it runs multiple searches, reads through the sources, and synthesizes everything into a thorough, well-cited answer. Use it when a question is too complex for a simple lookup, and when you need a response you can actually trust and verify. + + :param input: The research question or complex query requiring in-depth investigation and multi-step reasoning. + + Note: The maximum length of the input is 40,000 characters. + :param research_effort: Controls how much time and effort the Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. + + Available levels: + - `lite`: Returns answers quickly. Good for straightforward questions that just need a fast, reliable answer. + - `standard`: The default. Balances speed and depth, a good fit for most questions. + - `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed. + - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + request = models.ResearchRequest( + input=input, + research_effort=research_effort, + ) + + req = self._build_request( + method="POST", + path="/v1/research", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=True, + request_has_path_params=False, + request_has_query_params=True, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + security=self.sdk_configuration.security, + get_serialized_body=lambda: utils.serialize_request_body( + request, False, False, "json", models.ResearchRequest + ), + allow_empty_value=None, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["429", "500", "502", "503", "504"]) + + http_res = self.do_request( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="research", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, models.Security + ), + ), + request=req, + error_status_codes=["401", "403", "422", "4XX", "500", "5XX"], + retry_config=retry_config, + ) + + response_data: Any = None + if utils.match_response(http_res, "200", "application/json"): + return unmarshal_json_response(models.ResearchResponse, http_res) + if utils.match_response(http_res, "401", "application/json"): + response_data = unmarshal_json_response( + errors.ResearchUnauthorizedErrorData, http_res + ) + raise errors.ResearchUnauthorizedError(response_data, http_res) + if utils.match_response(http_res, "403", "application/json"): + response_data = unmarshal_json_response( + errors.ResearchForbiddenErrorData, http_res + ) + raise errors.ResearchForbiddenError(response_data, http_res) + if utils.match_response(http_res, "422", "application/json"): + response_data = unmarshal_json_response( + errors.UnprocessableEntityErrorData, http_res + ) + raise errors.UnprocessableEntityError(response_data, http_res) + if utils.match_response(http_res, "500", "application/json"): + response_data = unmarshal_json_response( + errors.ResearchInternalServerErrorData, http_res + ) + raise errors.ResearchInternalServerError(response_data, http_res) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + + raise errors.YouDefaultError("Unexpected response received", http_res) + + async def research_async( + self, + *, + input: str, + research_effort: Optional[ + models.ResearchEffort + ] = models.ResearchEffort.STANDARD, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> models.ResearchResponse: + r"""Returns comprehensive research-grade answers with multi-step reasoning + + Research goes beyond a single web search. In response to your question, it runs multiple searches, reads through the sources, and synthesizes everything into a thorough, well-cited answer. Use it when a question is too complex for a simple lookup, and when you need a response you can actually trust and verify. + + :param input: The research question or complex query requiring in-depth investigation and multi-step reasoning. + + Note: The maximum length of the input is 40,000 characters. + :param research_effort: Controls how much time and effort the Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. + + Available levels: + - `lite`: Returns answers quickly. Good for straightforward questions that just need a fast, reliable answer. + - `standard`: The default. Balances speed and depth, a good fit for most questions. + - `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed. + - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + request = models.ResearchRequest( + input=input, + research_effort=research_effort, + ) + + req = self._build_request_async( + method="POST", + path="/v1/research", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=True, + request_has_path_params=False, + request_has_query_params=True, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + security=self.sdk_configuration.security, + get_serialized_body=lambda: utils.serialize_request_body( + request, False, False, "json", models.ResearchRequest + ), + allow_empty_value=None, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["429", "500", "502", "503", "504"]) + + http_res = await self.do_request_async( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="research", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, models.Security + ), + ), + request=req, + error_status_codes=["401", "403", "422", "4XX", "500", "5XX"], + retry_config=retry_config, + ) + + response_data: Any = None + if utils.match_response(http_res, "200", "application/json"): + return unmarshal_json_response(models.ResearchResponse, http_res) + if utils.match_response(http_res, "401", "application/json"): + response_data = unmarshal_json_response( + errors.ResearchUnauthorizedErrorData, http_res + ) + raise errors.ResearchUnauthorizedError(response_data, http_res) + if utils.match_response(http_res, "403", "application/json"): + response_data = unmarshal_json_response( + errors.ResearchForbiddenErrorData, http_res + ) + raise errors.ResearchForbiddenError(response_data, http_res) + if utils.match_response(http_res, "422", "application/json"): + response_data = unmarshal_json_response( + errors.UnprocessableEntityErrorData, http_res + ) + raise errors.UnprocessableEntityError(response_data, http_res) + if utils.match_response(http_res, "500", "application/json"): + response_data = unmarshal_json_response( + errors.ResearchInternalServerErrorData, http_res + ) + raise errors.ResearchInternalServerError(response_data, http_res) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + + raise errors.YouDefaultError("Unexpected response received", http_res) diff --git a/src/youdotcom/sdkconfiguration.py b/src/youdotcom/sdkconfiguration.py index 05b634c..ea49a89 100644 --- a/src/youdotcom/sdkconfiguration.py +++ b/src/youdotcom/sdkconfiguration.py @@ -16,7 +16,7 @@ SERVERS = [ - "https://ydc-index.io", + "https://api.you.com", ] """Contains the list of servers available to the SDK""" diff --git a/src/youdotcom/search.py b/src/youdotcom/search.py index 47c8e0c..355ce77 100644 --- a/src/youdotcom/search.py +++ b/src/youdotcom/search.py @@ -14,7 +14,7 @@ def unified( self, *, query: str = "Your query", - count: Optional[int] = None, + count: Optional[int] = 10, freshness: Optional[ Union[models.SearchFreshness, models.SearchFreshnessTypedDict] ] = None, @@ -39,9 +39,13 @@ def unified( ) -> models.SearchResponse: r"""Returns a list of unified search results from web and news sources + This endpoint is designed to return LLM-ready web results based on a user's query. Based on a classification mechanism, it can return web results and news associated with your query. If you need to feed an LLM with the results of a query that sounds like `What are the latest geopolitical updates from India`, then this endpoint is the right one for you. + :param query: The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. :param count: Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). - :param freshness: Specifies the freshness of the results to return. + :param freshness: Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. + + When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. :param offset: Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. :param country: The country code that determines the geographical focus of the web results. :param language: The language of the web results that will be returned (BCP 47 format). @@ -146,7 +150,7 @@ async def unified_async( self, *, query: str = "Your query", - count: Optional[int] = None, + count: Optional[int] = 10, freshness: Optional[ Union[models.SearchFreshness, models.SearchFreshnessTypedDict] ] = None, @@ -171,9 +175,13 @@ async def unified_async( ) -> models.SearchResponse: r"""Returns a list of unified search results from web and news sources + This endpoint is designed to return LLM-ready web results based on a user's query. Based on a classification mechanism, it can return web results and news associated with your query. If you need to feed an LLM with the results of a query that sounds like `What are the latest geopolitical updates from India`, then this endpoint is the right one for you. + :param query: The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. :param count: Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). - :param freshness: Specifies the freshness of the results to return. + :param freshness: Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. + + When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. :param offset: Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. :param country: The country code that determines the geographical focus of the web results. :param language: The language of the web results that will be returned (BCP 47 format). diff --git a/src/youdotcom/utils/__init__.py b/src/youdotcom/utils/__init__.py index f4525b2..aded759 100644 --- a/src/youdotcom/utils/__init__.py +++ b/src/youdotcom/utils/__init__.py @@ -1,20 +1,16 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" -from typing import TYPE_CHECKING, Callable, TypeVar -from importlib import import_module +from typing import Any, TYPE_CHECKING, Callable, TypeVar import asyncio -import builtins -import sys + +from .dynamic_imports import lazy_getattr, lazy_dir _T = TypeVar("_T") async def run_sync_in_thread(func: Callable[..., _T], *args) -> _T: """Run a synchronous function in a thread pool to avoid blocking the event loop.""" - if sys.version_info >= (3, 9): - return await asyncio.to_thread(func, *args) - loop = asyncio.get_event_loop() - return await loop.run_in_executor(None, func, *args) + return await asyncio.to_thread(func, *args) if TYPE_CHECKING: @@ -172,38 +168,11 @@ async def run_sync_in_thread(func: Callable[..., _T], *args) -> _T: } -def dynamic_import(modname, retries=3): - for attempt in range(retries): - try: - return import_module(modname, __package__) - except KeyError: - # Clear any half-initialized module and retry - sys.modules.pop(modname, None) - if attempt == retries - 1: - break - raise KeyError(f"Failed to import module '{modname}' after {retries} attempts") - - -def __getattr__(attr_name: str) -> object: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError( - f"no {attr_name} found in _dynamic_imports, module name -> {__name__} " - ) - - try: - module = dynamic_import(module_name) - return getattr(module, attr_name) - except ImportError as e: - raise ImportError( - f"Failed to import {attr_name} from {module_name}: {e}" - ) from e - except AttributeError as e: - raise AttributeError( - f"Failed to get {attr_name} from {module_name}: {e}" - ) from e +def __getattr__(attr_name: str) -> Any: + return lazy_getattr( + attr_name, package=__package__, dynamic_imports=_dynamic_imports + ) def __dir__(): - lazy_attrs = builtins.list(_dynamic_imports.keys()) - return builtins.sorted(lazy_attrs) + return lazy_dir(dynamic_imports=_dynamic_imports) diff --git a/src/youdotcom/utils/dynamic_imports.py b/src/youdotcom/utils/dynamic_imports.py new file mode 100644 index 0000000..673edf8 --- /dev/null +++ b/src/youdotcom/utils/dynamic_imports.py @@ -0,0 +1,54 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from importlib import import_module +import builtins +import sys + + +def dynamic_import(package, modname, retries=3): + """Import a module relative to package, retrying on KeyError from half-initialized modules.""" + for attempt in range(retries): + try: + return import_module(modname, package) + except KeyError: + sys.modules.pop(modname, None) + if attempt == retries - 1: + break + raise KeyError(f"Failed to import module '{modname}' after {retries} attempts") + + +def lazy_getattr(attr_name, *, package, dynamic_imports, sub_packages=None): + """Module-level __getattr__ that lazily loads from a dynamic_imports mapping. + + Args: + attr_name: The attribute being looked up. + package: The caller's __package__ (for relative imports). + dynamic_imports: Dict mapping attribute names to relative module paths. + sub_packages: Optional list of subpackage names to lazy-load. + """ + module_name = dynamic_imports.get(attr_name) + if module_name is not None: + try: + module = dynamic_import(package, module_name) + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + if sub_packages and attr_name in sub_packages: + return import_module(f".{attr_name}", package) + + raise AttributeError(f"module '{package}' has no attribute '{attr_name}'") + + +def lazy_dir(*, dynamic_imports, sub_packages=None): + """Module-level __dir__ that lists lazily-loadable attributes.""" + lazy_attrs = builtins.list(dynamic_imports.keys()) + if sub_packages: + lazy_attrs.extend(sub_packages) + return builtins.sorted(lazy_attrs) diff --git a/src/youdotcom/utils/eventstreaming.py b/src/youdotcom/utils/eventstreaming.py index 0969899..f2052fc 100644 --- a/src/youdotcom/utils/eventstreaming.py +++ b/src/youdotcom/utils/eventstreaming.py @@ -2,7 +2,9 @@ import re import json +from dataclasses import dataclass, asdict from typing import ( + Any, Callable, Generic, TypeVar, @@ -22,6 +24,7 @@ class EventStream(Generic[T]): client_ref: Optional[object] response: httpx.Response generator: Generator[T, None, None] + _closed: bool def __init__( self, @@ -33,17 +36,21 @@ def __init__( self.response = response self.generator = stream_events(response, decoder, sentinel) self.client_ref = client_ref + self._closed = False def __iter__(self): return self def __next__(self): + if self._closed: + raise StopIteration return next(self.generator) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): + self._closed = True self.response.close() @@ -53,6 +60,7 @@ class EventStreamAsync(Generic[T]): client_ref: Optional[object] response: httpx.Response generator: AsyncGenerator[T, None] + _closed: bool def __init__( self, @@ -64,33 +72,45 @@ def __init__( self.response = response self.generator = stream_events_async(response, decoder, sentinel) self.client_ref = client_ref + self._closed = False def __aiter__(self): return self async def __anext__(self): + if self._closed: + raise StopAsyncIteration return await self.generator.__anext__() async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): + self._closed = True await self.response.aclose() +@dataclass class ServerEvent: id: Optional[str] = None event: Optional[str] = None - data: Optional[str] = None + data: Any = None retry: Optional[int] = None MESSAGE_BOUNDARIES = [ b"\r\n\r\n", - b"\n\n", + b"\r\n\r", + b"\r\n\n", + b"\r\r\n", + b"\n\r\n", b"\r\r", + b"\n\r", + b"\n\n", ] +UTF8_BOM = b"\xef\xbb\xbf" + async def stream_events_async( response: httpx.Response, @@ -99,14 +119,10 @@ async def stream_events_async( ) -> AsyncGenerator[T, None]: buffer = bytearray() position = 0 - discard = False + event_id: Optional[str] = None async for chunk in response.aiter_bytes(): - # We've encountered the sentinel value and should no longer process - # incoming data. Instead we throw new data away until the server closes - # the connection. - if discard: - continue - + if len(buffer) == 0 and chunk.startswith(UTF8_BOM): + chunk = chunk[len(UTF8_BOM) :] buffer += chunk for i in range(position, len(buffer)): char = buffer[i : i + 1] @@ -121,15 +137,22 @@ async def stream_events_async( block = buffer[position:i] position = i + len(seq) - event, discard = _parse_event(block, decoder, sentinel) + event, discard, event_id = _parse_event( + raw=block, decoder=decoder, sentinel=sentinel, event_id=event_id + ) if event is not None: yield event + if discard: + await response.aclose() + return if position > 0: buffer = buffer[position:] position = 0 - event, discard = _parse_event(buffer, decoder, sentinel) + event, discard, _ = _parse_event( + raw=buffer, decoder=decoder, sentinel=sentinel, event_id=event_id + ) if event is not None: yield event @@ -141,14 +164,10 @@ def stream_events( ) -> Generator[T, None, None]: buffer = bytearray() position = 0 - discard = False + event_id: Optional[str] = None for chunk in response.iter_bytes(): - # We've encountered the sentinel value and should no longer process - # incoming data. Instead we throw new data away until the server closes - # the connection. - if discard: - continue - + if len(buffer) == 0 and chunk.startswith(UTF8_BOM): + chunk = chunk[len(UTF8_BOM) :] buffer += chunk for i in range(position, len(buffer)): char = buffer[i : i + 1] @@ -163,22 +182,33 @@ def stream_events( block = buffer[position:i] position = i + len(seq) - event, discard = _parse_event(block, decoder, sentinel) + event, discard, event_id = _parse_event( + raw=block, decoder=decoder, sentinel=sentinel, event_id=event_id + ) if event is not None: yield event + if discard: + response.close() + return if position > 0: buffer = buffer[position:] position = 0 - event, discard = _parse_event(buffer, decoder, sentinel) + event, discard, _ = _parse_event( + raw=buffer, decoder=decoder, sentinel=sentinel, event_id=event_id + ) if event is not None: yield event def _parse_event( - raw: bytearray, decoder: Callable[[str], T], sentinel: Optional[str] = None -) -> Tuple[Optional[T], bool]: + *, + raw: bytearray, + decoder: Callable[[str], T], + sentinel: Optional[str] = None, + event_id: Optional[str] = None, +) -> Tuple[Optional[T], bool, Optional[str]]: block = raw.decode() lines = re.split(r"\r?\n|\r", block) publish = False @@ -189,13 +219,16 @@ def _parse_event( continue delim = line.find(":") - if delim <= 0: + if delim == 0: continue - field = line[0:delim] - value = line[delim + 1 :] if delim < len(line) - 1 else "" - if len(value) and value[0] == " ": - value = value[1:] + field = line + value = "" + if delim > 0: + field = line[0:delim] + value = line[delim + 1 :] if delim < len(line) - 1 else "" + if len(value) and value[0] == " ": + value = value[1:] if field == "event": event.event = value @@ -204,37 +237,36 @@ def _parse_event( data += value + "\n" publish = True elif field == "id": - event.id = value publish = True + if "\x00" not in value: + event_id = value elif field == "retry": - event.retry = int(value) if value.isdigit() else None + if value.isdigit(): + event.retry = int(value) publish = True + event.id = event_id + if sentinel and data == f"{sentinel}\n": - return None, True + return None, True, event_id if data: data = data[:-1] - event.data = data - - data_is_primitive = ( - data.isnumeric() or data == "true" or data == "false" or data == "null" - ) - data_is_json = ( - data.startswith("{") or data.startswith("[") or data.startswith('"') - ) - - if data_is_primitive or data_is_json: - try: - event.data = json.loads(data) - except Exception: - pass + try: + event.data = json.loads(data) + except json.JSONDecodeError: + event.data = data out = None if publish: - out = decoder(json.dumps(event.__dict__)) - - return out, False + out_dict = { + k: v + for k, v in asdict(event).items() + if v is not None or (k == "data" and data) + } + out = decoder(json.dumps(out_dict)) + + return out, False, event_id def _peek_sequence(position: int, buffer: bytearray, sequence: bytes): diff --git a/src/youdotcom/utils/retries.py b/src/youdotcom/utils/retries.py index 88a91b1..af07d4e 100644 --- a/src/youdotcom/utils/retries.py +++ b/src/youdotcom/utils/retries.py @@ -144,12 +144,7 @@ def do_request() -> httpx.Response: if res.status_code == parsed_code: raise TemporaryError(res) - except httpx.ConnectError as exception: - if retries.config.retry_connection_errors: - raise - - raise PermanentError(exception) from exception - except httpx.TimeoutException as exception: + except (httpx.NetworkError, httpx.TimeoutException) as exception: if retries.config.retry_connection_errors: raise @@ -193,12 +188,7 @@ async def do_request() -> httpx.Response: if res.status_code == parsed_code: raise TemporaryError(res) - except httpx.ConnectError as exception: - if retries.config.retry_connection_errors: - raise - - raise PermanentError(exception) from exception - except httpx.TimeoutException as exception: + except (httpx.NetworkError, httpx.TimeoutException) as exception: if retries.config.retry_connection_errors: raise diff --git a/src/youdotcom/utils/security.py b/src/youdotcom/utils/security.py index ee42de6..e51915d 100644 --- a/src/youdotcom/utils/security.py +++ b/src/youdotcom/utils/security.py @@ -153,6 +153,8 @@ def _parse_security_scheme_value( elif scheme_type == "http": if sub_type == "bearer": headers[header_name] = _apply_bearer(value) + elif sub_type == "basic": + headers[header_name] = value elif sub_type == "custom": return else: diff --git a/tests/mockserver/.gitignore b/tests/mockserver/.gitignore index 9544318..a7db7f2 100644 --- a/tests/mockserver/.gitignore +++ b/tests/mockserver/.gitignore @@ -1 +1,2 @@ _debug +mockserver diff --git a/tests/mockserver/internal/handler/generated_handlers.go b/tests/mockserver/internal/handler/generated_handlers.go index 4ab9c94..4aa0eaa 100644 --- a/tests/mockserver/internal/handler/generated_handlers.go +++ b/tests/mockserver/internal/handler/generated_handlers.go @@ -15,5 +15,6 @@ func GeneratedHandlers(ctx context.Context, dir *logging.HTTPFileDirectory, rt * NewGeneratedHandler(ctx, http.MethodGet, "/v1/search", pathGetV1Search(dir, rt)), NewGeneratedHandler(ctx, http.MethodPost, "/v1/agents/runs", pathPostV1AgentsRuns(dir, rt)), NewGeneratedHandler(ctx, http.MethodPost, "/v1/contents", pathPostV1Contents(dir, rt)), + NewGeneratedHandler(ctx, http.MethodPost, "/v1/research", pathPostV1Research(dir, rt)), } } diff --git a/tests/mockserver/internal/handler/pathpostv1agentsruns.go b/tests/mockserver/internal/handler/pathpostv1agentsruns.go index c0af295..0347e0c 100644 --- a/tests/mockserver/internal/handler/pathpostv1agentsruns.go +++ b/tests/mockserver/internal/handler/pathpostv1agentsruns.go @@ -27,6 +27,8 @@ func pathPostV1AgentsRuns(dir *logging.HTTPFileDirectory, rt *tracking.RequestTr testPostV1AgentsRunsUnauthorized(w, req) case "post_/v1/agents/runs-forbidden[0]": testPostV1AgentsRunsForbidden(w, req) + case "post_/v1/agents/runs-bad-request[0]": + testPostV1AgentsRunsBadRequest(w, req) default: dir.HandlerFunc("post_/v1/agents/runs", testPostV1AgentsRunsPostV1AgentsRuns0)(w, req) } @@ -45,6 +47,12 @@ func testPostV1AgentsRunsForbidden(w http.ResponseWriter, req *http.Request) { _, _ = w.Write([]byte(`{"errors":[{"status":"403","code":"forbidden","title":"Forbidden","detail":"API key lacks scope for this path"}]}`)) } +func testPostV1AgentsRunsBadRequest(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`{"detail":"Bad request"}`)) +} + func testPostV1AgentsRunsPostV1AgentsRuns0(w http.ResponseWriter, req *http.Request) { if err := assert.SecurityHeader(req, "X-API-Key", false); err != nil { log.Printf("assertion error: %s\n", err) diff --git a/tests/mockserver/internal/handler/pathpostv1research.go b/tests/mockserver/internal/handler/pathpostv1research.go new file mode 100644 index 0000000..11cd0a0 --- /dev/null +++ b/tests/mockserver/internal/handler/pathpostv1research.go @@ -0,0 +1,125 @@ +package handler + +import ( + "encoding/json" + "fmt" + "io" + "log" + "mockserver/internal/handler/assert" + "mockserver/internal/logging" + "mockserver/internal/tracking" + "net/http" +) + +func pathPostV1Research(dir *logging.HTTPFileDirectory, rt *tracking.RequestTracker) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + test := req.Header.Get("x-speakeasy-test-name") + instanceID := req.Header.Get("x-speakeasy-test-instance-id") + + count := rt.GetRequestCount(test, instanceID) + + switch fmt.Sprintf("%s[%d]", test, count) { + case "post_/v1/research[0]": + dir.HandlerFunc("post_/v1/research", testPostV1ResearchSuccess)(w, req) + case "post_/v1/research-unauthorized[0]": + testPostV1ResearchUnauthorized(w, req) + case "post_/v1/research-forbidden[0]": + testPostV1ResearchForbidden(w, req) + case "post_/v1/research-unprocessable[0]": + testPostV1ResearchUnprocessable(w, req) + case "post_/v1/research-internal-error[0]": + testPostV1ResearchInternalError(w, req) + default: + dir.HandlerFunc("post_/v1/research", testPostV1ResearchSuccess)(w, req) + } + } +} + +func testPostV1ResearchSuccess(w http.ResponseWriter, req *http.Request) { + if err := assert.SecurityHeader(req, "X-API-Key", false); err != nil { + log.Printf("assertion error: %s\n", err) + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + if err := assert.ContentType(req, "application/json", true); err != nil { + log.Printf("assertion error: %s\n", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := assert.HeaderExists(req, "User-Agent"); err != nil { + log.Printf("assertion error: %s\n", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + var requestBody map[string]interface{} + bodyBytes, err := io.ReadAll(req.Body) + if err != nil { + log.Printf("error reading request body: %s\n", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := json.Unmarshal(bodyBytes, &requestBody); err != nil { + log.Printf("error parsing request body: %s\n", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + input, _ := requestBody["input"].(string) + effort, _ := requestBody["research_effort"].(string) + if effort == "" { + effort = "standard" + } + + respBody := map[string]interface{}{ + "output": map[string]interface{}{ + "content": fmt.Sprintf("# Mock Research Response\n\nThis is a mock research response for: %s (effort: %s)\n\nQuantum computing has seen significant advances in recent years.", input, effort), + "content_type": "text", + "sources": []map[string]interface{}{ + { + "url": "https://example.com/research/1", + "title": "Mock Research Source 1", + "snippets": []string{"This is a relevant snippet from source 1."}, + }, + { + "url": "https://example.com/research/2", + "title": "Mock Research Source 2", + "snippets": []string{"This is a relevant snippet from source 2."}, + }, + }, + }, + } + + respBodyBytes, err := json.Marshal(respBody) + if err != nil { + http.Error(w, "Unable to encode response body as JSON: "+err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(respBodyBytes) +} + +func testPostV1ResearchUnauthorized(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"message":"Invalid or expired API key"}`)) +} + +func testPostV1ResearchForbidden(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"message":"Forbidden"}`)) +} + +func testPostV1ResearchUnprocessable(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnprocessableEntity) + _, _ = w.Write([]byte(`{"detail":[{"type":"missing","loc":["body","input"],"msg":"Field required","input":""}]}`)) +} + +func testPostV1ResearchInternalError(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"message":"Internal server error"}`)) +} diff --git a/tests/mockserver/mockserver b/tests/mockserver/mockserver deleted file mode 100755 index 3284ae3..0000000 Binary files a/tests/mockserver/mockserver and /dev/null differ diff --git a/tests/test_client.py b/tests/test_client.py index fb323a5..229e12c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +"""Test utilities for the You.com Python SDK test suite.""" import httpx import uuid diff --git a/tests/test_live.py b/tests/test_live.py index 6122d2c..2093180 100644 --- a/tests/test_live.py +++ b/tests/test_live.py @@ -27,6 +27,8 @@ SearchEffort, ReportVerbosity, AgentRunsBatchResponse, + ResearchEffort, + ResearchResponse, ) @@ -240,6 +242,65 @@ def test_advanced_agent_with_research(self, you_client): assert res.output is not None +class TestLiveResearch: + """Live tests for the Research API (new in 2.3.0).""" + + def test_research_basic(self, you_client): + """Test basic research query.""" + with you_client as you: + res = you.research( + input="What is the capital of France?", + research_effort=ResearchEffort.LITE, + ) + + assert isinstance(res, ResearchResponse) + assert res.output is not None + assert res.output.content is not None + assert len(res.output.content) > 0 + + def test_research_deep_effort(self, you_client): + """Test research with deep effort level.""" + with you_client as you: + res = you.research( + input="Explain the tradeoffs between transformer and SSM architectures", + research_effort=ResearchEffort.DEEP, + ) + + assert isinstance(res, ResearchResponse) + assert res.output is not None + assert res.output.content is not None + assert len(res.output.content) > 0 + + def test_research_exhaustive_effort(self, you_client): + """Test research with exhaustive effort level.""" + with you_client as you: + res = you.research( + input="Compare global approaches to AI regulation across the US, EU, and China", + research_effort=ResearchEffort.EXHAUSTIVE, + ) + + assert isinstance(res, ResearchResponse) + assert res.output is not None + assert res.output.content is not None + assert len(res.output.content) > 0 + + def test_research_with_sources(self, you_client): + """Test research query returns sources.""" + with you_client as you: + res = you.research( + input="What are the benefits of renewable energy?", + research_effort=ResearchEffort.STANDARD, + ) + + assert isinstance(res, ResearchResponse) + assert res.output is not None + assert res.output.content is not None + assert res.output.sources is not None + assert len(res.output.sources) > 0 + for source in res.output.sources: + assert source.url is not None + + if __name__ == "__main__": # Run with: python -m pytest tests/test_live.py -v pytest.main([__file__, "-v"]) diff --git a/tests/test_research.py b/tests/test_research.py new file mode 100644 index 0000000..793a617 --- /dev/null +++ b/tests/test_research.py @@ -0,0 +1,172 @@ +import os +import uuid +import pytest + +import httpx + +from tests.test_client import create_test_http_client +from youdotcom import You +from youdotcom.errors import ( + ResearchForbiddenError, + ResearchInternalServerError, + ResearchUnauthorizedError, + UnprocessableEntityError, + YouDefaultError, +) +from youdotcom.models import ( + ResearchEffort, + ResearchResponse, +) + + +@pytest.fixture +def server_url(): + return os.getenv("TEST_SERVER_URL", "http://localhost:18080") + + +@pytest.fixture +def api_key(): + return os.getenv("YOU_API_KEY_AUTH", "test-api-key") + + +class TestResearchBasic: + def test_basic_research(self, server_url, api_key): + client = create_test_http_client("post_/v1/research") + + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + res = you.research( + input="What are the latest advances in quantum computing?", + research_effort=ResearchEffort.STANDARD, + server_url=server_url, + ) + + assert isinstance(res, ResearchResponse) + assert res.output is not None + assert res.output.content is not None + assert len(res.output.content) > 0 + + def test_research_lite_effort(self, server_url, api_key): + client = create_test_http_client("post_/v1/research") + + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + res = you.research( + input="What is the capital of France?", + research_effort=ResearchEffort.LITE, + server_url=server_url, + ) + + assert isinstance(res, ResearchResponse) + assert res.output is not None + + def test_research_deep_effort(self, server_url, api_key): + client = create_test_http_client("post_/v1/research") + + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + res = you.research( + input="Explain the tradeoffs between transformer and SSM architectures", + research_effort=ResearchEffort.DEEP, + server_url=server_url, + ) + + assert isinstance(res, ResearchResponse) + assert res.output is not None + assert res.output.content is not None + assert len(res.output.content) > 0 + + def test_research_exhaustive_effort(self, server_url, api_key): + client = create_test_http_client("post_/v1/research") + + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + res = you.research( + input="Compare global approaches to AI regulation across the US, EU, and China", + research_effort=ResearchEffort.EXHAUSTIVE, + server_url=server_url, + ) + + assert isinstance(res, ResearchResponse) + assert res.output is not None + assert res.output.content is not None + assert len(res.output.content) > 0 + + def test_research_with_sources(self, server_url, api_key): + client = create_test_http_client("post_/v1/research") + + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + res = you.research( + input="What are the benefits of renewable energy?", + research_effort=ResearchEffort.STANDARD, + server_url=server_url, + ) + + assert isinstance(res, ResearchResponse) + assert res.output is not None + assert res.output.sources is not None + assert len(res.output.sources) > 0 + for source in res.output.sources: + assert source.url is not None + + +class TestResearchAsync: + @pytest.mark.asyncio + async def test_basic_research_async(self, server_url, api_key): + async_client = httpx.AsyncClient( + headers={ + "x-speakeasy-test-name": "post_/v1/research", + "x-speakeasy-test-instance-id": str(uuid.uuid4()), + }, + follow_redirects=True, + ) + + async with You(server_url=server_url, async_client=async_client, api_key_auth=api_key) as you: + res = await you.research_async( + input="What are the latest advances in quantum computing?", + research_effort=ResearchEffort.STANDARD, + server_url=server_url, + ) + + assert isinstance(res, ResearchResponse) + assert res.output is not None + assert res.output.content is not None + assert len(res.output.content) > 0 + + +class TestResearchErrors: + def test_unauthorized(self, server_url): + client = create_test_http_client("post_/v1/research-unauthorized") + + with You(server_url=server_url, client=client, api_key_auth="invalid") as you: + with pytest.raises((ResearchUnauthorizedError, YouDefaultError)): + you.research( + input="test", + server_url=server_url, + ) + + def test_forbidden(self, server_url, api_key): + client = create_test_http_client("post_/v1/research-forbidden") + + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + with pytest.raises((ResearchForbiddenError, YouDefaultError)): + you.research( + input="test", + server_url=server_url, + ) + + def test_unprocessable_entity(self, server_url, api_key): + client = create_test_http_client("post_/v1/research-unprocessable") + + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + with pytest.raises((UnprocessableEntityError, YouDefaultError)): + you.research( + input="", + server_url=server_url, + ) + + def test_internal_server_error(self, server_url, api_key): + client = create_test_http_client("post_/v1/research-internal-error") + + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + with pytest.raises((ResearchInternalServerError, YouDefaultError)): + you.research( + input="test", + server_url=server_url, + ) diff --git a/tests/test_runs.py b/tests/test_runs.py index 4d91416..0bd43ba 100644 --- a/tests/test_runs.py +++ b/tests/test_runs.py @@ -4,8 +4,9 @@ from tests.test_client import create_test_http_client from youdotcom import You from youdotcom.errors import ( + AgentRuns400ResponseError, AgentRuns401ResponseError, - AgentRuns422ResponseError, + YouDefaultError, ) from youdotcom.models import ( ComputeTool, @@ -218,7 +219,20 @@ def test_forbidden(self, server_url, api_key): with You(server_url=server_url, client=client, api_key_auth=api_key) as you: # Mock server returns 403 which gets caught as a default error # In production API, this would be a more specific error type - with pytest.raises(Exception): # Accept any exception for mock server + with pytest.raises(YouDefaultError): + you.agents.runs.create( + request=ExpressAgentRunsRequest( + input="test", + stream=False, + ), + server_url=server_url, + ) + + def test_bad_request(self, server_url, api_key): + client = create_test_http_client("post_/v1/agents/runs-bad-request") + + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + with pytest.raises((AgentRuns400ResponseError, YouDefaultError)): you.agents.runs.create( request=ExpressAgentRunsRequest( input="test", diff --git a/uv.lock b/uv.lock index 57790e6..48cdd68 100644 --- a/uv.lock +++ b/uv.lock @@ -1,11 +1,10 @@ version = 1 revision = 3 -requires-python = ">=3.9.2" +requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", - "python_full_version < '3.10'", + "python_full_version < '3.11'", ] [[package]] @@ -181,12 +180,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541, upload-time = "2025-02-05T03:49:57.623Z" }, { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348, upload-time = "2025-02-05T03:48:52.361Z" }, { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648, upload-time = "2025-02-05T03:49:11.395Z" }, - { url = "https://files.pythonhosted.org/packages/5a/fa/79cf41a55b682794abe71372151dbbf856e3008f6767057229e6649d294a/mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078", size = 10737129, upload-time = "2025-02-05T03:50:24.509Z" }, - { url = "https://files.pythonhosted.org/packages/d3/33/dd8feb2597d648de29e3da0a8bf4e1afbda472964d2a4a0052203a6f3594/mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba", size = 9856335, upload-time = "2025-02-05T03:49:36.398Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b5/74508959c1b06b96674b364ffeb7ae5802646b32929b7701fc6b18447592/mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5", size = 11611935, upload-time = "2025-02-05T03:49:14.154Z" }, - { url = "https://files.pythonhosted.org/packages/6c/53/da61b9d9973efcd6507183fdad96606996191657fe79701b2c818714d573/mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b", size = 12365827, upload-time = "2025-02-05T03:48:59.458Z" }, - { url = "https://files.pythonhosted.org/packages/c1/72/965bd9ee89540c79a25778cc080c7e6ef40aa1eeac4d52cec7eae6eb5228/mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2", size = 12541924, upload-time = "2025-02-05T03:50:03.12Z" }, - { url = "https://files.pythonhosted.org/packages/46/d0/f41645c2eb263e6c77ada7d76f894c580c9ddb20d77f0c24d34273a4dab2/mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980", size = 9271176, upload-time = "2025-02-05T03:50:10.86Z" }, { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777, upload-time = "2025-02-05T03:50:08.348Z" }, ] @@ -208,27 +201,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] -[[package]] -name = "platformdirs" -version = "4.4.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, -] - [[package]] name = "platformdirs" version = "4.5.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, @@ -341,19 +317,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/54/db/160dffb57ed9a3705c4cbcbff0ac03bdae45f1ca7d58ab74645550df3fbd/pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", size = 2107999, upload-time = "2025-11-04T13:42:03.885Z" }, - { url = "https://files.pythonhosted.org/packages/a3/7d/88e7de946f60d9263cc84819f32513520b85c0f8322f9b8f6e4afc938383/pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", size = 1929745, upload-time = "2025-11-04T13:42:06.075Z" }, - { url = "https://files.pythonhosted.org/packages/d5/c2/aef51e5b283780e85e99ff19db0f05842d2d4a8a8cd15e63b0280029b08f/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", size = 1920220, upload-time = "2025-11-04T13:42:08.457Z" }, - { url = "https://files.pythonhosted.org/packages/c7/97/492ab10f9ac8695cd76b2fdb24e9e61f394051df71594e9bcc891c9f586e/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", size = 2067296, upload-time = "2025-11-04T13:42:10.817Z" }, - { url = "https://files.pythonhosted.org/packages/ec/23/984149650e5269c59a2a4c41d234a9570adc68ab29981825cfaf4cfad8f4/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", size = 2231548, upload-time = "2025-11-04T13:42:13.843Z" }, - { url = "https://files.pythonhosted.org/packages/71/0c/85bcbb885b9732c28bec67a222dbed5ed2d77baee1f8bba2002e8cd00c5c/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", size = 2362571, upload-time = "2025-11-04T13:42:16.208Z" }, - { url = "https://files.pythonhosted.org/packages/c0/4a/412d2048be12c334003e9b823a3fa3d038e46cc2d64dd8aab50b31b65499/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", size = 2068175, upload-time = "2025-11-04T13:42:18.911Z" }, - { url = "https://files.pythonhosted.org/packages/73/f4/c58b6a776b502d0a5540ad02e232514285513572060f0d78f7832ca3c98b/pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", size = 2177203, upload-time = "2025-11-04T13:42:22.578Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ae/f06ea4c7e7a9eead3d165e7623cd2ea0cb788e277e4f935af63fc98fa4e6/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", size = 2148191, upload-time = "2025-11-04T13:42:24.89Z" }, - { url = "https://files.pythonhosted.org/packages/c1/57/25a11dcdc656bf5f8b05902c3c2934ac3ea296257cc4a3f79a6319e61856/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", size = 2343907, upload-time = "2025-11-04T13:42:27.683Z" }, - { url = "https://files.pythonhosted.org/packages/96/82/e33d5f4933d7a03327c0c43c65d575e5919d4974ffc026bc917a5f7b9f61/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", size = 2322174, upload-time = "2025-11-04T13:42:30.776Z" }, - { url = "https://files.pythonhosted.org/packages/81/45/4091be67ce9f469e81656f880f3506f6a5624121ec5eb3eab37d7581897d/pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", size = 1990353, upload-time = "2025-11-04T13:42:33.111Z" }, - { url = "https://files.pythonhosted.org/packages/44/8a/a98aede18db6e9cd5d66bcacd8a409fcf8134204cdede2e7de35c5a2c5ef/pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", size = 2015698, upload-time = "2025-11-04T13:42:35.484Z" }, { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, @@ -390,11 +353,9 @@ dependencies = [ { name = "dill" }, { name = "isort" }, { name = "mccabe" }, - { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "platformdirs" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tomlkit" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9a/e9/60280b14cc1012794120345ce378504cf17409e38cd88f455dc24e0ad6b5/pylint-3.2.3.tar.gz", hash = "sha256:02f6c562b215582386068d52a30f520d84fdbcf2a95fc7e855b816060d048b60", size = 1506739, upload-time = "2024-06-06T14:19:17.955Z" } wheels = [ @@ -495,7 +456,7 @@ wheels = [ [[package]] name = "youdotcom" -version = "2.2.0" +version = "2.3.0" source = { editable = "." } dependencies = [ { name = "httpcore" },