diff --git a/.fern/metadata.json b/.fern/metadata.json index 59b2e79..3bb3596 100644 --- a/.fern/metadata.json +++ b/.fern/metadata.json @@ -1,9 +1,10 @@ { - "cliVersion": "3.5.0", + "cliVersion": "3.39.0", "generatorName": "fernapi/fern-python-sdk", - "generatorVersion": "4.38.4", + "generatorVersion": "4.46.14", "generatorConfig": { "client_class_name": "Lattice", "package_name": "anduril" - } + }, + "sdkVersion": "5.0.0" } \ No newline at end of file diff --git a/README.md b/README.md index fce708c..67135ce 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ The Lattice SDK Python library provides convenient access to the Lattice SDK API - [Exception Handling](#exception-handling) - [Streaming](#streaming) - [Pagination](#pagination) +- [Oauth Token Override](#oauth-token-override) - [Advanced](#advanced) - [Access Raw Response Data](#access-raw-response-data) - [Retries](#retries) @@ -56,7 +57,8 @@ Instantiate and use the client with the following: from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.entities.long_poll_entity_events( session_token="sessionToken", @@ -73,7 +75,8 @@ import asyncio from anduril import AsyncLattice client = AsyncLattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) @@ -109,7 +112,8 @@ The SDK supports streaming responses, as well, the response will be a generator from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) response = client.entities.stream_entities() for chunk in response.data: @@ -124,7 +128,8 @@ Paginated requests will return a `SyncPager` or `AsyncPager`, which can be used from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) response = client.objects.list_objects() for item in response: @@ -143,6 +148,24 @@ for page in pager.iter_pages(): print(item) ``` +## Oauth Token Override + +This SDK supports two authentication methods: OAuth client credentials flow (automatic token management) or direct bearer token authentication. You can choose between these options when initializing the client: + +```python +from anduril import Lattice + +# Option 1: Direct bearer token (bypass OAuth flow) +client = Lattice(..., token="my-pre-generated-bearer-token") + +from anduril import Lattice + +# Option 2: OAuth client credentials flow (automatic token management) +client = Lattice( + ..., client_id="your-client-id", client_secret="your-client-secret" +) +``` + ## Advanced ### Access Raw Response Data diff --git a/poetry.lock b/poetry.lock index 9decbf8..ea0fda4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -38,13 +38,13 @@ trio = ["trio (>=0.26.1)"] [[package]] name = "certifi" -version = "2025.11.12" +version = "2026.1.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" files = [ - {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"}, - {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, + {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, + {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, ] [[package]] @@ -517,53 +517,58 @@ files = [ [[package]] name = "tomli" -version = "2.3.0" +version = "2.4.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, - {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, - {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, - {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, - {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, - {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, - {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, - {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, - {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, - {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, - {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, - {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, - {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, - {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, + {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, + {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, + {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, + {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, + {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, + {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, + {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, + {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, + {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, + {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, + {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, + {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, + {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, + {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, + {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, + {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, + {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 60f47db..50965ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,10 @@ [project] name = "anduril-lattice-sdk" +dynamic = ["version"] [tool.poetry] name = "anduril-lattice-sdk" -version = "4.0.0" +version = "5.0.0" description = "HTTP clients for the Anduril Lattice SDK" readme = "README.md" authors = [ diff --git a/reference.md b/reference.md index 343a9e3..f6472fc 100644 --- a/reference.md +++ b/reference.md @@ -1,6 +1,6 @@ # Reference ## Entities -
client.entities.publish_entity(...) +
client.entities.publish_entity(...) -> AsyncHttpResponse[Entity]
@@ -36,7 +36,8 @@ provenance.sourceUpdateTime is greater than the provenance.sourceUpdateTime of t from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.entities.publish_entity() @@ -404,7 +405,7 @@ Describes an entity's security classification levels at an overall classificatio
-
client.entities.get_entity(...) +
client.entities.get_entity(...) -> AsyncHttpResponse[Entity]
@@ -420,7 +421,8 @@ Describes an entity's security classification levels at an overall classificatio from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.entities.get_entity( entity_id="entityId", @@ -460,7 +462,7 @@ client.entities.get_entity(
-
client.entities.override_entity(...) +
client.entities.override_entity(...) -> AsyncHttpResponse[Entity]
@@ -496,7 +498,8 @@ concurrently for the same field path, the last writer wins. from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.entities.override_entity( entity_id="entityId", @@ -564,7 +567,7 @@ the object and ignore all other fields.
-
client.entities.remove_entity_override(...) +
client.entities.remove_entity_override(...) -> AsyncHttpResponse[Entity]
@@ -594,7 +597,8 @@ This operation clears the override value from the specified field path on the en from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.entities.remove_entity_override( entity_id="entityId", @@ -643,7 +647,7 @@ client.entities.remove_entity_override(
-
client.entities.long_poll_entity_events(...) +
client.entities.long_poll_entity_events(...) -> AsyncHttpResponse[EntityEventResponse]
@@ -681,7 +685,8 @@ In this case you must start a new session by sending a request with an empty ses from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.entities.long_poll_entity_events( session_token="sessionToken", @@ -729,7 +734,9 @@ client.entities.long_poll_entity_events(
-
client.entities.stream_entities(...) +
client.entities.stream_entities(...) -> typing.AsyncIterator[ + AsyncHttpResponse[typing.AsyncIterator[StreamEntitiesResponse]] +]
@@ -777,7 +784,8 @@ this provides real-time updates with minimal latency and reduced server load. from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) response = client.entities.stream_entities() for chunk in response.data: @@ -834,7 +842,7 @@ for chunk in response.data:
## Tasks -
client.tasks.create_task(...) +
client.tasks.create_task(...) -> AsyncHttpResponse[Task]
@@ -871,7 +879,8 @@ through other Tasks API endpoints. from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.tasks.create_task() @@ -978,7 +987,7 @@ task. For example, an entity Objective, an entity Keep In Zone, etc.
-
client.tasks.get_task(...) +
client.tasks.get_task(...) -> AsyncHttpResponse[Task]
@@ -1015,7 +1024,8 @@ perspective. from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.tasks.get_task( task_id="taskId", @@ -1055,7 +1065,7 @@ client.tasks.get_task(
-
client.tasks.update_task_status(...) +
client.tasks.update_task_status(...) -> AsyncHttpResponse[Task]
@@ -1095,7 +1105,8 @@ reaches these states, no further updates are allowed. from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.tasks.update_task_status( task_id="taskId", @@ -1164,7 +1175,7 @@ is known are considered stale and ignored.
-
client.tasks.query_tasks(...) +
client.tasks.query_tasks(...) -> AsyncHttpResponse[TaskQueryResults]
@@ -1208,7 +1219,8 @@ By default, this returns the latest task version for each matching task from the from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.tasks.query_tasks() @@ -1274,7 +1286,7 @@ any of the remaining parameters, but not both.
-
client.tasks.listen_as_agent(...) +
client.tasks.listen_as_agent(...) -> AsyncHttpResponse[AgentRequest]
@@ -1323,7 +1335,8 @@ period you will be expected to reinitiate a new request. from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.tasks.listen_as_agent() @@ -1362,7 +1375,7 @@ client.tasks.listen_as_agent()
## Objects -
client.objects.list_objects(...) +
client.objects.list_objects(...) -> AsyncPager[PathMetadata, ListResponse]
@@ -1392,7 +1405,8 @@ Lists objects in your environment. You can define a prefix to list a subset of y from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) response = client.objects.list_objects() for item in response: @@ -1459,7 +1473,7 @@ for page in response.iter_pages():
-
client.objects.get_object(...) +
client.objects.get_object(...) -> typing.AsyncIterator[AsyncHttpResponse[typing.AsyncIterator[bytes]]]
@@ -1489,7 +1503,8 @@ Fetches an object from your environment using the objectPath path parameter. from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.objects.get_object( object_path="objectPath", @@ -1545,7 +1560,84 @@ client.objects.get_object(
-
client.objects.delete_object(...) +
client.objects.upload_object(...) -> AsyncHttpResponse[PathMetadata] +
+
+ +#### πŸ“ Description + +
+
+ +
+
+ +Uploads an object. The object must be 1 GiB or smaller. +
+
+
+
+ +#### πŸ”Œ Usage + +
+
+ +
+
+ +```python +from anduril import Lattice + +client = Lattice( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", +) +client.objects.upload_object() + +``` +
+
+
+
+ +#### βš™οΈ Parameters + +
+
+ +
+
+ +**object_path:** `str` β€” Path of the Object that is to be uploaded. + +
+
+ +
+
+ +**request:** `typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` β€” Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.objects.delete_object(...) -> AsyncHttpResponse[None]
@@ -1575,7 +1667,8 @@ Deletes an object from your environment given the objectPath path parameter. from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.objects.delete_object( object_path="objectPath", @@ -1615,7 +1708,7 @@ client.objects.delete_object(
-
client.objects.get_object_metadata(...) +
client.objects.get_object_metadata(...) -> AsyncHttpResponse[None]
@@ -1645,7 +1738,8 @@ Returns metadata for a specified object path. Use this to fetch metadata such as from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.objects.get_object_metadata( object_path="objectPath", @@ -1685,3 +1779,81 @@ client.objects.get_object_metadata(
+## oauth +
client.oauth.get_token(...) -> AsyncHttpResponse[GetTokenResponse] +
+
+ +#### πŸ“ Description + +
+
+ +
+
+ +Gets a new short-lived token using the specified client credentials +
+
+
+
+ +#### πŸ”Œ Usage + +
+
+ +
+
+ +```python +from anduril import Lattice + +client = Lattice( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", +) +client.oauth.get_token() + +``` +
+
+
+
+ +#### βš™οΈ Parameters + +
+
+ +
+
+ +**client_id:** `typing.Optional[str]` β€” The client identifier + +
+
+ +
+
+ +**client_secret:** `typing.Optional[str]` β€” The client secret + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` β€” Request-specific configuration. + +
+
+
+
+ + +
+
+
+ diff --git a/src/anduril/__init__.py b/src/anduril/__init__.py index 7410936..4b46332 100644 --- a/src/anduril/__init__.py +++ b/src/anduril/__init__.py @@ -12,6 +12,7 @@ ActiveTarget, Agent, AgentRequest, + AgentTaskRequest, Alert, AlertCondition, AlertLevel, @@ -20,6 +21,7 @@ AlternateId, AlternateIdType, AngleOfArrival, + BadRequestErrorBody, Bandwidth, BandwidthRange, CancelRequest, @@ -185,6 +187,7 @@ TransponderCodes, TransponderCodesMode4InterrogationResponse, UInt32Range, + UnauthorizedErrorBody, User, VisualDetails, ) @@ -198,10 +201,11 @@ TooManyRequestsError, UnauthorizedError, ) - from . import entities, entity, object, objects, task, tasks + from . import entities, entity, oauth, object, objects, task, tasks from .client import AsyncLattice, Lattice from .entities import StreamEntitiesResponse, StreamEntitiesResponse_Entity, StreamEntitiesResponse_Heartbeat from .environment import LatticeEnvironment + from .oauth import GetTokenResponse from .objects import GetObjectRequestAcceptEncoding from .tasks import TaskQueryStatusFilter, TaskQueryStatusFilterStatus, TaskQueryUpdateTimeRange from .version import __version__ @@ -211,6 +215,7 @@ "ActiveTarget": ".types", "Agent": ".types", "AgentRequest": ".types", + "AgentTaskRequest": ".types", "Alert": ".types", "AlertCondition": ".types", "AlertLevel": ".types", @@ -221,6 +226,7 @@ "AngleOfArrival": ".types", "AsyncLattice": ".client", "BadRequestError": ".errors", + "BadRequestErrorBody": ".types", "Bandwidth": ".types", "BandwidthRange": ".types", "CancelRequest": ".types", @@ -279,6 +285,7 @@ "GeoPolygonPosition": ".types", "GeoShape": ".types", "GetObjectRequestAcceptEncoding": ".objects", + "GetTokenResponse": ".oauth", "GoogleProtobufAny": ".types", "GroupChild": ".types", "GroupDetails": ".types", @@ -402,11 +409,13 @@ "TransponderCodesMode4InterrogationResponse": ".types", "UInt32Range": ".types", "UnauthorizedError": ".errors", + "UnauthorizedErrorBody": ".types", "User": ".types", "VisualDetails": ".types", "__version__": ".version", "entities": ".entities", "entity": ".entity", + "oauth": ".oauth", "object": ".object", "objects": ".objects", "task": ".task", @@ -441,6 +450,7 @@ def __dir__(): "ActiveTarget", "Agent", "AgentRequest", + "AgentTaskRequest", "Alert", "AlertCondition", "AlertLevel", @@ -451,6 +461,7 @@ def __dir__(): "AngleOfArrival", "AsyncLattice", "BadRequestError", + "BadRequestErrorBody", "Bandwidth", "BandwidthRange", "CancelRequest", @@ -509,6 +520,7 @@ def __dir__(): "GeoPolygonPosition", "GeoShape", "GetObjectRequestAcceptEncoding", + "GetTokenResponse", "GoogleProtobufAny", "GroupChild", "GroupDetails", @@ -632,11 +644,13 @@ def __dir__(): "TransponderCodesMode4InterrogationResponse", "UInt32Range", "UnauthorizedError", + "UnauthorizedErrorBody", "User", "VisualDetails", "__version__", "entities", "entity", + "oauth", "object", "objects", "task", diff --git a/src/anduril/client.py b/src/anduril/client.py index 64ef0fd..3db7e78 100644 --- a/src/anduril/client.py +++ b/src/anduril/client.py @@ -5,11 +5,14 @@ import typing import httpx +from .core.api_error import ApiError from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .core.oauth_token_provider import AsyncOAuthTokenProvider, OAuthTokenProvider from .environment import LatticeEnvironment if typing.TYPE_CHECKING: from .entities.client import AsyncEntitiesClient, EntitiesClient + from .oauth.client import AsyncOauthClient, OauthClient from .objects.client import AsyncObjectsClient, ObjectsClient from .tasks.client import AsyncTasksClient, TasksClient @@ -20,21 +23,32 @@ class Lattice: Parameters ---------- + base_url : typing.Optional[str] The base url to use for requests from the client. - environment : LatticeEnvironment - The environment to use for requests from the client. from .environment import LatticeEnvironment + client_id : str + The client identifier used for authentication. + client_secret : str + The client secret used for authentication. + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. - Defaults to LatticeEnvironment.DEFAULT + httpx_client : typing.Optional[httpx.Client] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + # or ... + base_url : typing.Optional[str] + The base url to use for requests from the client. - token : typing.Union[str, typing.Callable[[], str]] - headers : typing.Optional[typing.Dict[str, str]] - Additional headers to send with every request. + token : typing.Callable[[], str] + Authenticate by providing a callable that returns a pre-generated bearer token. In this mode, OAuth client credentials are not required. timeout : typing.Optional[float] The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. @@ -50,38 +64,106 @@ class Lattice: from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + ) + + # or ... + + from anduril import Lattice + + client = Lattice( + base_url="https://yourhost.com/path/to/api", + token="YOUR_BEARER_TOKEN", ) """ + @typing.overload + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: LatticeEnvironment = LatticeEnvironment.DEFAULT, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.Client] = None, + client_id: str, + client_secret: str, + ): ... + @typing.overload def __init__( self, *, base_url: typing.Optional[str] = None, environment: LatticeEnvironment = LatticeEnvironment.DEFAULT, - token: typing.Union[str, typing.Callable[[], str]], headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, + token: typing.Callable[[], str], + ): ... + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: LatticeEnvironment = LatticeEnvironment.DEFAULT, + headers: typing.Optional[typing.Dict[str, str]] = None, + client_id: typing.Optional[str] = None, + client_secret: typing.Optional[str] = None, + token: typing.Optional[typing.Callable[[], str]] = None, + _token_getter_override: typing.Optional[typing.Callable[[], str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.Client] = None, ): _defaulted_timeout = ( timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read ) - self._client_wrapper = SyncClientWrapper( - base_url=_get_base_url(base_url=base_url, environment=environment), - token=token, - headers=headers, - httpx_client=httpx_client - if httpx_client is not None - else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) - if follow_redirects is not None - else httpx.Client(timeout=_defaulted_timeout), - timeout=_defaulted_timeout, - ) + if token is not None: + self._client_wrapper = SyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.Client(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + token=_token_getter_override if _token_getter_override is not None else token, + ) + elif client_id is not None and client_secret is not None: + oauth_token_provider = OAuthTokenProvider( + client_id=client_id, + client_secret=client_secret, + client_wrapper=SyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + headers=headers, + httpx_client=httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.Client(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ), + ) + self._client_wrapper = SyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + headers=headers, + token=_token_getter_override if _token_getter_override is not None else oauth_token_provider.get_token, + httpx_client=httpx_client + if httpx_client is not None + else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.Client(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + else: + raise ApiError( + body="The client must be instantiated with either 'token' or both 'client_id' and 'client_secret'" + ) self._entities: typing.Optional[EntitiesClient] = None self._tasks: typing.Optional[TasksClient] = None self._objects: typing.Optional[ObjectsClient] = None + self._oauth: typing.Optional[OauthClient] = None @property def entities(self): @@ -107,6 +189,14 @@ def objects(self): self._objects = ObjectsClient(client_wrapper=self._client_wrapper) return self._objects + @property + def oauth(self): + if self._oauth is None: + from .oauth.client import OauthClient # noqa: E402 + + self._oauth = OauthClient(client_wrapper=self._client_wrapper) + return self._oauth + class AsyncLattice: """ @@ -114,21 +204,32 @@ class AsyncLattice: Parameters ---------- + base_url : typing.Optional[str] The base url to use for requests from the client. - environment : LatticeEnvironment - The environment to use for requests from the client. from .environment import LatticeEnvironment + client_id : str + The client identifier used for authentication. + client_secret : str + The client secret used for authentication. + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. - Defaults to LatticeEnvironment.DEFAULT + httpx_client : typing.Optional[httpx.AsyncClient] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + # or ... + base_url : typing.Optional[str] + The base url to use for requests from the client. - token : typing.Union[str, typing.Callable[[], str]] - headers : typing.Optional[typing.Dict[str, str]] - Additional headers to send with every request. + token : typing.Callable[[], str] + Authenticate by providing a callable that returns a pre-generated bearer token. In this mode, OAuth client credentials are not required. timeout : typing.Optional[float] The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. @@ -144,38 +245,107 @@ class AsyncLattice: from anduril import AsyncLattice client = AsyncLattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + ) + + # or ... + + from anduril import AsyncLattice + + client = AsyncLattice( + base_url="https://yourhost.com/path/to/api", + token="YOUR_BEARER_TOKEN", ) """ + @typing.overload + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: LatticeEnvironment = LatticeEnvironment.DEFAULT, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.AsyncClient] = None, + client_id: str, + client_secret: str, + ): ... + @typing.overload def __init__( self, *, base_url: typing.Optional[str] = None, environment: LatticeEnvironment = LatticeEnvironment.DEFAULT, - token: typing.Union[str, typing.Callable[[], str]], headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, + token: typing.Callable[[], str], + ): ... + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: LatticeEnvironment = LatticeEnvironment.DEFAULT, + headers: typing.Optional[typing.Dict[str, str]] = None, + client_id: typing.Optional[str] = None, + client_secret: typing.Optional[str] = None, + token: typing.Optional[typing.Callable[[], str]] = None, + _token_getter_override: typing.Optional[typing.Callable[[], str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.AsyncClient] = None, ): _defaulted_timeout = ( timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read ) - self._client_wrapper = AsyncClientWrapper( - base_url=_get_base_url(base_url=base_url, environment=environment), - token=token, - headers=headers, - httpx_client=httpx_client - if httpx_client is not None - else httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects) - if follow_redirects is not None - else httpx.AsyncClient(timeout=_defaulted_timeout), - timeout=_defaulted_timeout, - ) + if token is not None: + self._client_wrapper = AsyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.AsyncClient(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + token=_token_getter_override if _token_getter_override is not None else token, + ) + elif client_id is not None and client_secret is not None: + oauth_token_provider = AsyncOAuthTokenProvider( + client_id=client_id, + client_secret=client_secret, + client_wrapper=AsyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + headers=headers, + httpx_client=httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.AsyncClient(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ), + ) + self._client_wrapper = AsyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + headers=headers, + token=_token_getter_override, + async_token=oauth_token_provider.get_token, + httpx_client=httpx_client + if httpx_client is not None + else httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.AsyncClient(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + else: + raise ApiError( + body="The client must be instantiated with either 'token' or both 'client_id' and 'client_secret'" + ) self._entities: typing.Optional[AsyncEntitiesClient] = None self._tasks: typing.Optional[AsyncTasksClient] = None self._objects: typing.Optional[AsyncObjectsClient] = None + self._oauth: typing.Optional[AsyncOauthClient] = None @property def entities(self): @@ -201,6 +371,14 @@ def objects(self): self._objects = AsyncObjectsClient(client_wrapper=self._client_wrapper) return self._objects + @property + def oauth(self): + if self._oauth is None: + from .oauth.client import AsyncOauthClient # noqa: E402 + + self._oauth = AsyncOauthClient(client_wrapper=self._client_wrapper) + return self._oauth + def _get_base_url(*, base_url: typing.Optional[str] = None, environment: LatticeEnvironment) -> str: if base_url is not None: diff --git a/src/anduril/core/client_wrapper.py b/src/anduril/core/client_wrapper.py index d1cefd3..9173c04 100644 --- a/src/anduril/core/client_wrapper.py +++ b/src/anduril/core/client_wrapper.py @@ -10,7 +10,7 @@ class BaseClientWrapper: def __init__( self, *, - token: typing.Union[str, typing.Callable[[], str]], + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, headers: typing.Optional[typing.Dict[str, str]] = None, base_url: str, timeout: typing.Optional[float] = None, @@ -22,17 +22,19 @@ def __init__( def get_headers(self) -> typing.Dict[str, str]: headers: typing.Dict[str, str] = { - "User-Agent": "anduril-lattice-sdk/4.0.0", + "User-Agent": "anduril-lattice-sdk/5.0.0", "X-Fern-Language": "Python", "X-Fern-SDK-Name": "anduril-lattice-sdk", - "X-Fern-SDK-Version": "4.0.0", + "X-Fern-SDK-Version": "5.0.0", **(self.get_custom_headers() or {}), } - headers["Authorization"] = f"Bearer {self._get_token()}" + token = self._get_token() + if token is not None: + headers["Authorization"] = f"Bearer {token}" return headers - def _get_token(self) -> str: - if isinstance(self._token, str): + def _get_token(self) -> typing.Optional[str]: + if isinstance(self._token, str) or self._token is None: return self._token else: return self._token() @@ -51,7 +53,7 @@ class SyncClientWrapper(BaseClientWrapper): def __init__( self, *, - token: typing.Union[str, typing.Callable[[], str]], + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, headers: typing.Optional[typing.Dict[str, str]] = None, base_url: str, timeout: typing.Optional[float] = None, @@ -70,16 +72,26 @@ class AsyncClientWrapper(BaseClientWrapper): def __init__( self, *, - token: typing.Union[str, typing.Callable[[], str]], + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, headers: typing.Optional[typing.Dict[str, str]] = None, base_url: str, timeout: typing.Optional[float] = None, + async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, httpx_client: httpx.AsyncClient, ): super().__init__(token=token, headers=headers, base_url=base_url, timeout=timeout) + self._async_token = async_token self.httpx_client = AsyncHttpClient( httpx_client=httpx_client, base_headers=self.get_headers, base_timeout=self.get_timeout, base_url=self.get_base_url, + async_base_headers=self.async_get_headers, ) + + async def async_get_headers(self) -> typing.Dict[str, str]: + headers = self.get_headers() + if self._async_token is not None: + token = await self._async_token() + headers["Authorization"] = f"Bearer {token}" + return headers diff --git a/src/anduril/core/http_client.py b/src/anduril/core/http_client.py index e4173f9..7c6c936 100644 --- a/src/anduril/core/http_client.py +++ b/src/anduril/core/http_client.py @@ -5,7 +5,6 @@ import re import time import typing -import urllib.parse from contextlib import asynccontextmanager, contextmanager from random import random @@ -14,13 +13,13 @@ from .force_multipart import FORCE_MULTIPART from .jsonable_encoder import jsonable_encoder from .query_encoder import encode_query -from .remove_none_from_dict import remove_none_from_dict +from .remove_none_from_dict import remove_none_from_dict as remove_none_from_dict from .request_options import RequestOptions from httpx._types import RequestFiles -INITIAL_RETRY_DELAY_SECONDS = 0.5 -MAX_RETRY_DELAY_SECONDS = 10 -MAX_RETRY_DELAY_SECONDS_FROM_HEADER = 30 +INITIAL_RETRY_DELAY_SECONDS = 1.0 +MAX_RETRY_DELAY_SECONDS = 60.0 +JITTER_FACTOR = 0.2 # 20% random jitter def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: @@ -64,6 +63,38 @@ def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float return seconds +def _add_positive_jitter(delay: float) -> float: + """Add positive jitter (0-20%) to prevent thundering herd.""" + jitter_multiplier = 1 + random() * JITTER_FACTOR + return delay * jitter_multiplier + + +def _add_symmetric_jitter(delay: float) -> float: + """Add symmetric jitter (Β±10%) for exponential backoff.""" + jitter_multiplier = 1 + (random() - 0.5) * JITTER_FACTOR + return delay * jitter_multiplier + + +def _parse_x_ratelimit_reset(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + Parse the X-RateLimit-Reset header (Unix timestamp in seconds). + Returns seconds to wait, or None if header is missing/invalid. + """ + reset_time_str = response_headers.get("x-ratelimit-reset") + if reset_time_str is None: + return None + + try: + reset_time = int(reset_time_str) + delay = reset_time - time.time() + if delay > 0: + return delay + except (ValueError, TypeError): + pass + + return None + + def _retry_timeout(response: httpx.Response, retries: int) -> float: """ Determine the amount of time to wait before retrying a request. @@ -71,17 +102,19 @@ def _retry_timeout(response: httpx.Response, retries: int) -> float: with a jitter to determine the number of seconds to wait. """ - # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. + # 1. Check Retry-After header first retry_after = _parse_retry_after(response.headers) - if retry_after is not None and retry_after <= MAX_RETRY_DELAY_SECONDS_FROM_HEADER: - return retry_after + if retry_after is not None and retry_after > 0: + return min(retry_after, MAX_RETRY_DELAY_SECONDS) - # Apply exponential backoff, capped at MAX_RETRY_DELAY_SECONDS. - retry_delay = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + # 2. Check X-RateLimit-Reset header (with positive jitter) + ratelimit_reset = _parse_x_ratelimit_reset(response.headers) + if ratelimit_reset is not None: + return _add_positive_jitter(min(ratelimit_reset, MAX_RETRY_DELAY_SECONDS)) - # Add a randomness / jitter to the retry delay to avoid overwhelming the server with retries. - timeout = retry_delay * (1 - 0.25 * random()) - return timeout if timeout >= 0 else 0 + # 3. Fall back to exponential backoff (with symmetric jitter) + backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + return _add_symmetric_jitter(backoff) def _should_retry(response: httpx.Response) -> bool: @@ -89,6 +122,45 @@ def _should_retry(response: httpx.Response) -> bool: return response.status_code >= 500 or response.status_code in retryable_400s +def _build_url(base_url: str, path: typing.Optional[str]) -> str: + """ + Build a full URL by joining a base URL with a path. + + This function correctly handles base URLs that contain path prefixes (e.g., tenant-based URLs) + by using string concatenation instead of urllib.parse.urljoin(), which would incorrectly + strip path components when the path starts with '/'. + + Example: + >>> _build_url("https://cloud.example.com/org/tenant/api", "/users") + 'https://cloud.example.com/org/tenant/api/users' + + Args: + base_url: The base URL, which may contain path prefixes. + path: The path to append. Can be None or empty string. + + Returns: + The full URL with base_url and path properly joined. + """ + if not path: + return base_url + return f"{base_url.rstrip('/')}/{path.lstrip('/')}" + + +def _maybe_filter_none_from_multipart_data( + data: typing.Optional[typing.Any], + request_files: typing.Optional[RequestFiles], + force_multipart: typing.Optional[bool], +) -> typing.Optional[typing.Any]: + """ + Filter None values from data body for multipart/form requests. + This prevents httpx from converting None to empty strings in multipart encoding. + Only applies when files are present or force_multipart is True. + """ + if data is not None and isinstance(data, typing.Mapping) and (request_files or force_multipart): + return remove_none_from_dict(data) + return data + + def remove_omit_from_dict( original: typing.Dict[str, typing.Optional[typing.Any]], omit: typing.Optional[typing.Any], @@ -143,8 +215,19 @@ def get_request_body( # If both data and json are None, we send json data in the event extra properties are specified json_body = maybe_filter_request_body(json, request_options, omit) - # If you have an empty JSON body, you should just send None - return (json_body if json_body != {} else None), data_body if data_body != {} else None + has_additional_body_parameters = bool( + request_options is not None and request_options.get("additional_body_parameters") + ) + + # Only collapse empty dict to None when the body was not explicitly provided + # and there are no additional body parameters. This preserves explicit empty + # bodies (e.g., when an endpoint has a request body type but all fields are optional). + if json_body == {} and json is None and not has_additional_body_parameters: + json_body = None + if data_body == {} and data is None and not has_additional_body_parameters: + data_body = None + + return json_body, data_body class HttpClient: @@ -188,7 +271,7 @@ def request( ] = None, headers: typing.Optional[typing.Dict[str, typing.Any]] = None, request_options: typing.Optional[RequestOptions] = None, - retries: int = 2, + retries: int = 0, omit: typing.Optional[typing.Any] = None, force_multipart: typing.Optional[bool] = None, ) -> httpx.Response: @@ -210,9 +293,31 @@ def request( if (request_files is None or len(request_files) == 0) and force_multipart: request_files = FORCE_MULTIPART + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + response = self.httpx_client.request( method=method, - url=urllib.parse.urljoin(f"{base_url}/", path), + url=_build_url(base_url, path), headers=jsonable_encoder( remove_none_from_dict( { @@ -222,23 +327,7 @@ def request( } ) ), - params=encode_query( - jsonable_encoder( - remove_none_from_dict( - remove_omit_from_dict( - { - **(params if params is not None else {}), - **( - request_options.get("additional_query_parameters", {}) or {} - if request_options is not None - else {} - ), - }, - omit, - ) - ) - ) - ), + params=_encoded_params if _encoded_params else None, json=json_body, data=data_body, content=content, @@ -246,9 +335,9 @@ def request( timeout=timeout, ) - max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0 + max_retries: int = request_options.get("max_retries", 2) if request_options is not None else 2 if _should_retry(response=response): - if max_retries > retries: + if retries < max_retries: time.sleep(_retry_timeout(response=response, retries=retries)) return self.request( path=path, @@ -285,7 +374,7 @@ def stream( ] = None, headers: typing.Optional[typing.Dict[str, typing.Any]] = None, request_options: typing.Optional[RequestOptions] = None, - retries: int = 2, + retries: int = 0, omit: typing.Optional[typing.Any] = None, force_multipart: typing.Optional[bool] = None, ) -> typing.Iterator[httpx.Response]: @@ -307,9 +396,31 @@ def stream( json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + with self.httpx_client.stream( method=method, - url=urllib.parse.urljoin(f"{base_url}/", path), + url=_build_url(base_url, path), headers=jsonable_encoder( remove_none_from_dict( { @@ -319,23 +430,7 @@ def stream( } ) ), - params=encode_query( - jsonable_encoder( - remove_none_from_dict( - remove_omit_from_dict( - { - **(params if params is not None else {}), - **( - request_options.get("additional_query_parameters", {}) - if request_options is not None - else {} - ), - }, - omit, - ) - ) - ) - ), + params=_encoded_params if _encoded_params else None, json=json_body, data=data_body, content=content, @@ -353,12 +448,19 @@ def __init__( base_timeout: typing.Callable[[], typing.Optional[float]], base_headers: typing.Callable[[], typing.Dict[str, str]], base_url: typing.Optional[typing.Callable[[], str]] = None, + async_base_headers: typing.Optional[typing.Callable[[], typing.Awaitable[typing.Dict[str, str]]]] = None, ): self.base_url = base_url self.base_timeout = base_timeout self.base_headers = base_headers + self.async_base_headers = async_base_headers self.httpx_client = httpx_client + async def _get_headers(self) -> typing.Dict[str, str]: + if self.async_base_headers is not None: + return await self.async_base_headers() + return self.base_headers() + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: base_url = maybe_base_url if self.base_url is not None and base_url is None: @@ -386,7 +488,7 @@ async def request( ] = None, headers: typing.Optional[typing.Dict[str, typing.Any]] = None, request_options: typing.Optional[RequestOptions] = None, - retries: int = 2, + retries: int = 0, omit: typing.Optional[typing.Any] = None, force_multipart: typing.Optional[bool] = None, ) -> httpx.Response: @@ -408,36 +510,45 @@ async def request( json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Get headers (supports async token providers) + _headers = await self._get_headers() + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + # Add the input to each of these and do None-safety checks response = await self.httpx_client.request( method=method, - url=urllib.parse.urljoin(f"{base_url}/", path), + url=_build_url(base_url, path), headers=jsonable_encoder( remove_none_from_dict( { - **self.base_headers(), + **_headers, **(headers if headers is not None else {}), **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), } ) ), - params=encode_query( - jsonable_encoder( - remove_none_from_dict( - remove_omit_from_dict( - { - **(params if params is not None else {}), - **( - request_options.get("additional_query_parameters", {}) or {} - if request_options is not None - else {} - ), - }, - omit, - ) - ) - ) - ), + params=_encoded_params if _encoded_params else None, json=json_body, data=data_body, content=content, @@ -445,9 +556,9 @@ async def request( timeout=timeout, ) - max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0 + max_retries: int = request_options.get("max_retries", 2) if request_options is not None else 2 if _should_retry(response=response): - if max_retries > retries: + if retries < max_retries: await asyncio.sleep(_retry_timeout(response=response, retries=retries)) return await self.request( path=path, @@ -483,7 +594,7 @@ async def stream( ] = None, headers: typing.Optional[typing.Dict[str, typing.Any]] = None, request_options: typing.Optional[RequestOptions] = None, - retries: int = 2, + retries: int = 0, omit: typing.Optional[typing.Any] = None, force_multipart: typing.Optional[bool] = None, ) -> typing.AsyncIterator[httpx.Response]: @@ -505,35 +616,44 @@ async def stream( json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Get headers (supports async token providers) + _headers = await self._get_headers() + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit=omit, + ) + ) + ) + ) + async with self.httpx_client.stream( method=method, - url=urllib.parse.urljoin(f"{base_url}/", path), + url=_build_url(base_url, path), headers=jsonable_encoder( remove_none_from_dict( { - **self.base_headers(), + **_headers, **(headers if headers is not None else {}), **(request_options.get("additional_headers", {}) if request_options is not None else {}), } ) ), - params=encode_query( - jsonable_encoder( - remove_none_from_dict( - remove_omit_from_dict( - { - **(params if params is not None else {}), - **( - request_options.get("additional_query_parameters", {}) - if request_options is not None - else {} - ), - }, - omit=omit, - ) - ) - ) - ), + params=_encoded_params if _encoded_params else None, json=json_body, data=data_body, content=content, diff --git a/src/anduril/core/jsonable_encoder.py b/src/anduril/core/jsonable_encoder.py index afee366..f8beaea 100644 --- a/src/anduril/core/jsonable_encoder.py +++ b/src/anduril/core/jsonable_encoder.py @@ -30,6 +30,10 @@ def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None) -> Any: custom_encoder = custom_encoder or {} + # Generated SDKs use Ellipsis (`...`) as the sentinel value for "OMIT". + # OMIT values should be excluded from serialized payloads. + if obj is Ellipsis: + return None if custom_encoder: if type(obj) in custom_encoder: return custom_encoder[type(obj)](obj) @@ -70,6 +74,8 @@ def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any] allowed_keys = set(obj.keys()) for key, value in obj.items(): if key in allowed_keys: + if value is Ellipsis: + continue encoded_key = jsonable_encoder(key, custom_encoder=custom_encoder) encoded_value = jsonable_encoder(value, custom_encoder=custom_encoder) encoded_dict[encoded_key] = encoded_value @@ -77,6 +83,8 @@ def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any] if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): encoded_list = [] for item in obj: + if item is Ellipsis: + continue encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder)) return encoded_list diff --git a/src/anduril/core/oauth_token_provider.py b/src/anduril/core/oauth_token_provider.py new file mode 100644 index 0000000..bdc3860 --- /dev/null +++ b/src/anduril/core/oauth_token_provider.py @@ -0,0 +1,75 @@ +# This file was auto-generated by Fern from our API Definition. + +import asyncio +import datetime as dt +import threading +import typing +from asyncio import Lock as asyncio_Lock +from threading import Lock as threading_Lock + +from ..oauth.client import AsyncOauthClient, OauthClient +from .client_wrapper import AsyncClientWrapper, SyncClientWrapper + + +class OAuthTokenProvider: + BUFFER_IN_MINUTES = 2 + + def __init__(self, *, client_id: str, client_secret: str, client_wrapper: SyncClientWrapper): + self._client_id = client_id + self._client_secret = client_secret + self._access_token: typing.Optional[str] = None + self._expires_at: dt.datetime = dt.datetime.now() + self._auth_client = OauthClient(client_wrapper=client_wrapper) + self._lock: threading_Lock = threading.Lock() + + def get_token(self) -> str: + if self._access_token and self._expires_at > dt.datetime.now(): + return self._access_token + with self._lock: + if self._access_token and self._expires_at > dt.datetime.now(): + return self._access_token + return self._refresh() + + def _refresh(self) -> str: + token_response = self._auth_client.get_token(client_id=self._client_id, client_secret=self._client_secret) + self._access_token = token_response.access_token + self._expires_at = self._get_expires_at( + expires_in_seconds=token_response.expires_in if token_response.expires_in is not None else 3600, + buffer_in_minutes=self.BUFFER_IN_MINUTES, + ) + return self._access_token + + def _get_expires_at(self, *, expires_in_seconds: int, buffer_in_minutes: int): + return dt.datetime.now() + dt.timedelta(seconds=expires_in_seconds) - dt.timedelta(minutes=buffer_in_minutes) + + +class AsyncOAuthTokenProvider: + BUFFER_IN_MINUTES = 2 + + def __init__(self, *, client_id: str, client_secret: str, client_wrapper: AsyncClientWrapper): + self._client_id = client_id + self._client_secret = client_secret + self._access_token: typing.Optional[str] = None + self._expires_at: dt.datetime = dt.datetime.now() + self._auth_client = AsyncOauthClient(client_wrapper=client_wrapper) + self._lock: asyncio_Lock = asyncio.Lock() + + async def get_token(self) -> str: + if self._access_token and self._expires_at > dt.datetime.now(): + return self._access_token + async with self._lock: + if self._access_token and self._expires_at > dt.datetime.now(): + return self._access_token + return await self._refresh() + + async def _refresh(self) -> str: + token_response = await self._auth_client.get_token(client_id=self._client_id, client_secret=self._client_secret) + self._access_token = token_response.access_token + self._expires_at = self._get_expires_at( + expires_in_seconds=token_response.expires_in if token_response.expires_in is not None else 3600, + buffer_in_minutes=self.BUFFER_IN_MINUTES, + ) + return self._access_token + + def _get_expires_at(self, *, expires_in_seconds: int, buffer_in_minutes: int): + return dt.datetime.now() + dt.timedelta(seconds=expires_in_seconds) - dt.timedelta(minutes=buffer_in_minutes) diff --git a/src/anduril/core/pydantic_utilities.py b/src/anduril/core/pydantic_utilities.py index 185e5c4..12dc057 100644 --- a/src/anduril/core/pydantic_utilities.py +++ b/src/anduril/core/pydantic_utilities.py @@ -2,6 +2,7 @@ # nopycln: file import datetime as dt +import inspect from collections import defaultdict from typing import Any, Callable, ClassVar, Dict, List, Mapping, Optional, Set, Tuple, Type, TypeVar, Union, cast @@ -37,7 +38,36 @@ def parse_obj_as(type_: Type[T], object_: Any) -> T: - dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + # convert_and_respect_annotation_metadata is required for TypedDict aliasing. + # + # For Pydantic models, whether we should pre-dealias depends on how the model encodes aliasing: + # - If the model uses real Pydantic aliases (pydantic.Field(alias=...)), then we must pass wire keys through + # unchanged so Pydantic can validate them. + # - If the model encodes aliasing only via FieldMetadata annotations, then we MUST pre-dealias because Pydantic + # will not recognize those aliases during validation. + if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel): + has_pydantic_aliases = False + if IS_PYDANTIC_V2: + for field_name, field_info in getattr(type_, "model_fields", {}).items(): # type: ignore[attr-defined] + alias = getattr(field_info, "alias", None) + if alias is not None and alias != field_name: + has_pydantic_aliases = True + break + else: + for field in getattr(type_, "__fields__", {}).values(): + alias = getattr(field, "alias", None) + name = getattr(field, "name", None) + if alias is not None and name is not None and alias != name: + has_pydantic_aliases = True + break + + dealiased_object = ( + object_ + if has_pydantic_aliases + else convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + ) + else: + dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") if IS_PYDANTIC_V2: adapter = pydantic.TypeAdapter(type_) # type: ignore[attr-defined] return adapter.validate_python(dealiased_object) @@ -59,6 +89,43 @@ class UniversalBaseModel(pydantic.BaseModel): protected_namespaces=(), ) + @pydantic.model_validator(mode="before") # type: ignore[attr-defined] + @classmethod + def _coerce_field_names_to_aliases(cls, data: Any) -> Any: + """ + Accept Python field names in input by rewriting them to their Pydantic aliases, + while avoiding silent collisions when a key could refer to multiple fields. + """ + if not isinstance(data, Mapping): + return data + + fields = getattr(cls, "model_fields", {}) # type: ignore[attr-defined] + name_to_alias: Dict[str, str] = {} + alias_to_name: Dict[str, str] = {} + + for name, field_info in fields.items(): + alias = getattr(field_info, "alias", None) or name + name_to_alias[name] = alias + if alias != name: + alias_to_name[alias] = name + + # Detect ambiguous keys: a key that is an alias for one field and a name for another. + ambiguous_keys = set(alias_to_name.keys()).intersection(set(name_to_alias.keys())) + for key in ambiguous_keys: + if key in data and name_to_alias[key] not in data: + raise ValueError( + f"Ambiguous input key '{key}': it is both a field name and an alias. " + "Provide the explicit alias key to disambiguate." + ) + + original_keys = set(data.keys()) + rewritten: Dict[str, Any] = dict(data) + for name, alias in name_to_alias.items(): + if alias != name and name in original_keys and alias not in rewritten: + rewritten[alias] = rewritten.pop(name) + + return rewritten + @pydantic.model_serializer(mode="plain", when_used="json") # type: ignore[attr-defined] def serialize_model(self) -> Any: # type: ignore[name-defined] serialized = self.dict() # type: ignore[attr-defined] @@ -71,6 +138,40 @@ class Config: smart_union = True json_encoders = {dt.datetime: serialize_datetime} + @pydantic.root_validator(pre=True) + def _coerce_field_names_to_aliases(cls, values: Any) -> Any: + """ + Pydantic v1 equivalent of _coerce_field_names_to_aliases. + """ + if not isinstance(values, Mapping): + return values + + fields = getattr(cls, "__fields__", {}) + name_to_alias: Dict[str, str] = {} + alias_to_name: Dict[str, str] = {} + + for name, field in fields.items(): + alias = getattr(field, "alias", None) or name + name_to_alias[name] = alias + if alias != name: + alias_to_name[alias] = name + + ambiguous_keys = set(alias_to_name.keys()).intersection(set(name_to_alias.keys())) + for key in ambiguous_keys: + if key in values and name_to_alias[key] not in values: + raise ValueError( + f"Ambiguous input key '{key}': it is both a field name and an alias. " + "Provide the explicit alias key to disambiguate." + ) + + original_keys = set(values.keys()) + rewritten: Dict[str, Any] = dict(values) + for name, alias in name_to_alias.items(): + if alias != name and name in original_keys and alias not in rewritten: + rewritten[alias] = rewritten.pop(name) + + return rewritten + @classmethod def model_construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") diff --git a/src/anduril/entities/client.py b/src/anduril/entities/client.py index fe45e45..acb9f92 100644 --- a/src/anduril/entities/client.py +++ b/src/anduril/entities/client.py @@ -260,7 +260,8 @@ def publish_entity( from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.entities.publish_entity() """ @@ -327,7 +328,8 @@ def get_entity(self, entity_id: str, *, request_options: typing.Optional[Request from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.entities.get_entity( entity_id="entityId", @@ -382,7 +384,8 @@ def override_entity( from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.entities.override_entity( entity_id="entityId", @@ -421,7 +424,8 @@ def remove_entity_override( from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.entities.remove_entity_override( entity_id="entityId", @@ -470,7 +474,8 @@ def long_poll_entity_events( from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.entities.long_poll_entity_events( session_token="sessionToken", @@ -534,7 +539,8 @@ def stream_entities( from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) response = client.entities.stream_entities() for chunk in response: @@ -765,7 +771,8 @@ async def publish_entity( from anduril import AsyncLattice client = AsyncLattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) @@ -840,7 +847,8 @@ async def get_entity(self, entity_id: str, *, request_options: typing.Optional[R from anduril import AsyncLattice client = AsyncLattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) @@ -903,7 +911,8 @@ async def override_entity( from anduril import AsyncLattice client = AsyncLattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) @@ -950,7 +959,8 @@ async def remove_entity_override( from anduril import AsyncLattice client = AsyncLattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) @@ -1009,7 +1019,8 @@ async def long_poll_entity_events( from anduril import AsyncLattice client = AsyncLattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) @@ -1081,7 +1092,8 @@ async def stream_entities( from anduril import AsyncLattice client = AsyncLattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) diff --git a/src/anduril/entities/types/stream_entities_response.py b/src/anduril/entities/types/stream_entities_response.py index f7ceb7d..253f92f 100644 --- a/src/anduril/entities/types/stream_entities_response.py +++ b/src/anduril/entities/types/stream_entities_response.py @@ -37,7 +37,7 @@ class StreamEntitiesResponse_Entity(UniversalBaseModel): event: typing.Literal["entity"] = "entity" event_type: typing_extensions.Annotated[typing.Optional[EntityEventEventType], FieldMetadata(alias="eventType")] = ( - None + pydantic.Field(alias="eventType", default=None) ) time: typing.Optional[dt.datetime] = None entity: typing.Optional["Entity"] = None @@ -52,9 +52,11 @@ class Config: extra = pydantic.Extra.allow -from ...types.entity import Entity # noqa: E402, I001 - StreamEntitiesResponse = typing_extensions.Annotated[ typing.Union[StreamEntitiesResponse_Heartbeat, StreamEntitiesResponse_Entity], pydantic.Field(discriminator="event") ] -update_forward_refs(StreamEntitiesResponse_Entity) +from ...types.entity import Entity # noqa: E402, I001 +from ...types.override import Override # noqa: E402, I001 +from ...types.overrides import Overrides # noqa: E402, I001 + +update_forward_refs(StreamEntitiesResponse_Entity, Entity=Entity, Override=Override, Overrides=Overrides) diff --git a/src/anduril/entity/types/error.py b/src/anduril/entity/types/error.py index 3fca493..f3b2a1e 100644 --- a/src/anduril/entity/types/error.py +++ b/src/anduril/entity/types/error.py @@ -9,7 +9,7 @@ class Error(UniversalBaseModel): - error_code: typing_extensions.Annotated[str, FieldMetadata(alias="errorCode")] + error_code: typing_extensions.Annotated[str, FieldMetadata(alias="errorCode")] = pydantic.Field(alias="errorCode") message: str if IS_PYDANTIC_V2: diff --git a/src/anduril/oauth/__init__.py b/src/anduril/oauth/__init__.py new file mode 100644 index 0000000..7bed663 --- /dev/null +++ b/src/anduril/oauth/__init__.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import GetTokenResponse +_dynamic_imports: typing.Dict[str, str] = {"GetTokenResponse": ".types"} + + +def __getattr__(attr_name: str) -> typing.Any: + 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 = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + 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 __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["GetTokenResponse"] diff --git a/src/anduril/oauth/client.py b/src/anduril/oauth/client.py new file mode 100644 index 0000000..a8a1554 --- /dev/null +++ b/src/anduril/oauth/client.py @@ -0,0 +1,133 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawOauthClient, RawOauthClient +from .types.get_token_response import GetTokenResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class OauthClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawOauthClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawOauthClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawOauthClient + """ + return self._raw_client + + def get_token( + self, + *, + client_id: typing.Optional[str] = OMIT, + client_secret: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetTokenResponse: + """ + Gets a new short-lived token using the specified client credentials + + Parameters + ---------- + client_id : typing.Optional[str] + The client identifier + + client_secret : typing.Optional[str] + The client secret + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetTokenResponse + Access token response + + Examples + -------- + from anduril import Lattice + + client = Lattice( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + ) + client.oauth.get_token() + """ + _response = self._raw_client.get_token( + client_id=client_id, client_secret=client_secret, request_options=request_options + ) + return _response.data + + +class AsyncOauthClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawOauthClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawOauthClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawOauthClient + """ + return self._raw_client + + async def get_token( + self, + *, + client_id: typing.Optional[str] = OMIT, + client_secret: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetTokenResponse: + """ + Gets a new short-lived token using the specified client credentials + + Parameters + ---------- + client_id : typing.Optional[str] + The client identifier + + client_secret : typing.Optional[str] + The client secret + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetTokenResponse + Access token response + + Examples + -------- + import asyncio + + from anduril import AsyncLattice + + client = AsyncLattice( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + ) + + + async def main() -> None: + await client.oauth.get_token() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_token( + client_id=client_id, client_secret=client_secret, request_options=request_options + ) + return _response.data diff --git a/src/anduril/oauth/raw_client.py b/src/anduril/oauth/raw_client.py new file mode 100644 index 0000000..41863b1 --- /dev/null +++ b/src/anduril/oauth/raw_client.py @@ -0,0 +1,180 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..errors.bad_request_error import BadRequestError +from ..errors.unauthorized_error import UnauthorizedError +from .types.get_token_response import GetTokenResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawOauthClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_token( + self, + *, + client_id: typing.Optional[str] = OMIT, + client_secret: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetTokenResponse]: + """ + Gets a new short-lived token using the specified client credentials + + Parameters + ---------- + client_id : typing.Optional[str] + The client identifier + + client_secret : typing.Optional[str] + The client secret + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetTokenResponse] + Access token response + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/oauth/token", + method="POST", + data={ + "client_id": client_id, + "client_secret": client_secret, + "grant_type": "client_credentials", + }, + headers={ + "content-type": "application/x-www-form-urlencoded", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetTokenResponse, + parse_obj_as( + type_=GetTokenResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawOauthClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_token( + self, + *, + client_id: typing.Optional[str] = OMIT, + client_secret: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetTokenResponse]: + """ + Gets a new short-lived token using the specified client credentials + + Parameters + ---------- + client_id : typing.Optional[str] + The client identifier + + client_secret : typing.Optional[str] + The client secret + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetTokenResponse] + Access token response + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/oauth/token", + method="POST", + data={ + "client_id": client_id, + "client_secret": client_secret, + "grant_type": "client_credentials", + }, + headers={ + "content-type": "application/x-www-form-urlencoded", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetTokenResponse, + parse_obj_as( + type_=GetTokenResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/anduril/oauth/types/__init__.py b/src/anduril/oauth/types/__init__.py new file mode 100644 index 0000000..add5295 --- /dev/null +++ b/src/anduril/oauth/types/__init__.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .get_token_response import GetTokenResponse +_dynamic_imports: typing.Dict[str, str] = {"GetTokenResponse": ".get_token_response"} + + +def __getattr__(attr_name: str) -> typing.Any: + 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 = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + 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 __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["GetTokenResponse"] diff --git a/src/anduril/oauth/types/get_token_response.py b/src/anduril/oauth/types/get_token_response.py new file mode 100644 index 0000000..cbc3e6a --- /dev/null +++ b/src/anduril/oauth/types/get_token_response.py @@ -0,0 +1,51 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class GetTokenResponse(UniversalBaseModel): + access_token: str = pydantic.Field() + """ + The access token + """ + + token_type: str = pydantic.Field() + """ + The type of token (typically "Bearer") + """ + + expires_in: typing.Optional[int] = pydantic.Field(default=None) + """ + Lifetime of the access token in seconds + """ + + refresh_expires_in: typing.Optional[int] = pydantic.Field(default=None) + """ + Lifetime of the refresh token + """ + + not_before_policy: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="not-before-policy")] = ( + pydantic.Field(alias="not-before-policy", default=None) + ) + """ + Enforce that a token cannot be used before a specific unixtime + """ + + scope: typing.Optional[str] = pydantic.Field(default=None) + """ + The scope of the access token + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/anduril/objects/client.py b/src/anduril/objects/client.py index 4e0b198..2fd622c 100644 --- a/src/anduril/objects/client.py +++ b/src/anduril/objects/client.py @@ -69,7 +69,8 @@ def list_objects( from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) response = client.objects.list_objects() for item in response: @@ -121,7 +122,8 @@ def get_object( from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.objects.get_object( object_path="objectPath", @@ -156,6 +158,16 @@ def upload_object( ------- PathMetadata Successful upload + + Examples + -------- + from anduril import Lattice + + client = Lattice( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + ) + client.objects.upload_object() """ _response = self._raw_client.upload_object(object_path, request=request, request_options=request_options) return _response.data @@ -181,7 +193,8 @@ def delete_object(self, object_path: str, *, request_options: typing.Optional[Re from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.objects.delete_object( object_path="objectPath", @@ -213,7 +226,8 @@ def get_object_metadata( from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.objects.get_object_metadata( object_path="objectPath", @@ -279,7 +293,8 @@ async def list_objects( from anduril import AsyncLattice client = AsyncLattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) @@ -340,7 +355,8 @@ async def get_object( from anduril import AsyncLattice client = AsyncLattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) @@ -382,6 +398,24 @@ async def upload_object( ------- PathMetadata Successful upload + + Examples + -------- + import asyncio + + from anduril import AsyncLattice + + client = AsyncLattice( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + ) + + + async def main() -> None: + await client.objects.upload_object() + + + asyncio.run(main()) """ _response = await self._raw_client.upload_object(object_path, request=request, request_options=request_options) return _response.data @@ -409,7 +443,8 @@ async def delete_object(self, object_path: str, *, request_options: typing.Optio from anduril import AsyncLattice client = AsyncLattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) @@ -449,7 +484,8 @@ async def get_object_metadata( from anduril import AsyncLattice client = AsyncLattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) diff --git a/src/anduril/tasks/client.py b/src/anduril/tasks/client.py index f005196..c9d23b7 100644 --- a/src/anduril/tasks/client.py +++ b/src/anduril/tasks/client.py @@ -102,7 +102,8 @@ def create_task( from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.tasks.create_task() """ @@ -148,7 +149,8 @@ def get_task(self, task_id: str, *, request_options: typing.Optional[RequestOpti from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.tasks.get_task( task_id="taskId", @@ -208,7 +210,8 @@ def update_task_status( from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.tasks.update_task_status( task_id="taskId", @@ -277,7 +280,8 @@ def query_tasks( from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.tasks.query_tasks() """ @@ -336,7 +340,8 @@ def listen_as_agent( from anduril import Lattice client = Lattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) client.tasks.listen_as_agent() """ @@ -427,7 +432,8 @@ async def create_task( from anduril import AsyncLattice client = AsyncLattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) @@ -481,7 +487,8 @@ async def get_task(self, task_id: str, *, request_options: typing.Optional[Reque from anduril import AsyncLattice client = AsyncLattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) @@ -549,7 +556,8 @@ async def update_task_status( from anduril import AsyncLattice client = AsyncLattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) @@ -626,7 +634,8 @@ async def query_tasks( from anduril import AsyncLattice client = AsyncLattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) @@ -693,7 +702,8 @@ async def listen_as_agent( from anduril import AsyncLattice client = AsyncLattice( - token="YOUR_TOKEN", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", ) diff --git a/src/anduril/tasks/types/task_query_update_time_range.py b/src/anduril/tasks/types/task_query_update_time_range.py index 1757903..cd8476c 100644 --- a/src/anduril/tasks/types/task_query_update_time_range.py +++ b/src/anduril/tasks/types/task_query_update_time_range.py @@ -14,14 +14,14 @@ class TaskQueryUpdateTimeRange(UniversalBaseModel): """ start_time: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="startTime")] = pydantic.Field( - default=None + alias="startTime", default=None ) """ If provided, returns Tasks only updated after this time. """ end_time: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="endTime")] = pydantic.Field( - default=None + alias="endTime", default=None ) """ If provided, returns Tasks only updated before this time. diff --git a/src/anduril/types/__init__.py b/src/anduril/types/__init__.py index 62190c6..ac32b92 100644 --- a/src/anduril/types/__init__.py +++ b/src/anduril/types/__init__.py @@ -11,6 +11,7 @@ from .active_target import ActiveTarget from .agent import Agent from .agent_request import AgentRequest + from .agent_task_request import AgentTaskRequest from .alert import Alert from .alert_condition import AlertCondition from .alert_level import AlertLevel @@ -19,6 +20,7 @@ from .alternate_id import AlternateId from .alternate_id_type import AlternateIdType from .angle_of_arrival import AngleOfArrival + from .bad_request_error_body import BadRequestErrorBody from .bandwidth import Bandwidth from .bandwidth_range import BandwidthRange from .cancel_request import CancelRequest @@ -184,6 +186,7 @@ from .transponder_codes import TransponderCodes from .transponder_codes_mode4interrogation_response import TransponderCodesMode4InterrogationResponse from .u_int32range import UInt32Range + from .unauthorized_error_body import UnauthorizedErrorBody from .user import User from .visual_details import VisualDetails _dynamic_imports: typing.Dict[str, str] = { @@ -192,6 +195,7 @@ "ActiveTarget": ".active_target", "Agent": ".agent", "AgentRequest": ".agent_request", + "AgentTaskRequest": ".agent_task_request", "Alert": ".alert", "AlertCondition": ".alert_condition", "AlertLevel": ".alert_level", @@ -200,6 +204,7 @@ "AlternateId": ".alternate_id", "AlternateIdType": ".alternate_id_type", "AngleOfArrival": ".angle_of_arrival", + "BadRequestErrorBody": ".bad_request_error_body", "Bandwidth": ".bandwidth", "BandwidthRange": ".bandwidth_range", "CancelRequest": ".cancel_request", @@ -365,6 +370,7 @@ "TransponderCodes": ".transponder_codes", "TransponderCodesMode4InterrogationResponse": ".transponder_codes_mode4interrogation_response", "UInt32Range": ".u_int32range", + "UnauthorizedErrorBody": ".unauthorized_error_body", "User": ".user", "VisualDetails": ".visual_details", } @@ -397,6 +403,7 @@ def __dir__(): "ActiveTarget", "Agent", "AgentRequest", + "AgentTaskRequest", "Alert", "AlertCondition", "AlertLevel", @@ -405,6 +412,7 @@ def __dir__(): "AlternateId", "AlternateIdType", "AngleOfArrival", + "BadRequestErrorBody", "Bandwidth", "BandwidthRange", "CancelRequest", @@ -570,6 +578,7 @@ def __dir__(): "TransponderCodes", "TransponderCodesMode4InterrogationResponse", "UInt32Range", + "UnauthorizedErrorBody", "User", "VisualDetails", ] diff --git a/src/anduril/types/acm_details.py b/src/anduril/types/acm_details.py index f4ffdca..a216574 100644 --- a/src/anduril/types/acm_details.py +++ b/src/anduril/types/acm_details.py @@ -10,9 +10,11 @@ class AcmDetails(UniversalBaseModel): - acm_type: typing_extensions.Annotated[typing.Optional[AcmDetailsAcmType], FieldMetadata(alias="acmType")] = None + acm_type: typing_extensions.Annotated[typing.Optional[AcmDetailsAcmType], FieldMetadata(alias="acmType")] = ( + pydantic.Field(alias="acmType", default=None) + ) acm_description: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="acmDescription")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="acmDescription", default=None) ) """ Used for loosely typed associations, such as assignment to a specific fires unit. diff --git a/src/anduril/types/agent.py b/src/anduril/types/agent.py index 633c481..06c01c2 100644 --- a/src/anduril/types/agent.py +++ b/src/anduril/types/agent.py @@ -14,7 +14,7 @@ class Agent(UniversalBaseModel): """ entity_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="entityId")] = pydantic.Field( - default=None + alias="entityId", default=None ) """ Entity ID of the agent. diff --git a/src/anduril/types/agent_request.py b/src/anduril/types/agent_request.py index 5e80fd3..e7b2c46 100644 --- a/src/anduril/types/agent_request.py +++ b/src/anduril/types/agent_request.py @@ -29,13 +29,13 @@ class AgentRequest(UniversalBaseModel): execute_request: typing_extensions.Annotated[ typing.Optional[ExecuteRequest], FieldMetadata(alias="executeRequest") - ] = None + ] = pydantic.Field(alias="executeRequest", default=None) cancel_request: typing_extensions.Annotated[ typing.Optional[CancelRequest], FieldMetadata(alias="cancelRequest") - ] = None + ] = pydantic.Field(alias="cancelRequest", default=None) complete_request: typing_extensions.Annotated[ typing.Optional[CompleteRequest], FieldMetadata(alias="completeRequest") - ] = None + ] = pydantic.Field(alias="completeRequest", default=None) if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/anduril/types/agent_task_request.py b/src/anduril/types/agent_task_request.py new file mode 100644 index 0000000..9398920 --- /dev/null +++ b/src/anduril/types/agent_task_request.py @@ -0,0 +1,41 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel, update_forward_refs +from ..core.serialization import FieldMetadata +from .cancel_request import CancelRequest +from .complete_request import CompleteRequest +from .execute_request import ExecuteRequest + + +class AgentTaskRequest(UniversalBaseModel): + """ + The wrapper for a task's action requests: execute, cancel, or complete. + """ + + execute_request: typing_extensions.Annotated[ + typing.Optional[ExecuteRequest], FieldMetadata(alias="executeRequest") + ] = pydantic.Field(alias="executeRequest", default=None) + cancel_request: typing_extensions.Annotated[ + typing.Optional[CancelRequest], FieldMetadata(alias="cancelRequest") + ] = pydantic.Field(alias="cancelRequest", default=None) + complete_request: typing_extensions.Annotated[ + typing.Optional[CompleteRequest], FieldMetadata(alias="completeRequest") + ] = pydantic.Field(alias="completeRequest", default=None) + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + +update_forward_refs(AgentTaskRequest) diff --git a/src/anduril/types/alert.py b/src/anduril/types/alert.py index a153007..1964536 100644 --- a/src/anduril/types/alert.py +++ b/src/anduril/types/alert.py @@ -18,7 +18,7 @@ class Alert(UniversalBaseModel): """ alert_code: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="alertCode")] = pydantic.Field( - default=None + alias="alertCode", default=None ) """ Short, machine-readable code that describes this alert. This code is intended to provide systems off-asset @@ -38,7 +38,7 @@ class Alert(UniversalBaseModel): """ activated_time: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="activatedTime")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="activatedTime", default=None) ) """ Time at which this alert was activated. @@ -46,7 +46,7 @@ class Alert(UniversalBaseModel): active_conditions: typing_extensions.Annotated[ typing.Optional[typing.List[AlertCondition]], FieldMetadata(alias="activeConditions") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="activeConditions", default=None) """ Set of conditions which have activated this alert. """ diff --git a/src/anduril/types/alert_condition.py b/src/anduril/types/alert_condition.py index 0cb521c..c23a483 100644 --- a/src/anduril/types/alert_condition.py +++ b/src/anduril/types/alert_condition.py @@ -14,7 +14,7 @@ class AlertCondition(UniversalBaseModel): """ condition_code: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="conditionCode")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="conditionCode", default=None) ) """ Short, machine-readable code that describes this condition. This code is intended to provide systems off-asset diff --git a/src/anduril/types/aliases.py b/src/anduril/types/aliases.py index dc8ae2f..32d4e2b 100644 --- a/src/anduril/types/aliases.py +++ b/src/anduril/types/aliases.py @@ -16,7 +16,7 @@ class Aliases(UniversalBaseModel): alternate_ids: typing_extensions.Annotated[ typing.Optional[typing.List[AlternateId]], FieldMetadata(alias="alternateIds") - ] = None + ] = pydantic.Field(alias="alternateIds", default=None) name: typing.Optional[str] = pydantic.Field(default=None) """ The best available version of the entity's display name. diff --git a/src/anduril/types/allocation.py b/src/anduril/types/allocation.py index 647375e..3fb5e0f 100644 --- a/src/anduril/types/allocation.py +++ b/src/anduril/types/allocation.py @@ -16,7 +16,7 @@ class Allocation(UniversalBaseModel): active_agents: typing_extensions.Annotated[ typing.Optional[typing.List[Agent]], FieldMetadata(alias="activeAgents") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="activeAgents", default=None) """ Agents actively being utilized in a task. """ diff --git a/src/anduril/types/angle_of_arrival.py b/src/anduril/types/angle_of_arrival.py index eef2c6e..0c94882 100644 --- a/src/anduril/types/angle_of_arrival.py +++ b/src/anduril/types/angle_of_arrival.py @@ -16,7 +16,7 @@ class AngleOfArrival(UniversalBaseModel): """ relative_pose: typing_extensions.Annotated[typing.Optional[Pose], FieldMetadata(alias="relativePose")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="relativePose", default=None) ) """ Origin (LLA) and attitude (relative to ENU) of a ray pointing towards the detection. The attitude represents a @@ -25,7 +25,7 @@ class AngleOfArrival(UniversalBaseModel): bearing_elevation_covariance_rad2: typing_extensions.Annotated[ typing.Optional[TMat2], FieldMetadata(alias="bearingElevationCovarianceRad2") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="bearingElevationCovarianceRad2", default=None) """ Bearing/elevation covariance matrix where bearing is defined in radians CCW+ about the z-axis from the x-axis of FLU frame and elevation is positive down from the FL/XY plane. diff --git a/src/anduril/types/bad_request_error_body.py b/src/anduril/types/bad_request_error_body.py new file mode 100644 index 0000000..922512f --- /dev/null +++ b/src/anduril/types/bad_request_error_body.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class BadRequestErrorBody(UniversalBaseModel): + error: str + error_description: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/anduril/types/bandwidth.py b/src/anduril/types/bandwidth.py index 711a868..faf458c 100644 --- a/src/anduril/types/bandwidth.py +++ b/src/anduril/types/bandwidth.py @@ -13,7 +13,9 @@ class Bandwidth(UniversalBaseModel): Describes the bandwidth of a signal """ - bandwidth_hz: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="bandwidthHz")] = None + bandwidth_hz: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="bandwidthHz")] = ( + pydantic.Field(alias="bandwidthHz", default=None) + ) if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/anduril/types/bandwidth_range.py b/src/anduril/types/bandwidth_range.py index 6a2ad2a..70f681a 100644 --- a/src/anduril/types/bandwidth_range.py +++ b/src/anduril/types/bandwidth_range.py @@ -16,10 +16,10 @@ class BandwidthRange(UniversalBaseModel): minimum_bandwidth: typing_extensions.Annotated[ typing.Optional[Bandwidth], FieldMetadata(alias="minimumBandwidth") - ] = None + ] = pydantic.Field(alias="minimumBandwidth", default=None) maximum_bandwidth: typing_extensions.Annotated[ typing.Optional[Bandwidth], FieldMetadata(alias="maximumBandwidth") - ] = None + ] = pydantic.Field(alias="maximumBandwidth", default=None) if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/anduril/types/cancel_request.py b/src/anduril/types/cancel_request.py index 05e760e..026bce6 100644 --- a/src/anduril/types/cancel_request.py +++ b/src/anduril/types/cancel_request.py @@ -17,7 +17,7 @@ class CancelRequest(UniversalBaseModel): """ task_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="taskId")] = pydantic.Field( - default=None + alias="taskId", default=None ) """ The unique task ID of the task to cancel. @@ -41,4 +41,4 @@ class Config: from .principal import Principal # noqa: E402, I001 -update_forward_refs(CancelRequest) +update_forward_refs(CancelRequest, Principal=Principal) diff --git a/src/anduril/types/complete_request.py b/src/anduril/types/complete_request.py index 7741822..aea1c35 100644 --- a/src/anduril/types/complete_request.py +++ b/src/anduril/types/complete_request.py @@ -15,7 +15,7 @@ class CompleteRequest(UniversalBaseModel): """ task_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="taskId")] = pydantic.Field( - default=None + alias="taskId", default=None ) """ ID of the task to complete. diff --git a/src/anduril/types/component_health.py b/src/anduril/types/component_health.py index 5a1f751..6b287bd 100644 --- a/src/anduril/types/component_health.py +++ b/src/anduril/types/component_health.py @@ -37,7 +37,7 @@ class ComponentHealth(UniversalBaseModel): """ update_time: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="updateTime")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="updateTime", default=None) ) """ The last update time for this specific component. diff --git a/src/anduril/types/correlation_membership.py b/src/anduril/types/correlation_membership.py index 82335d4..1fe3de8 100644 --- a/src/anduril/types/correlation_membership.py +++ b/src/anduril/types/correlation_membership.py @@ -13,7 +13,7 @@ class CorrelationMembership(UniversalBaseModel): correlation_set_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="correlationSetId")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="correlationSetId", default=None) ) """ The ID of the correlation set this entity belongs to. @@ -27,7 +27,7 @@ class CorrelationMembership(UniversalBaseModel): non_primary: typing_extensions.Annotated[ typing.Optional[NonPrimaryMembership], FieldMetadata(alias="nonPrimary") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="nonPrimary", default=None) """ This entity is not the primary of the correlation set. Note that there may not be a primary at all. diff --git a/src/anduril/types/correlation_metadata.py b/src/anduril/types/correlation_metadata.py index 7462d9d..ba34d9e 100644 --- a/src/anduril/types/correlation_metadata.py +++ b/src/anduril/types/correlation_metadata.py @@ -19,7 +19,7 @@ class CorrelationMetadata(UniversalBaseModel): replication_mode: typing_extensions.Annotated[ typing.Optional[CorrelationMetadataReplicationMode], FieldMetadata(alias="replicationMode") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="replicationMode", default=None) """ Indicates how the correlation will be distributed. Because a correlation is composed of multiple secondaries, each of which may have been correlated with different replication diff --git a/src/anduril/types/cron_window.py b/src/anduril/types/cron_window.py index b809a40..7563850 100644 --- a/src/anduril/types/cron_window.py +++ b/src/anduril/types/cron_window.py @@ -10,7 +10,7 @@ class CronWindow(UniversalBaseModel): cron_expression: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="cronExpression")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="cronExpression", default=None) ) """ in UTC, describes when and at what cadence this window starts, in the quartz flavor of cron @@ -25,7 +25,7 @@ class CronWindow(UniversalBaseModel): """ duration_millis: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="durationMillis")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="durationMillis", default=None) ) """ describes the duration diff --git a/src/anduril/types/decorrelated_single.py b/src/anduril/types/decorrelated_single.py index 2c7cde9..5e20520 100644 --- a/src/anduril/types/decorrelated_single.py +++ b/src/anduril/types/decorrelated_single.py @@ -11,7 +11,7 @@ class DecorrelatedSingle(UniversalBaseModel): entity_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="entityId")] = pydantic.Field( - default=None + alias="entityId", default=None ) """ The entity that was decorrelated against. diff --git a/src/anduril/types/decorrelation.py b/src/anduril/types/decorrelation.py index 6a3408d..2bdf4d4 100644 --- a/src/anduril/types/decorrelation.py +++ b/src/anduril/types/decorrelation.py @@ -12,7 +12,7 @@ class Decorrelation(UniversalBaseModel): all_: typing_extensions.Annotated[typing.Optional[DecorrelatedAll], FieldMetadata(alias="all")] = pydantic.Field( - default=None + alias="all", default=None ) """ This will be specified if this entity was decorrelated against all other entities. @@ -20,7 +20,7 @@ class Decorrelation(UniversalBaseModel): decorrelated_entities: typing_extensions.Annotated[ typing.Optional[typing.List[DecorrelatedSingle]], FieldMetadata(alias="decorrelatedEntities") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="decorrelatedEntities", default=None) """ A list of decorrelated entities that have been explicitly decorrelated against this entity which prevents lower precedence correlations from overriding it in the future. diff --git a/src/anduril/types/dimensions.py b/src/anduril/types/dimensions.py index 054d1ce..1023d7c 100644 --- a/src/anduril/types/dimensions.py +++ b/src/anduril/types/dimensions.py @@ -10,7 +10,7 @@ class Dimensions(UniversalBaseModel): length_m: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="lengthM")] = pydantic.Field( - default=None + alias="lengthM", default=None ) """ Length of the entity in meters diff --git a/src/anduril/types/echelon.py b/src/anduril/types/echelon.py index 9a55e29..660dabd 100644 --- a/src/anduril/types/echelon.py +++ b/src/anduril/types/echelon.py @@ -18,7 +18,7 @@ class Echelon(UniversalBaseModel): army_echelon: typing_extensions.Annotated[ typing.Optional[EchelonArmyEchelon], FieldMetadata(alias="armyEchelon") - ] = None + ] = pydantic.Field(alias="armyEchelon", default=None) if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/anduril/types/emitter_notation.py b/src/anduril/types/emitter_notation.py index 411bc9d..29e1770 100644 --- a/src/anduril/types/emitter_notation.py +++ b/src/anduril/types/emitter_notation.py @@ -13,7 +13,9 @@ class EmitterNotation(UniversalBaseModel): A representation of a single emitter notation. """ - emitter_notation: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="emitterNotation")] = None + emitter_notation: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="emitterNotation")] = ( + pydantic.Field(alias="emitterNotation", default=None) + ) confidence: typing.Optional[float] = pydantic.Field(default=None) """ confidence as a percentage that the emitter notation in this component is accurate diff --git a/src/anduril/types/entity.py b/src/anduril/types/entity.py index f664004..9f6ffd6 100644 --- a/src/anduril/types/entity.py +++ b/src/anduril/types/entity.py @@ -49,7 +49,7 @@ class Entity(UniversalBaseModel): """ entity_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="entityId")] = pydantic.Field( - default=None + alias="entityId", default=None ) """ A Globally Unique Identifier (GUID) for your entity. This is a required @@ -63,7 +63,7 @@ class Entity(UniversalBaseModel): """ is_live: typing_extensions.Annotated[typing.Optional[bool], FieldMetadata(alias="isLive")] = pydantic.Field( - default=None + alias="isLive", default=None ) """ Indicates the entity is active and should have a lifecycle state of CREATE or UPDATE. @@ -71,7 +71,7 @@ class Entity(UniversalBaseModel): """ created_time: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="createdTime")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="createdTime", default=None) ) """ The time when the entity was first known to the entity producer. If this field is empty, the Entity Manager API uses the @@ -81,7 +81,7 @@ class Entity(UniversalBaseModel): """ expiry_time: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="expiryTime")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="expiryTime", default=None) ) """ Future time that expires an entity and updates the is_live flag. @@ -94,7 +94,7 @@ class Entity(UniversalBaseModel): """ no_expiry: typing_extensions.Annotated[typing.Optional[bool], FieldMetadata(alias="noExpiry")] = pydantic.Field( - default=None + alias="noExpiry", default=None ) """ Use noExpiry only when the entity contains information that should be available to other @@ -115,20 +115,20 @@ class Entity(UniversalBaseModel): location_uncertainty: typing_extensions.Annotated[ typing.Optional[LocationUncertainty], FieldMetadata(alias="locationUncertainty") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="locationUncertainty", default=None) """ Indicates uncertainty of the entity's position and kinematics. """ geo_shape: typing_extensions.Annotated[typing.Optional[GeoShape], FieldMetadata(alias="geoShape")] = pydantic.Field( - default=None + alias="geoShape", default=None ) """ Geospatial representation of the entity, including entities that cover an area rather than a fixed point. """ geo_details: typing_extensions.Annotated[typing.Optional[GeoDetails], FieldMetadata(alias="geoDetails")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="geoDetails", default=None) ) """ Additional details on what the geospatial area or point represents, along with visual display details. @@ -150,7 +150,7 @@ class Entity(UniversalBaseModel): """ mil_view: typing_extensions.Annotated[typing.Optional[MilView], FieldMetadata(alias="milView")] = pydantic.Field( - default=None + alias="milView", default=None ) """ View of the entity. @@ -172,7 +172,7 @@ class Entity(UniversalBaseModel): """ power_state: typing_extensions.Annotated[typing.Optional[PowerState], FieldMetadata(alias="powerState")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="powerState", default=None) ) """ Details the entity's power source. @@ -197,7 +197,7 @@ class Entity(UniversalBaseModel): target_priority: typing_extensions.Annotated[ typing.Optional[TargetPriority], FieldMetadata(alias="targetPriority") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="targetPriority", default=None) """ The prioritization associated with an entity, such as if it's a threat or a high-value target. """ @@ -209,21 +209,21 @@ class Entity(UniversalBaseModel): transponder_codes: typing_extensions.Annotated[ typing.Optional[TransponderCodes], FieldMetadata(alias="transponderCodes") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="transponderCodes", default=None) """ A message describing any transponder codes associated with Mode 1, 2, 3, 4, 5, S interrogations. These are related to ADS-B modes. """ data_classification: typing_extensions.Annotated[ typing.Optional[Classification], FieldMetadata(alias="dataClassification") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="dataClassification", default=None) """ Describes an entity's security classification levels at an overall classification level and on a per field level. """ task_catalog: typing_extensions.Annotated[typing.Optional[TaskCatalog], FieldMetadata(alias="taskCatalog")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="taskCatalog", default=None) ) """ A catalog of tasks that can be performed by an entity. @@ -241,7 +241,7 @@ class Entity(UniversalBaseModel): visual_details: typing_extensions.Annotated[ typing.Optional[VisualDetails], FieldMetadata(alias="visualDetails") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="visualDetails", default=None) """ Visual details associated with the display of an entity in the client. """ @@ -252,7 +252,7 @@ class Entity(UniversalBaseModel): """ route_details: typing_extensions.Annotated[typing.Optional[RouteDetails], FieldMetadata(alias="routeDetails")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="routeDetails", default=None) ) """ Additional information about an entity's route. @@ -269,7 +269,7 @@ class Entity(UniversalBaseModel): """ group_details: typing_extensions.Annotated[typing.Optional[GroupDetails], FieldMetadata(alias="groupDetails")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="groupDetails", default=None) ) """ Details for the group associated with this entity. @@ -300,6 +300,7 @@ class Config: extra = pydantic.Extra.allow +from .override import Override # noqa: E402, I001 from .overrides import Overrides # noqa: E402, I001 -update_forward_refs(Entity) +update_forward_refs(Entity, Override=Override, Overrides=Overrides) diff --git a/src/anduril/types/entity_event.py b/src/anduril/types/entity_event.py index cf8f5e3..dcc8caf 100644 --- a/src/anduril/types/entity_event.py +++ b/src/anduril/types/entity_event.py @@ -18,7 +18,7 @@ class EntityEvent(UniversalBaseModel): """ event_type: typing_extensions.Annotated[typing.Optional[EntityEventEventType], FieldMetadata(alias="eventType")] = ( - None + pydantic.Field(alias="eventType", default=None) ) time: typing.Optional[dt.datetime] = None entity: typing.Optional["Entity"] = None @@ -34,5 +34,7 @@ class Config: from .entity import Entity # noqa: E402, I001 +from .override import Override # noqa: E402, I001 +from .overrides import Overrides # noqa: E402, I001 -update_forward_refs(EntityEvent) +update_forward_refs(EntityEvent, Entity=Entity, Override=Override, Overrides=Overrides) diff --git a/src/anduril/types/entity_event_response.py b/src/anduril/types/entity_event_response.py index bb78dc3..70f6837 100644 --- a/src/anduril/types/entity_event_response.py +++ b/src/anduril/types/entity_event_response.py @@ -13,7 +13,7 @@ class EntityEventResponse(UniversalBaseModel): session_token: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="sessionToken")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="sessionToken", default=None) ) """ Long-poll session identifier. Use this token to resume polling on subsequent requests. @@ -21,7 +21,7 @@ class EntityEventResponse(UniversalBaseModel): entity_events: typing_extensions.Annotated[ typing.Optional[typing.List[EntityEvent]], FieldMetadata(alias="entityEvents") - ] = None + ] = pydantic.Field(alias="entityEvents", default=None) if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/anduril/types/entity_ids_selector.py b/src/anduril/types/entity_ids_selector.py index bc7d9d9..f12b0fb 100644 --- a/src/anduril/types/entity_ids_selector.py +++ b/src/anduril/types/entity_ids_selector.py @@ -10,7 +10,7 @@ class EntityIdsSelector(UniversalBaseModel): entity_ids: typing_extensions.Annotated[typing.Optional[typing.List[str]], FieldMetadata(alias="entityIds")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="entityIds", default=None) ) """ Receive tasks as an assignee for one or more of the supplied entity ids. diff --git a/src/anduril/types/error_ellipse.py b/src/anduril/types/error_ellipse.py index d35b9fb..0d2a770 100644 --- a/src/anduril/types/error_ellipse.py +++ b/src/anduril/types/error_ellipse.py @@ -19,21 +19,21 @@ class ErrorEllipse(UniversalBaseModel): """ semi_major_axis_m: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="semiMajorAxisM")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="semiMajorAxisM", default=None) ) """ Defines the distance from the center point of the ellipse to the furthest distance on the perimeter in meters. """ semi_minor_axis_m: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="semiMinorAxisM")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="semiMinorAxisM", default=None) ) """ Defines the distance from the center point of the ellipse to the shortest distance on the perimeter in meters. """ orientation_d: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="orientationD")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="orientationD", default=None) ) """ The orientation of the semi-major relative to true north in degrees from clockwise: 0-180 due to symmetry across the semi-minor axis. diff --git a/src/anduril/types/field_classification_information.py b/src/anduril/types/field_classification_information.py index c641ff4..68a59eb 100644 --- a/src/anduril/types/field_classification_information.py +++ b/src/anduril/types/field_classification_information.py @@ -15,7 +15,7 @@ class FieldClassificationInformation(UniversalBaseModel): """ field_path: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="fieldPath")] = pydantic.Field( - default=None + alias="fieldPath", default=None ) """ Proto field path which is the string representation of a field. @@ -24,7 +24,7 @@ class FieldClassificationInformation(UniversalBaseModel): classification_information: typing_extensions.Annotated[ typing.Optional[ClassificationInformation], FieldMetadata(alias="classificationInformation") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="classificationInformation", default=None) """ The information which makes up the field level classification marking. """ diff --git a/src/anduril/types/field_of_view.py b/src/anduril/types/field_of_view.py index 25508e5..aabcc1a 100644 --- a/src/anduril/types/field_of_view.py +++ b/src/anduril/types/field_of_view.py @@ -18,7 +18,7 @@ class FieldOfView(UniversalBaseModel): """ fov_id: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="fovId")] = pydantic.Field( - default=None + alias="fovId", default=None ) """ The Id for one instance of a FieldOfView, persisted across multiple updates to provide continuity during @@ -27,7 +27,7 @@ class FieldOfView(UniversalBaseModel): """ mount_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="mountId")] = pydantic.Field( - default=None + alias="mountId", default=None ) """ The Id of the mount the sensor is on. @@ -35,21 +35,21 @@ class FieldOfView(UniversalBaseModel): projected_frustum: typing_extensions.Annotated[ typing.Optional[ProjectedFrustum], FieldMetadata(alias="projectedFrustum") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="projectedFrustum", default=None) """ The field of view the sensor projected onto the ground. """ projected_center_ray: typing_extensions.Annotated[ typing.Optional[Position], FieldMetadata(alias="projectedCenterRay") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="projectedCenterRay", default=None) """ Center ray of the frustum projected onto the ground. """ center_ray_pose: typing_extensions.Annotated[ typing.Optional[EntityManagerPose], FieldMetadata(alias="centerRayPose") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="centerRayPose", default=None) """ The origin and direction of the center ray for this sensor relative to the ENU frame. A ray which is aligned with the positive X axis in the sensor frame will be transformed into the ray along the sensor direction in the ENU @@ -57,14 +57,14 @@ class FieldOfView(UniversalBaseModel): """ horizontal_fov: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="horizontalFov")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="horizontalFov", default=None) ) """ Horizontal field of view in radians. """ vertical_fov: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="verticalFov")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="verticalFov", default=None) ) """ Vertical field of view in radians. diff --git a/src/anduril/types/frequency.py b/src/anduril/types/frequency.py index b7c6b06..235a576 100644 --- a/src/anduril/types/frequency.py +++ b/src/anduril/types/frequency.py @@ -15,7 +15,7 @@ class Frequency(UniversalBaseModel): """ frequency_hz: typing_extensions.Annotated[typing.Optional[Measurement], FieldMetadata(alias="frequencyHz")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="frequencyHz", default=None) ) """ Indicates a frequency of a signal (Hz) with its standard deviation. diff --git a/src/anduril/types/frequency_range.py b/src/anduril/types/frequency_range.py index 458cf2e..ab7fb87 100644 --- a/src/anduril/types/frequency_range.py +++ b/src/anduril/types/frequency_range.py @@ -16,14 +16,14 @@ class FrequencyRange(UniversalBaseModel): minimum_frequency_hz: typing_extensions.Annotated[ typing.Optional[Frequency], FieldMetadata(alias="minimumFrequencyHz") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="minimumFrequencyHz", default=None) """ Indicates the lowest measured frequency of a signal (Hz). """ maximum_frequency_hz: typing_extensions.Annotated[ typing.Optional[Frequency], FieldMetadata(alias="maximumFrequencyHz") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="maximumFrequencyHz", default=None) """ Indicates the maximum measured frequency of a signal (Hz). """ diff --git a/src/anduril/types/fuel.py b/src/anduril/types/fuel.py index f6aafa8..4446b12 100644 --- a/src/anduril/types/fuel.py +++ b/src/anduril/types/fuel.py @@ -16,7 +16,7 @@ class Fuel(UniversalBaseModel): """ fuel_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="fuelId")] = pydantic.Field( - default=None + alias="fuelId", default=None ) """ unique fuel identifier @@ -28,14 +28,14 @@ class Fuel(UniversalBaseModel): """ reported_date: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="reportedDate")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="reportedDate", default=None) ) """ timestamp the information was reported """ amount_gallons: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="amountGallons")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="amountGallons", default=None) ) """ amount of gallons on hand @@ -43,28 +43,28 @@ class Fuel(UniversalBaseModel): max_authorized_capacity_gallons: typing_extensions.Annotated[ typing.Optional[int], FieldMetadata(alias="maxAuthorizedCapacityGallons") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="maxAuthorizedCapacityGallons", default=None) """ how much the asset is allowed to have available (in gallons) """ operational_requirement_gallons: typing_extensions.Annotated[ typing.Optional[int], FieldMetadata(alias="operationalRequirementGallons") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="operationalRequirementGallons", default=None) """ minimum required for operations (in gallons) """ data_classification: typing_extensions.Annotated[ typing.Optional[Classification], FieldMetadata(alias="dataClassification") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="dataClassification", default=None) """ fuel in a single asset may have different levels of classification use case: fuel for a SECRET asset while diesel fuel may be UNCLASSIFIED """ data_source: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="dataSource")] = pydantic.Field( - default=None + alias="dataSource", default=None ) """ source of information diff --git a/src/anduril/types/geo_details.py b/src/anduril/types/geo_details.py index c926a3f..6bbd6f7 100644 --- a/src/anduril/types/geo_details.py +++ b/src/anduril/types/geo_details.py @@ -19,7 +19,7 @@ class GeoDetails(UniversalBaseModel): type: typing.Optional[GeoDetailsType] = None control_area: typing_extensions.Annotated[ typing.Optional[ControlAreaDetails], FieldMetadata(alias="controlArea") - ] = None + ] = pydantic.Field(alias="controlArea", default=None) acm: typing.Optional[AcmDetails] = None if IS_PYDANTIC_V2: diff --git a/src/anduril/types/geo_ellipse.py b/src/anduril/types/geo_ellipse.py index 81fa27a..38fd48c 100644 --- a/src/anduril/types/geo_ellipse.py +++ b/src/anduril/types/geo_ellipse.py @@ -16,28 +16,28 @@ class GeoEllipse(UniversalBaseModel): """ semi_major_axis_m: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="semiMajorAxisM")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="semiMajorAxisM", default=None) ) """ Defines the distance from the center point of the ellipse to the furthest distance on the perimeter in meters. """ semi_minor_axis_m: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="semiMinorAxisM")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="semiMinorAxisM", default=None) ) """ Defines the distance from the center point of the ellipse to the shortest distance on the perimeter in meters. """ orientation_d: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="orientationD")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="orientationD", default=None) ) """ The orientation of the semi-major relative to true north in degrees from clockwise: 0-180 due to symmetry across the semi-minor axis. """ height_m: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="heightM")] = pydantic.Field( - default=None + alias="heightM", default=None ) """ Optional height above entity position to extrude in meters. A non-zero value creates an elliptic cylinder diff --git a/src/anduril/types/geo_ellipsoid.py b/src/anduril/types/geo_ellipsoid.py index 3080bf3..314fb84 100644 --- a/src/anduril/types/geo_ellipsoid.py +++ b/src/anduril/types/geo_ellipsoid.py @@ -16,21 +16,21 @@ class GeoEllipsoid(UniversalBaseModel): """ forward_axis_m: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="forwardAxisM")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="forwardAxisM", default=None) ) """ Defines the distance from the center point to the surface along the forward axis """ side_axis_m: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="sideAxisM")] = pydantic.Field( - default=None + alias="sideAxisM", default=None ) """ Defines the distance from the center point to the surface along the side axis """ up_axis_m: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="upAxisM")] = pydantic.Field( - default=None + alias="upAxisM", default=None ) """ Defines the distance from the center point to the surface along the up axis diff --git a/src/anduril/types/geo_polygon.py b/src/anduril/types/geo_polygon.py index ba7e030..20e8992 100644 --- a/src/anduril/types/geo_polygon.py +++ b/src/anduril/types/geo_polygon.py @@ -21,7 +21,7 @@ class GeoPolygon(UniversalBaseModel): """ is_rectangle: typing_extensions.Annotated[typing.Optional[bool], FieldMetadata(alias="isRectangle")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="isRectangle", default=None) ) """ An extension hint that this polygon is a rectangle. When true this implies several things: diff --git a/src/anduril/types/geo_polygon_position.py b/src/anduril/types/geo_polygon_position.py index c23bbdf..9189d54 100644 --- a/src/anduril/types/geo_polygon_position.py +++ b/src/anduril/types/geo_polygon_position.py @@ -20,7 +20,7 @@ class GeoPolygonPosition(UniversalBaseModel): """ height_m: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="heightM")] = pydantic.Field( - default=None + alias="heightM", default=None ) """ optional height above base position to extrude in meters. diff --git a/src/anduril/types/google_protobuf_any.py b/src/anduril/types/google_protobuf_any.py index 8ba5dd6..f06e379 100644 --- a/src/anduril/types/google_protobuf_any.py +++ b/src/anduril/types/google_protobuf_any.py @@ -13,7 +13,9 @@ class GoogleProtobufAny(UniversalBaseModel): Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. """ - type: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="@type")] = pydantic.Field(default=None) + type: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="@type")] = pydantic.Field( + alias="@type", default=None + ) """ The type of the serialized message. """ diff --git a/src/anduril/types/health.py b/src/anduril/types/health.py index df19df1..cdad044 100644 --- a/src/anduril/types/health.py +++ b/src/anduril/types/health.py @@ -20,14 +20,14 @@ class Health(UniversalBaseModel): connection_status: typing_extensions.Annotated[ typing.Optional[HealthConnectionStatus], FieldMetadata(alias="connectionStatus") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="connectionStatus", default=None) """ Status indicating whether the entity is able to communicate with Entity Manager. """ health_status: typing_extensions.Annotated[ typing.Optional[HealthHealthStatus], FieldMetadata(alias="healthStatus") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="healthStatus", default=None) """ Top-level health status; typically a roll-up of individual component healths. """ @@ -38,7 +38,7 @@ class Health(UniversalBaseModel): """ update_time: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="updateTime")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="updateTime", default=None) ) """ The update time for the top-level health information. @@ -47,7 +47,7 @@ class Health(UniversalBaseModel): active_alerts: typing_extensions.Annotated[ typing.Optional[typing.List[Alert]], FieldMetadata(alias="activeAlerts") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="activeAlerts", default=None) """ Active alerts indicate a critical change in system state sent by the asset that must be made known to an operator or consumer of the common operating picture. diff --git a/src/anduril/types/high_value_target.py b/src/anduril/types/high_value_target.py index 3ded2f8..ac8802f 100644 --- a/src/anduril/types/high_value_target.py +++ b/src/anduril/types/high_value_target.py @@ -16,13 +16,13 @@ class HighValueTarget(UniversalBaseModel): is_high_value_target: typing_extensions.Annotated[ typing.Optional[bool], FieldMetadata(alias="isHighValueTarget") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="isHighValueTarget", default=None) """ Indicates whether the target matches any description from a high value target list. """ target_priority: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="targetPriority")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="targetPriority", default=None) ) """ The priority associated with the target. If the target's description appears on multiple high value target lists, @@ -34,14 +34,14 @@ class HighValueTarget(UniversalBaseModel): target_matches: typing_extensions.Annotated[ typing.Optional[typing.List[HighValueTargetMatch]], FieldMetadata(alias="targetMatches") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="targetMatches", default=None) """ All of the high value target descriptions that the target matches against. """ is_high_payoff_target: typing_extensions.Annotated[ typing.Optional[bool], FieldMetadata(alias="isHighPayoffTarget") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="isHighPayoffTarget", default=None) """ Indicates whether the target is a 'High Payoff Target'. Targets can be one or both of high value and high payoff. """ diff --git a/src/anduril/types/high_value_target_match.py b/src/anduril/types/high_value_target_match.py index 26e5f62..7c8d68c 100644 --- a/src/anduril/types/high_value_target_match.py +++ b/src/anduril/types/high_value_target_match.py @@ -11,14 +11,14 @@ class HighValueTargetMatch(UniversalBaseModel): high_value_target_list_id: typing_extensions.Annotated[ typing.Optional[str], FieldMetadata(alias="highValueTargetListId") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="highValueTargetListId", default=None) """ The ID of the high value target list that matches the target description. """ high_value_target_description_id: typing_extensions.Annotated[ typing.Optional[str], FieldMetadata(alias="highValueTargetDescriptionId") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="highValueTargetDescriptionId", default=None) """ The ID of the specific high value target description within a high value target list that was matched against. The ID is considered to be a globally unique identifier across all high value target IDs. diff --git a/src/anduril/types/line_of_bearing.py b/src/anduril/types/line_of_bearing.py index 4050b30..875e9ce 100644 --- a/src/anduril/types/line_of_bearing.py +++ b/src/anduril/types/line_of_bearing.py @@ -17,20 +17,20 @@ class LineOfBearing(UniversalBaseModel): angle_of_arrival: typing_extensions.Annotated[ typing.Optional[AngleOfArrival], FieldMetadata(alias="angleOfArrival") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="angleOfArrival", default=None) """ The direction pointing from this entity to the detection """ range_estimate_m: typing_extensions.Annotated[ typing.Optional[Measurement], FieldMetadata(alias="rangeEstimateM") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="rangeEstimateM", default=None) """ The estimated distance of the detection """ max_range_m: typing_extensions.Annotated[typing.Optional[Measurement], FieldMetadata(alias="maxRangeM")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="maxRangeM", default=None) ) """ The maximum distance of the detection diff --git a/src/anduril/types/lla.py b/src/anduril/types/lla.py index 99dc821..fb2628b 100644 --- a/src/anduril/types/lla.py +++ b/src/anduril/types/lla.py @@ -16,7 +16,7 @@ class Lla(UniversalBaseModel): is2d: typing.Optional[bool] = None altitude_reference: typing_extensions.Annotated[ typing.Optional[LlaAltitudeReference], FieldMetadata(alias="altitudeReference") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="altitudeReference", default=None) """ Meaning of alt. altitude in meters above either WGS84 or EGM96, use altitude_reference to diff --git a/src/anduril/types/location.py b/src/anduril/types/location.py index 4d81d07..4893981 100644 --- a/src/anduril/types/location.py +++ b/src/anduril/types/location.py @@ -22,14 +22,14 @@ class Location(UniversalBaseModel): """ velocity_enu: typing_extensions.Annotated[typing.Optional[Enu], FieldMetadata(alias="velocityEnu")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="velocityEnu", default=None) ) """ Velocity in an ENU reference frame centered on the corresponding position. All units are meters per second. """ speed_mps: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="speedMps")] = pydantic.Field( - default=None + alias="speedMps", default=None ) """ Speed is the magnitude of velocity_enu vector [sqrt(e^2 + n^2 + u^2)] when present, measured in m/s. @@ -41,7 +41,7 @@ class Location(UniversalBaseModel): """ attitude_enu: typing_extensions.Annotated[typing.Optional[Quaternion], FieldMetadata(alias="attitudeEnu")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="attitudeEnu", default=None) ) """ quaternion to translate from entity body frame to it's ENU frame diff --git a/src/anduril/types/location_uncertainty.py b/src/anduril/types/location_uncertainty.py index 85f2b2e..39915d4 100644 --- a/src/anduril/types/location_uncertainty.py +++ b/src/anduril/types/location_uncertainty.py @@ -17,7 +17,7 @@ class LocationUncertainty(UniversalBaseModel): position_enu_cov: typing_extensions.Annotated[ typing.Optional[EntityManagerTMat3], FieldMetadata(alias="positionEnuCov") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="positionEnuCov", default=None) """ Positional covariance represented by the upper triangle of the covariance matrix. It is valid to populate only the diagonal of the matrix if the full covariance matrix is unknown. @@ -25,7 +25,7 @@ class LocationUncertainty(UniversalBaseModel): velocity_enu_cov: typing_extensions.Annotated[ typing.Optional[EntityManagerTMat3], FieldMetadata(alias="velocityEnuCov") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="velocityEnuCov", default=None) """ Velocity covariance represented by the upper triangle of the covariance matrix. It is valid to populate only the diagonal of the matrix if the full covariance matrix is unknown. @@ -33,7 +33,7 @@ class LocationUncertainty(UniversalBaseModel): position_error_ellipse: typing_extensions.Annotated[ typing.Optional[ErrorEllipse], FieldMetadata(alias="positionErrorEllipse") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="positionErrorEllipse", default=None) """ An ellipse that describes the certainty probability and error boundary for a given geolocation. """ diff --git a/src/anduril/types/mean_keplerian_elements.py b/src/anduril/types/mean_keplerian_elements.py index 3e22283..f6120b7 100644 --- a/src/anduril/types/mean_keplerian_elements.py +++ b/src/anduril/types/mean_keplerian_elements.py @@ -16,14 +16,14 @@ class MeanKeplerianElements(UniversalBaseModel): """ semi_major_axis_km: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="semiMajorAxisKm")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="semiMajorAxisKm", default=None) ) """ Preferred: semi major axis in kilometers """ mean_motion: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="meanMotion")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="meanMotion", default=None) ) """ If using SGP/SGP4, provide the Keplerian Mean Motion in revolutions per day @@ -31,14 +31,14 @@ class MeanKeplerianElements(UniversalBaseModel): eccentricity: typing.Optional[float] = None inclination_deg: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="inclinationDeg")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="inclinationDeg", default=None) ) """ Angle of inclination in deg """ ra_of_asc_node_deg: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="raOfAscNodeDeg")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="raOfAscNodeDeg", default=None) ) """ Right ascension of the ascending node in deg @@ -46,13 +46,13 @@ class MeanKeplerianElements(UniversalBaseModel): arg_of_pericenter_deg: typing_extensions.Annotated[ typing.Optional[float], FieldMetadata(alias="argOfPericenterDeg") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="argOfPericenterDeg", default=None) """ Argument of pericenter in deg """ mean_anomaly_deg: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="meanAnomalyDeg")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="meanAnomalyDeg", default=None) ) """ Mean anomaly in deg diff --git a/src/anduril/types/media_item.py b/src/anduril/types/media_item.py index 6fe09ce..41660ec 100644 --- a/src/anduril/types/media_item.py +++ b/src/anduril/types/media_item.py @@ -12,7 +12,7 @@ class MediaItem(UniversalBaseModel): type: typing.Optional[MediaItemType] = None relative_path: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="relativePath")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="relativePath", default=None) ) """ The path, relative to the environment base URL, where media related to an entity can be accessed diff --git a/src/anduril/types/mode5.py b/src/anduril/types/mode5.py index 4dcbe53..2a477f7 100644 --- a/src/anduril/types/mode5.py +++ b/src/anduril/types/mode5.py @@ -16,7 +16,7 @@ class Mode5(UniversalBaseModel): mode5interrogation_response: typing_extensions.Annotated[ typing.Optional[Mode5Mode5InterrogationResponse], FieldMetadata(alias="mode5InterrogationResponse") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="mode5InterrogationResponse", default=None) """ The validity of the response from the Mode 5 interrogation. """ @@ -27,7 +27,7 @@ class Mode5(UniversalBaseModel): """ mode5platform_id: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="mode5PlatformId")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="mode5PlatformId", default=None) ) """ The Mode 5 platform identification code. diff --git a/src/anduril/types/ontology.py b/src/anduril/types/ontology.py index a33db7f..962c2f2 100644 --- a/src/anduril/types/ontology.py +++ b/src/anduril/types/ontology.py @@ -15,14 +15,14 @@ class Ontology(UniversalBaseModel): """ platform_type: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="platformType")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="platformType", default=None) ) """ A string that describes the entity's high-level type with natural language. """ specific_type: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="specificType")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="specificType", default=None) ) """ A string that describes the entity's exact model or type. diff --git a/src/anduril/types/orbit.py b/src/anduril/types/orbit.py index d788e0a..45890ff 100644 --- a/src/anduril/types/orbit.py +++ b/src/anduril/types/orbit.py @@ -12,7 +12,7 @@ class Orbit(UniversalBaseModel): orbit_mean_elements: typing_extensions.Annotated[ typing.Optional[OrbitMeanElements], FieldMetadata(alias="orbitMeanElements") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="orbitMeanElements", default=None) """ Orbit Mean Elements data, analogous to the Orbit Mean Elements Message in CCSDS 502.0-B-3 """ diff --git a/src/anduril/types/orbit_mean_elements.py b/src/anduril/types/orbit_mean_elements.py index 3378a1e..d0fead9 100644 --- a/src/anduril/types/orbit_mean_elements.py +++ b/src/anduril/types/orbit_mean_elements.py @@ -19,10 +19,10 @@ class OrbitMeanElements(UniversalBaseModel): metadata: typing.Optional[OrbitMeanElementsMetadata] = None mean_keplerian_elements: typing_extensions.Annotated[ typing.Optional[MeanKeplerianElements], FieldMetadata(alias="meanKeplerianElements") - ] = None + ] = pydantic.Field(alias="meanKeplerianElements", default=None) tle_parameters: typing_extensions.Annotated[ typing.Optional[TleParameters], FieldMetadata(alias="tleParameters") - ] = None + ] = pydantic.Field(alias="tleParameters", default=None) if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/anduril/types/orbit_mean_elements_metadata.py b/src/anduril/types/orbit_mean_elements_metadata.py index f7ae8f0..ca2cc78 100644 --- a/src/anduril/types/orbit_mean_elements_metadata.py +++ b/src/anduril/types/orbit_mean_elements_metadata.py @@ -13,7 +13,7 @@ class OrbitMeanElementsMetadata(UniversalBaseModel): creation_date: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="creationDate")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="creationDate", default=None) ) """ Creation date/time in UTC @@ -25,7 +25,7 @@ class OrbitMeanElementsMetadata(UniversalBaseModel): """ message_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="messageId")] = pydantic.Field( - default=None + alias="messageId", default=None ) """ ID that uniquely identifies a message from a given originator. @@ -33,13 +33,13 @@ class OrbitMeanElementsMetadata(UniversalBaseModel): ref_frame: typing_extensions.Annotated[ typing.Optional[OrbitMeanElementsMetadataRefFrame], FieldMetadata(alias="refFrame") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="refFrame", default=None) """ Reference frame, assumed to be Earth-centered """ ref_frame_epoch: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="refFrameEpoch")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="refFrameEpoch", default=None) ) """ Reference frame epoch in UTC - mandatory only if not intrinsic to frame definition @@ -47,7 +47,7 @@ class OrbitMeanElementsMetadata(UniversalBaseModel): mean_element_theory: typing_extensions.Annotated[ typing.Optional[OrbitMeanElementsMetadataMeanElementTheory], FieldMetadata(alias="meanElementTheory") - ] = None + ] = pydantic.Field(alias="meanElementTheory", default=None) if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/anduril/types/override.py b/src/anduril/types/override.py index 7cf10fb..db5faf8 100644 --- a/src/anduril/types/override.py +++ b/src/anduril/types/override.py @@ -20,14 +20,14 @@ class Override(UniversalBaseModel): """ request_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="requestId")] = pydantic.Field( - default=None + alias="requestId", default=None ) """ override request id for an override request """ field_path: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="fieldPath")] = pydantic.Field( - default=None + alias="fieldPath", default=None ) """ proto field path which is the string representation of a field. @@ -36,7 +36,7 @@ class Override(UniversalBaseModel): masked_field_value: typing_extensions.Annotated[ typing.Optional["Entity"], FieldMetadata(alias="maskedFieldValue") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="maskedFieldValue", default=None) """ new field value corresponding to field path. In the shape of an empty entity with only the changed value. example: entity: { mil_view: { disposition: Disposition_DISPOSITION_HOSTILE } } @@ -56,7 +56,7 @@ class Override(UniversalBaseModel): request_timestamp: typing_extensions.Annotated[ typing.Optional[dt.datetime], FieldMetadata(alias="requestTimestamp") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="requestTimestamp", default=None) """ Timestamp of the override request. The timestamp is generated by the Entity Manager instance that receives the request. """ @@ -72,5 +72,6 @@ class Config: from .entity import Entity # noqa: E402, I001 +from .overrides import Overrides # noqa: E402, I001 -update_forward_refs(Override) +update_forward_refs(Override, Entity=Entity, Overrides=Overrides) diff --git a/src/anduril/types/overrides.py b/src/anduril/types/overrides.py index 496b404..06fe0f8 100644 --- a/src/anduril/types/overrides.py +++ b/src/anduril/types/overrides.py @@ -25,6 +25,7 @@ class Config: extra = pydantic.Extra.allow +from .entity import Entity # noqa: E402, I001 from .override import Override # noqa: E402, I001 -update_forward_refs(Overrides) +update_forward_refs(Overrides, Entity=Entity, Override=Override) diff --git a/src/anduril/types/owner.py b/src/anduril/types/owner.py index 32bc8a6..da4e3e0 100644 --- a/src/anduril/types/owner.py +++ b/src/anduril/types/owner.py @@ -14,7 +14,7 @@ class Owner(UniversalBaseModel): """ entity_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="entityId")] = pydantic.Field( - default=None + alias="entityId", default=None ) """ Entity ID of the owner. diff --git a/src/anduril/types/payload_configuration.py b/src/anduril/types/payload_configuration.py index 96125b6..f0c2744 100644 --- a/src/anduril/types/payload_configuration.py +++ b/src/anduril/types/payload_configuration.py @@ -12,7 +12,7 @@ class PayloadConfiguration(UniversalBaseModel): capability_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="capabilityId")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="capabilityId", default=None) ) """ Identifying ID for the capability. @@ -27,21 +27,21 @@ class PayloadConfiguration(UniversalBaseModel): effective_environment: typing_extensions.Annotated[ typing.Optional[typing.List[PayloadConfigurationEffectiveEnvironmentItem]], FieldMetadata(alias="effectiveEnvironment"), - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="effectiveEnvironment", default=None) """ The target environments the configuration is effective against. """ payload_operational_state: typing_extensions.Annotated[ typing.Optional[PayloadConfigurationPayloadOperationalState], FieldMetadata(alias="payloadOperationalState") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="payloadOperationalState", default=None) """ The operational state of this payload. """ payload_description: typing_extensions.Annotated[ typing.Optional[str], FieldMetadata(alias="payloadDescription") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="payloadDescription", default=None) """ A human readable description of the payload """ diff --git a/src/anduril/types/payloads.py b/src/anduril/types/payloads.py index 17a5f31..506e824 100644 --- a/src/anduril/types/payloads.py +++ b/src/anduril/types/payloads.py @@ -16,7 +16,7 @@ class Payloads(UniversalBaseModel): payload_configurations: typing_extensions.Annotated[ typing.Optional[typing.List[Payload]], FieldMetadata(alias="payloadConfigurations") - ] = None + ] = pydantic.Field(alias="payloadConfigurations", default=None) if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/anduril/types/pose.py b/src/anduril/types/pose.py index a99d3b7..ecb0fd5 100644 --- a/src/anduril/types/pose.py +++ b/src/anduril/types/pose.py @@ -17,7 +17,7 @@ class Pose(UniversalBaseModel): """ att_enu: typing_extensions.Annotated[typing.Optional[Quaternion], FieldMetadata(alias="attEnu")] = pydantic.Field( - default=None + alias="attEnu", default=None ) """ The quaternion to transform a point in the Pose frame to the ENU frame. The Pose frame could be Body, Turret, diff --git a/src/anduril/types/position.py b/src/anduril/types/position.py index afcc4b4..5222211 100644 --- a/src/anduril/types/position.py +++ b/src/anduril/types/position.py @@ -18,14 +18,14 @@ class Position(UniversalBaseModel): """ latitude_degrees: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="latitudeDegrees")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="latitudeDegrees", default=None) ) """ WGS84 geodetic latitude in decimal degrees. """ longitude_degrees: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="longitudeDegrees")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="longitudeDegrees", default=None) ) """ WGS84 longitude in decimal degrees. @@ -33,7 +33,7 @@ class Position(UniversalBaseModel): altitude_hae_meters: typing_extensions.Annotated[ typing.Optional[float], FieldMetadata(alias="altitudeHaeMeters") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="altitudeHaeMeters", default=None) """ altitude as height above ellipsoid (WGS84) in meters. DoubleValue wrapper is used to distinguish optional from default 0. @@ -41,7 +41,7 @@ class Position(UniversalBaseModel): altitude_agl_meters: typing_extensions.Annotated[ typing.Optional[float], FieldMetadata(alias="altitudeAglMeters") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="altitudeAglMeters", default=None) """ Altitude as AGL (Above Ground Level) if the upstream data source has this value set. This value represents the entity's height above the terrain. This is typically measured with a radar altimeter or by using a terrain tile @@ -50,7 +50,7 @@ class Position(UniversalBaseModel): altitude_asf_meters: typing_extensions.Annotated[ typing.Optional[float], FieldMetadata(alias="altitudeAsfMeters") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="altitudeAsfMeters", default=None) """ Altitude as ASF (Above Sea Floor) if the upstream data source has this value set. If the value is not set from the upstream, this value is not set. @@ -58,7 +58,7 @@ class Position(UniversalBaseModel): pressure_depth_meters: typing_extensions.Annotated[ typing.Optional[float], FieldMetadata(alias="pressureDepthMeters") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="pressureDepthMeters", default=None) """ The depth of the entity from the surface of the water through sensor measurements based on differential pressure between the interior and exterior of the vessel. If the value is not set from the upstream, this value is not set. diff --git a/src/anduril/types/power_level.py b/src/anduril/types/power_level.py index 3bab529..7baf4a2 100644 --- a/src/anduril/types/power_level.py +++ b/src/anduril/types/power_level.py @@ -24,7 +24,7 @@ class PowerLevel(UniversalBaseModel): """ percent_remaining: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="percentRemaining")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="percentRemaining", default=None) ) """ Percent of power remaining. @@ -37,7 +37,7 @@ class PowerLevel(UniversalBaseModel): """ current_amps: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="currentAmps")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="currentAmps", default=None) ) """ Current in amps of the power source subsystem, as reported by the power source. If the source does not @@ -46,7 +46,7 @@ class PowerLevel(UniversalBaseModel): run_time_to_empty_mins: typing_extensions.Annotated[ typing.Optional[float], FieldMetadata(alias="runTimeToEmptyMins") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="runTimeToEmptyMins", default=None) """ Estimated minutes until empty. Calculated with consumption at the moment, as reported by the power source. If the source does not report this value this field will be null. @@ -54,7 +54,7 @@ class PowerLevel(UniversalBaseModel): consumption_rate_l_per_s: typing_extensions.Annotated[ typing.Optional[float], FieldMetadata(alias="consumptionRateLPerS") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="consumptionRateLPerS", default=None) """ Fuel consumption rate in liters per second. """ diff --git a/src/anduril/types/power_source.py b/src/anduril/types/power_source.py index 15f2976..d73bfa9 100644 --- a/src/anduril/types/power_source.py +++ b/src/anduril/types/power_source.py @@ -18,20 +18,20 @@ class PowerSource(UniversalBaseModel): power_status: typing_extensions.Annotated[ typing.Optional[PowerSourcePowerStatus], FieldMetadata(alias="powerStatus") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="powerStatus", default=None) """ Status of the power source. """ power_type: typing_extensions.Annotated[typing.Optional[PowerSourcePowerType], FieldMetadata(alias="powerType")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="powerType", default=None) ) """ Used to determine the type of power source. """ power_level: typing_extensions.Annotated[typing.Optional[PowerLevel], FieldMetadata(alias="powerLevel")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="powerLevel", default=None) ) """ Power level of the system. If absent, the power level is assumed to be unknown. diff --git a/src/anduril/types/power_state.py b/src/anduril/types/power_state.py index 7e30c62..6458978 100644 --- a/src/anduril/types/power_state.py +++ b/src/anduril/types/power_state.py @@ -16,7 +16,7 @@ class PowerState(UniversalBaseModel): source_id_to_state: typing_extensions.Annotated[ typing.Optional[typing.Dict[str, PowerSource]], FieldMetadata(alias="sourceIdToState") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="sourceIdToState", default=None) """ This is a map where the key is a unique id of the power source and the value is additional information about the power source. diff --git a/src/anduril/types/primary_correlation.py b/src/anduril/types/primary_correlation.py index 0e733ac..9566f0b 100644 --- a/src/anduril/types/primary_correlation.py +++ b/src/anduril/types/primary_correlation.py @@ -11,7 +11,7 @@ class PrimaryCorrelation(UniversalBaseModel): secondary_entity_ids: typing_extensions.Annotated[ typing.Optional[typing.List[str]], FieldMetadata(alias="secondaryEntityIds") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="secondaryEntityIds", default=None) """ The secondary entity IDs part of this correlation. """ diff --git a/src/anduril/types/principal.py b/src/anduril/types/principal.py index bc71e06..c4a653f 100644 --- a/src/anduril/types/principal.py +++ b/src/anduril/types/principal.py @@ -22,7 +22,7 @@ class Principal(UniversalBaseModel): user: typing.Optional[User] = None team: typing.Optional[Team] = None on_behalf_of: typing_extensions.Annotated[typing.Optional["Principal"], FieldMetadata(alias="onBehalfOf")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="onBehalfOf", default=None) ) """ The Principal _this_ Principal is acting on behalf of. diff --git a/src/anduril/types/projected_frustum.py b/src/anduril/types/projected_frustum.py index 3fe3592..bd8cad8 100644 --- a/src/anduril/types/projected_frustum.py +++ b/src/anduril/types/projected_frustum.py @@ -16,28 +16,28 @@ class ProjectedFrustum(UniversalBaseModel): """ upper_left: typing_extensions.Annotated[typing.Optional[Position], FieldMetadata(alias="upperLeft")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="upperLeft", default=None) ) """ Upper left point of the frustum. """ upper_right: typing_extensions.Annotated[typing.Optional[Position], FieldMetadata(alias="upperRight")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="upperRight", default=None) ) """ Upper right point of the frustum. """ bottom_right: typing_extensions.Annotated[typing.Optional[Position], FieldMetadata(alias="bottomRight")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="bottomRight", default=None) ) """ Bottom right point of the frustum. """ bottom_left: typing_extensions.Annotated[typing.Optional[Position], FieldMetadata(alias="bottomLeft")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="bottomLeft", default=None) ) """ Bottom left point of the frustum. diff --git a/src/anduril/types/provenance.py b/src/anduril/types/provenance.py index 51978ca..e2bffe7 100644 --- a/src/anduril/types/provenance.py +++ b/src/anduril/types/provenance.py @@ -15,21 +15,21 @@ class Provenance(UniversalBaseModel): """ integration_name: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="integrationName")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="integrationName", default=None) ) """ Name of the integration that produced this entity """ data_type: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="dataType")] = pydantic.Field( - default=None + alias="dataType", default=None ) """ Source data type of this entity. Examples: ADSB, Link16, etc. """ source_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="sourceId")] = pydantic.Field( - default=None + alias="sourceId", default=None ) """ An ID that allows an element from a source to be uniquely identified @@ -37,7 +37,7 @@ class Provenance(UniversalBaseModel): source_update_time: typing_extensions.Annotated[ typing.Optional[dt.datetime], FieldMetadata(alias="sourceUpdateTime") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="sourceUpdateTime", default=None) """ The time, according to the source system, that the data in the entity was last modified. Generally, this should be the time that the source-reported time of validity of the data in the entity. This field must be @@ -45,7 +45,7 @@ class Provenance(UniversalBaseModel): """ source_description: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="sourceDescription")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="sourceDescription", default=None) ) """ Description of the modification source. In the case of a user this is the email address. diff --git a/src/anduril/types/pulse_repetition_interval.py b/src/anduril/types/pulse_repetition_interval.py index ddf6595..4a0312e 100644 --- a/src/anduril/types/pulse_repetition_interval.py +++ b/src/anduril/types/pulse_repetition_interval.py @@ -16,7 +16,7 @@ class PulseRepetitionInterval(UniversalBaseModel): pulse_repetition_interval_s: typing_extensions.Annotated[ typing.Optional[Measurement], FieldMetadata(alias="pulseRepetitionIntervalS") - ] = None + ] = pydantic.Field(alias="pulseRepetitionIntervalS", default=None) if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/anduril/types/range_rings.py b/src/anduril/types/range_rings.py index 9a09e6c..547c728 100644 --- a/src/anduril/types/range_rings.py +++ b/src/anduril/types/range_rings.py @@ -15,28 +15,28 @@ class RangeRings(UniversalBaseModel): """ min_distance_m: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="minDistanceM")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="minDistanceM", default=None) ) """ The minimum range ring distance, specified in meters. """ max_distance_m: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="maxDistanceM")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="maxDistanceM", default=None) ) """ The maximum range ring distance, specified in meters. """ ring_count: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="ringCount")] = pydantic.Field( - default=None + alias="ringCount", default=None ) """ The count of range rings. """ ring_line_color: typing_extensions.Annotated[typing.Optional[Color], FieldMetadata(alias="ringLineColor")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="ringLineColor", default=None) ) """ The color of range rings, specified in hex string. diff --git a/src/anduril/types/relations.py b/src/anduril/types/relations.py index d0f86b7..65f3cef 100644 --- a/src/anduril/types/relations.py +++ b/src/anduril/types/relations.py @@ -22,7 +22,7 @@ class Relations(UniversalBaseModel): """ parent_task_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="parentTaskId")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="parentTaskId", default=None) ) """ Identifies the parent task if the task is a sub-task. @@ -40,4 +40,4 @@ class Config: from .principal import Principal # noqa: E402, I001 -update_forward_refs(Relations) +update_forward_refs(Relations, Principal=Principal) diff --git a/src/anduril/types/relationship.py b/src/anduril/types/relationship.py index afd84fb..7fd4f3a 100644 --- a/src/anduril/types/relationship.py +++ b/src/anduril/types/relationship.py @@ -15,14 +15,14 @@ class Relationship(UniversalBaseModel): """ related_entity_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="relatedEntityId")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="relatedEntityId", default=None) ) """ The entity ID to which this entity is related. """ relationship_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="relationshipId")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="relationshipId", default=None) ) """ A unique identifier for this relationship. Allows removing or updating relationships. @@ -30,7 +30,7 @@ class Relationship(UniversalBaseModel): relationship_type: typing_extensions.Annotated[ typing.Optional[RelationshipType], FieldMetadata(alias="relationshipType") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="relationshipType", default=None) """ The relationship type """ diff --git a/src/anduril/types/relationship_type.py b/src/anduril/types/relationship_type.py index 7ef3bbc..0260b97 100644 --- a/src/anduril/types/relationship_type.py +++ b/src/anduril/types/relationship_type.py @@ -18,12 +18,20 @@ class RelationshipType(UniversalBaseModel): Determines the type of relationship between this entity and another. """ - tracked_by: typing_extensions.Annotated[typing.Optional[TrackedBy], FieldMetadata(alias="trackedBy")] = None - group_child: typing_extensions.Annotated[typing.Optional[GroupChild], FieldMetadata(alias="groupChild")] = None - group_parent: typing_extensions.Annotated[typing.Optional[GroupParent], FieldMetadata(alias="groupParent")] = None - merged_from: typing_extensions.Annotated[typing.Optional[MergedFrom], FieldMetadata(alias="mergedFrom")] = None + tracked_by: typing_extensions.Annotated[typing.Optional[TrackedBy], FieldMetadata(alias="trackedBy")] = ( + pydantic.Field(alias="trackedBy", default=None) + ) + group_child: typing_extensions.Annotated[typing.Optional[GroupChild], FieldMetadata(alias="groupChild")] = ( + pydantic.Field(alias="groupChild", default=None) + ) + group_parent: typing_extensions.Annotated[typing.Optional[GroupParent], FieldMetadata(alias="groupParent")] = ( + pydantic.Field(alias="groupParent", default=None) + ) + merged_from: typing_extensions.Annotated[typing.Optional[MergedFrom], FieldMetadata(alias="mergedFrom")] = ( + pydantic.Field(alias="mergedFrom", default=None) + ) active_target: typing_extensions.Annotated[typing.Optional[ActiveTarget], FieldMetadata(alias="activeTarget")] = ( - None + pydantic.Field(alias="activeTarget", default=None) ) if IS_PYDANTIC_V2: diff --git a/src/anduril/types/replication.py b/src/anduril/types/replication.py index a84072b..cbab471 100644 --- a/src/anduril/types/replication.py +++ b/src/anduril/types/replication.py @@ -15,7 +15,7 @@ class Replication(UniversalBaseModel): """ stale_time: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="staleTime")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="staleTime", default=None) ) """ The time by which this task should be assumed to be stale. diff --git a/src/anduril/types/rf_configuration.py b/src/anduril/types/rf_configuration.py index 67c398e..f26ada5 100644 --- a/src/anduril/types/rf_configuration.py +++ b/src/anduril/types/rf_configuration.py @@ -17,14 +17,14 @@ class RfConfiguration(UniversalBaseModel): frequency_range_hz: typing_extensions.Annotated[ typing.Optional[typing.List[FrequencyRange]], FieldMetadata(alias="frequencyRangeHz") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="frequencyRangeHz", default=None) """ Frequency ranges that are available for this sensor. """ bandwidth_range_hz: typing_extensions.Annotated[ typing.Optional[typing.List[BandwidthRange]], FieldMetadata(alias="bandwidthRangeHz") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="bandwidthRangeHz", default=None) """ Bandwidth ranges that are available for this sensor. """ diff --git a/src/anduril/types/route_details.py b/src/anduril/types/route_details.py index 725589b..a21437a 100644 --- a/src/anduril/types/route_details.py +++ b/src/anduril/types/route_details.py @@ -11,7 +11,7 @@ class RouteDetails(UniversalBaseModel): destination_name: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="destinationName")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="destinationName", default=None) ) """ Free form text giving the name of the entity's destination @@ -19,7 +19,7 @@ class RouteDetails(UniversalBaseModel): estimated_arrival_time: typing_extensions.Annotated[ typing.Optional[dt.datetime], FieldMetadata(alias="estimatedArrivalTime") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="estimatedArrivalTime", default=None) """ Estimated time of arrival at destination """ diff --git a/src/anduril/types/scan_characteristics.py b/src/anduril/types/scan_characteristics.py index 7b39c80..4c67eab 100644 --- a/src/anduril/types/scan_characteristics.py +++ b/src/anduril/types/scan_characteristics.py @@ -16,8 +16,10 @@ class ScanCharacteristics(UniversalBaseModel): scan_type: typing_extensions.Annotated[ typing.Optional[ScanCharacteristicsScanType], FieldMetadata(alias="scanType") - ] = None - scan_period_s: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="scanPeriodS")] = None + ] = pydantic.Field(alias="scanType", default=None) + scan_period_s: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="scanPeriodS")] = ( + pydantic.Field(alias="scanPeriodS", default=None) + ) if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/anduril/types/schedule.py b/src/anduril/types/schedule.py index 34ee54e..0b3b4b5 100644 --- a/src/anduril/types/schedule.py +++ b/src/anduril/types/schedule.py @@ -21,7 +21,7 @@ class Schedule(UniversalBaseModel): """ schedule_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="scheduleId")] = pydantic.Field( - default=None + alias="scheduleId", default=None ) """ A unique identifier for this schedule. @@ -29,7 +29,7 @@ class Schedule(UniversalBaseModel): schedule_type: typing_extensions.Annotated[ typing.Optional[ScheduleScheduleType], FieldMetadata(alias="scheduleType") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="scheduleType", default=None) """ The schedule type """ diff --git a/src/anduril/types/secondary_correlation.py b/src/anduril/types/secondary_correlation.py index 89fab38..ebe7e3e 100644 --- a/src/anduril/types/secondary_correlation.py +++ b/src/anduril/types/secondary_correlation.py @@ -11,7 +11,7 @@ class SecondaryCorrelation(UniversalBaseModel): primary_entity_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="primaryEntityId")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="primaryEntityId", default=None) ) """ The primary of this correlation. diff --git a/src/anduril/types/sensor.py b/src/anduril/types/sensor.py index 82ef19c..391dec5 100644 --- a/src/anduril/types/sensor.py +++ b/src/anduril/types/sensor.py @@ -19,7 +19,7 @@ class Sensor(UniversalBaseModel): """ sensor_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="sensorId")] = pydantic.Field( - default=None + alias="sensorId", default=None ) """ This generally is used to indicate a specific type at a more detailed granularity. E.g. COMInt or LWIR @@ -27,16 +27,16 @@ class Sensor(UniversalBaseModel): operational_state: typing_extensions.Annotated[ typing.Optional[SensorOperationalState], FieldMetadata(alias="operationalState") - ] = None + ] = pydantic.Field(alias="operationalState", default=None) sensor_type: typing_extensions.Annotated[typing.Optional[SensorSensorType], FieldMetadata(alias="sensorType")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="sensorType", default=None) ) """ The type of sensor """ sensor_description: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="sensorDescription")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="sensorDescription", default=None) ) """ A human readable description of the sensor @@ -44,21 +44,21 @@ class Sensor(UniversalBaseModel): rf_configuraton: typing_extensions.Annotated[ typing.Optional[RfConfiguration], FieldMetadata(alias="rfConfiguraton") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="rfConfiguraton", default=None) """ RF configuration details of the sensor """ last_detection_timestamp: typing_extensions.Annotated[ typing.Optional[dt.datetime], FieldMetadata(alias="lastDetectionTimestamp") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="lastDetectionTimestamp", default=None) """ Time of the latest detection from the sensor """ fields_of_view: typing_extensions.Annotated[ typing.Optional[typing.List[FieldOfView]], FieldMetadata(alias="fieldsOfView") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="fieldsOfView", default=None) """ Multiple fields of view for a single sensor component """ diff --git a/src/anduril/types/signal.py b/src/anduril/types/signal.py index 81a74c4..82f931f 100644 --- a/src/anduril/types/signal.py +++ b/src/anduril/types/signal.py @@ -22,12 +22,12 @@ class Signal(UniversalBaseModel): frequency_center: typing_extensions.Annotated[ typing.Optional[Frequency], FieldMetadata(alias="frequencyCenter") - ] = None + ] = pydantic.Field(alias="frequencyCenter", default=None) frequency_range: typing_extensions.Annotated[ typing.Optional[FrequencyRange], FieldMetadata(alias="frequencyRange") - ] = None + ] = pydantic.Field(alias="frequencyRange", default=None) bandwidth_hz: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="bandwidthHz")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="bandwidthHz", default=None) ) """ Indicates the bandwidth of a signal (Hz). @@ -35,24 +35,24 @@ class Signal(UniversalBaseModel): signal_to_noise_ratio: typing_extensions.Annotated[ typing.Optional[float], FieldMetadata(alias="signalToNoiseRatio") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="signalToNoiseRatio", default=None) """ Indicates the signal to noise (SNR) of this signal. """ line_of_bearing: typing_extensions.Annotated[ typing.Optional[LineOfBearing], FieldMetadata(alias="lineOfBearing") - ] = None + ] = pydantic.Field(alias="lineOfBearing", default=None) fixed: typing.Optional[Fixed] = None emitter_notations: typing_extensions.Annotated[ typing.Optional[typing.List[EmitterNotation]], FieldMetadata(alias="emitterNotations") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="emitterNotations", default=None) """ Emitter notations associated with this entity. """ pulse_width_s: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="pulseWidthS")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="pulseWidthS", default=None) ) """ length in time of a single pulse @@ -60,14 +60,14 @@ class Signal(UniversalBaseModel): pulse_repetition_interval: typing_extensions.Annotated[ typing.Optional[PulseRepetitionInterval], FieldMetadata(alias="pulseRepetitionInterval") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="pulseRepetitionInterval", default=None) """ length in time between the start of two pulses """ scan_characteristics: typing_extensions.Annotated[ typing.Optional[ScanCharacteristics], FieldMetadata(alias="scanCharacteristics") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="scanCharacteristics", default=None) """ describes how a signal is observing the environment """ diff --git a/src/anduril/types/status.py b/src/anduril/types/status.py index 671df1e..1b7525b 100644 --- a/src/anduril/types/status.py +++ b/src/anduril/types/status.py @@ -14,7 +14,7 @@ class Status(UniversalBaseModel): """ platform_activity: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="platformActivity")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="platformActivity", default=None) ) """ A string that describes the activity that the entity is performing. diff --git a/src/anduril/types/symbology.py b/src/anduril/types/symbology.py index f1b43d9..16ce130 100644 --- a/src/anduril/types/symbology.py +++ b/src/anduril/types/symbology.py @@ -14,7 +14,9 @@ class Symbology(UniversalBaseModel): Symbology associated with an entity. """ - mil_std2525c: typing_extensions.Annotated[typing.Optional[MilStd2525C], FieldMetadata(alias="milStd2525C")] = None + mil_std2525c: typing_extensions.Annotated[typing.Optional[MilStd2525C], FieldMetadata(alias="milStd2525C")] = ( + pydantic.Field(alias="milStd2525C", default=None) + ) if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/anduril/types/system.py b/src/anduril/types/system.py index 646d984..da24ea4 100644 --- a/src/anduril/types/system.py +++ b/src/anduril/types/system.py @@ -14,14 +14,14 @@ class System(UniversalBaseModel): """ service_name: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="serviceName")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="serviceName", default=None) ) """ Name of the service associated with this System. """ entity_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="entityId")] = pydantic.Field( - default=None + alias="entityId", default=None ) """ The Entity ID of the System. @@ -29,7 +29,7 @@ class System(UniversalBaseModel): manages_own_scheduling: typing_extensions.Annotated[ typing.Optional[bool], FieldMetadata(alias="managesOwnScheduling") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="managesOwnScheduling", default=None) """ Whether the System Principal (for example, an Asset) can own scheduling. This means we bypass manager-owned scheduling and defer to the system diff --git a/src/anduril/types/target_priority.py b/src/anduril/types/target_priority.py index e816e8d..1244e87 100644 --- a/src/anduril/types/target_priority.py +++ b/src/anduril/types/target_priority.py @@ -17,7 +17,7 @@ class TargetPriority(UniversalBaseModel): high_value_target: typing_extensions.Annotated[ typing.Optional[HighValueTarget], FieldMetadata(alias="highValueTarget") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="highValueTarget", default=None) """ Describes the target priority in relation to high value target lists. """ diff --git a/src/anduril/types/task.py b/src/anduril/types/task.py index c9fbc25..3907f82 100644 --- a/src/anduril/types/task.py +++ b/src/anduril/types/task.py @@ -37,7 +37,7 @@ class Task(UniversalBaseModel): """ display_name: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="displayName")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="displayName", default=None) ) """ DEPRECATED: Human readable display name for this task, should be short (<100 chars). @@ -49,14 +49,14 @@ class Task(UniversalBaseModel): """ created_by: typing_extensions.Annotated[typing.Optional["Principal"], FieldMetadata(alias="createdBy")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="createdBy", default=None) ) """ Records who created this task. This field will not change after the task has been created. """ last_updated_by: typing_extensions.Annotated[typing.Optional["Principal"], FieldMetadata(alias="lastUpdatedBy")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="lastUpdatedBy", default=None) ) """ Records who updated this task last. @@ -64,7 +64,7 @@ class Task(UniversalBaseModel): last_update_time: typing_extensions.Annotated[ typing.Optional[dt.datetime], FieldMetadata(alias="lastUpdateTime") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="lastUpdateTime", default=None) """ Records the time of last update. """ @@ -75,7 +75,7 @@ class Task(UniversalBaseModel): """ scheduled_time: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="scheduledTime")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="scheduledTime", default=None) ) """ If the task has been scheduled to execute, what time it should execute at. @@ -93,14 +93,14 @@ class Task(UniversalBaseModel): is_executed_elsewhere: typing_extensions.Annotated[ typing.Optional[bool], FieldMetadata(alias="isExecutedElsewhere") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="isExecutedElsewhere", default=None) """ If set, execution of this task is managed elsewhere, not by Task Manager. In other words, task manager will not attempt to update the assigned agent with execution instructions. """ create_time: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="createTime")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="createTime", default=None) ) """ Time of task creation. @@ -113,7 +113,7 @@ class Task(UniversalBaseModel): initial_entities: typing_extensions.Annotated[ typing.Optional[typing.List[TaskEntity]], FieldMetadata(alias="initialEntities") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="initialEntities", default=None) """ If populated, indicates an initial set of entities that can be used to execute an entity aware task For example, an entity Objective, an entity Keep In Zone, etc. @@ -139,4 +139,4 @@ class Config: from .principal import Principal # noqa: E402, I001 -update_forward_refs(Task) +update_forward_refs(Task, Principal=Principal) diff --git a/src/anduril/types/task_catalog.py b/src/anduril/types/task_catalog.py index 5116096..445cb5f 100644 --- a/src/anduril/types/task_catalog.py +++ b/src/anduril/types/task_catalog.py @@ -16,7 +16,7 @@ class TaskCatalog(UniversalBaseModel): task_definitions: typing_extensions.Annotated[ typing.Optional[typing.List[TaskDefinition]], FieldMetadata(alias="taskDefinitions") - ] = None + ] = pydantic.Field(alias="taskDefinitions", default=None) if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/anduril/types/task_definition.py b/src/anduril/types/task_definition.py index 19083e3..dc9e76b 100644 --- a/src/anduril/types/task_definition.py +++ b/src/anduril/types/task_definition.py @@ -15,7 +15,7 @@ class TaskDefinition(UniversalBaseModel): task_specification_url: typing_extensions.Annotated[ typing.Optional[str], FieldMetadata(alias="taskSpecificationUrl") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="taskSpecificationUrl", default=None) """ Url path must be prefixed with `type.googleapis.com/`. """ diff --git a/src/anduril/types/task_entity.py b/src/anduril/types/task_entity.py index 66d6471..0ae48eb 100644 --- a/src/anduril/types/task_entity.py +++ b/src/anduril/types/task_entity.py @@ -38,5 +38,7 @@ class Config: from .entity import Entity # noqa: E402, I001 +from .override import Override # noqa: E402, I001 +from .overrides import Overrides # noqa: E402, I001 -update_forward_refs(TaskEntity) +update_forward_refs(TaskEntity, Entity=Entity, Override=Override, Overrides=Overrides) diff --git a/src/anduril/types/task_error.py b/src/anduril/types/task_error.py index c8608b5..4fe1c22 100644 --- a/src/anduril/types/task_error.py +++ b/src/anduril/types/task_error.py @@ -31,7 +31,7 @@ class TaskError(UniversalBaseModel): error_details: typing_extensions.Annotated[ typing.Optional[GoogleProtobufAny], FieldMetadata(alias="errorDetails") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="errorDetails", default=None) """ Any additional details regarding this error. """ diff --git a/src/anduril/types/task_query_results.py b/src/anduril/types/task_query_results.py index d5c66a9..e7b7cfa 100644 --- a/src/anduril/types/task_query_results.py +++ b/src/anduril/types/task_query_results.py @@ -24,7 +24,7 @@ class TaskQueryResults(UniversalBaseModel): tasks: typing.Optional[typing.List[Task]] = None next_page_token: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="nextPageToken")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="nextPageToken", default=None) ) """ Incomplete results can be detected by a non-empty nextPageToken field in the query results. In order to retrieve diff --git a/src/anduril/types/task_status.py b/src/anduril/types/task_status.py index 7e2339c..132aa06 100644 --- a/src/anduril/types/task_status.py +++ b/src/anduril/types/task_status.py @@ -29,7 +29,7 @@ class TaskStatus(UniversalBaseModel): """ task_error: typing_extensions.Annotated[typing.Optional[TaskError], FieldMetadata(alias="taskError")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="taskError", default=None) ) """ Any errors associated with the task. @@ -46,7 +46,7 @@ class TaskStatus(UniversalBaseModel): """ start_time: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="startTime")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="startTime", default=None) ) """ Time the task began execution, may not be known even for executing Tasks. diff --git a/src/anduril/types/task_version.py b/src/anduril/types/task_version.py index a5f60b0..1189b02 100644 --- a/src/anduril/types/task_version.py +++ b/src/anduril/types/task_version.py @@ -18,14 +18,14 @@ class TaskVersion(UniversalBaseModel): """ task_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="taskId")] = pydantic.Field( - default=None + alias="taskId", default=None ) """ The unique identifier for this task, used to distinguish it from all other tasks in the system. """ definition_version: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="definitionVersion")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="definitionVersion", default=None) ) """ Counter that increments on changes to the task definition. @@ -33,7 +33,7 @@ class TaskVersion(UniversalBaseModel): """ status_version: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="statusVersion")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="statusVersion", default=None) ) """ Counter that increments on changes to TaskStatus. diff --git a/src/anduril/types/team.py b/src/anduril/types/team.py index ac5e422..415471b 100644 --- a/src/anduril/types/team.py +++ b/src/anduril/types/team.py @@ -15,7 +15,7 @@ class Team(UniversalBaseModel): """ entity_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="entityId")] = pydantic.Field( - default=None + alias="entityId", default=None ) """ Entity ID of the team diff --git a/src/anduril/types/threat.py b/src/anduril/types/threat.py index d6bcb10..e97d8ab 100644 --- a/src/anduril/types/threat.py +++ b/src/anduril/types/threat.py @@ -14,7 +14,7 @@ class Threat(UniversalBaseModel): """ is_threat: typing_extensions.Annotated[typing.Optional[bool], FieldMetadata(alias="isThreat")] = pydantic.Field( - default=None + alias="isThreat", default=None ) """ Indicates that the entity has been determined to be a threat. diff --git a/src/anduril/types/tle_parameters.py b/src/anduril/types/tle_parameters.py index 1a1e1c0..3c6cdc9 100644 --- a/src/anduril/types/tle_parameters.py +++ b/src/anduril/types/tle_parameters.py @@ -10,7 +10,7 @@ class TleParameters(UniversalBaseModel): ephemeris_type: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="ephemerisType")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="ephemerisType", default=None) ) """ Integer specifying TLE ephemeris type @@ -18,21 +18,23 @@ class TleParameters(UniversalBaseModel): classification_type: typing_extensions.Annotated[ typing.Optional[str], FieldMetadata(alias="classificationType") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="classificationType", default=None) """ User-defined free-text message classification/caveats of this TLE """ norad_cat_id: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="noradCatId")] = pydantic.Field( - default=None + alias="noradCatId", default=None ) """ Norad catalog number: integer up to nine digits. """ - element_set_no: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="elementSetNo")] = None + element_set_no: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="elementSetNo")] = ( + pydantic.Field(alias="elementSetNo", default=None) + ) rev_at_epoch: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="revAtEpoch")] = pydantic.Field( - default=None + alias="revAtEpoch", default=None ) """ Optional: revolution number @@ -49,14 +51,14 @@ class TleParameters(UniversalBaseModel): """ mean_motion_dot: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="meanMotionDot")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="meanMotionDot", default=None) ) """ First time derivative of mean motion in rev / day^2 """ mean_motion_ddot: typing_extensions.Annotated[typing.Optional[float], FieldMetadata(alias="meanMotionDdot")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="meanMotionDdot", default=None) ) """ Second time derivative of mean motion in rev / day^3. For use with SGP or PPT3. diff --git a/src/anduril/types/tracked.py b/src/anduril/types/tracked.py index 781c054..fdce7f3 100644 --- a/src/anduril/types/tracked.py +++ b/src/anduril/types/tracked.py @@ -18,13 +18,13 @@ class Tracked(UniversalBaseModel): track_quality_wrapper: typing_extensions.Annotated[ typing.Optional[int], FieldMetadata(alias="trackQualityWrapper") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="trackQualityWrapper", default=None) """ Quality score, 0-15, nil if none """ sensor_hits: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="sensorHits")] = pydantic.Field( - default=None + alias="sensorHits", default=None ) """ Sensor hits aggregation on the tracked entity. @@ -32,7 +32,7 @@ class Tracked(UniversalBaseModel): number_of_objects: typing_extensions.Annotated[ typing.Optional[UInt32Range], FieldMetadata(alias="numberOfObjects") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="numberOfObjects", default=None) """ Estimated number of objects or units that are represented by this entity. Known as Strength in certain contexts (Link16) if UpperBound == LowerBound; (strength = LowerBound) @@ -44,7 +44,7 @@ class Tracked(UniversalBaseModel): radar_cross_section: typing_extensions.Annotated[ typing.Optional[float], FieldMetadata(alias="radarCrossSection") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="radarCrossSection", default=None) """ The radar cross section (RCS) is a measure of how detectable an object is by radar. A large RCS indicates an object is more easily detected. The unit is β€œdecibels per square meter,” or dBsm @@ -52,14 +52,14 @@ class Tracked(UniversalBaseModel): last_measurement_time: typing_extensions.Annotated[ typing.Optional[dt.datetime], FieldMetadata(alias="lastMeasurementTime") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="lastMeasurementTime", default=None) """ Timestamp of the latest tracking measurement for this entity. """ line_of_bearing: typing_extensions.Annotated[ typing.Optional[LineOfBearing], FieldMetadata(alias="lineOfBearing") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="lineOfBearing", default=None) """ The relative position of a track with respect to the entity that is tracking it. Used for tracks that do not yet have a 3D position. For this entity (A), being tracked by some entity (B), this LineOfBearing would express a ray from B to A. diff --git a/src/anduril/types/tracked_by.py b/src/anduril/types/tracked_by.py index 4c1ca7e..cdf3be5 100644 --- a/src/anduril/types/tracked_by.py +++ b/src/anduril/types/tracked_by.py @@ -18,7 +18,7 @@ class TrackedBy(UniversalBaseModel): actively_tracking_sensors: typing_extensions.Annotated[ typing.Optional[Sensors], FieldMetadata(alias="activelyTrackingSensors") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="activelyTrackingSensors", default=None) """ Sensor details of the tracking entity's sensors that were active and tracking the tracked entity. This may be a subset of the total sensors available on the tracking entity. @@ -26,7 +26,7 @@ class TrackedBy(UniversalBaseModel): last_measurement_timestamp: typing_extensions.Annotated[ typing.Optional[dt.datetime], FieldMetadata(alias="lastMeasurementTimestamp") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="lastMeasurementTimestamp", default=None) """ Latest time that any sensor in actively_tracking_sensors detected the tracked entity. """ diff --git a/src/anduril/types/transponder_codes.py b/src/anduril/types/transponder_codes.py index 946a8e9..b7d18da 100644 --- a/src/anduril/types/transponder_codes.py +++ b/src/anduril/types/transponder_codes.py @@ -33,7 +33,7 @@ class TransponderCodes(UniversalBaseModel): mode4interrogation_response: typing_extensions.Annotated[ typing.Optional[TransponderCodesMode4InterrogationResponse], FieldMetadata(alias="mode4InterrogationResponse") - ] = pydantic.Field(default=None) + ] = pydantic.Field(alias="mode4InterrogationResponse", default=None) """ The validity of the response from the Mode 4 interrogation. """ @@ -44,7 +44,7 @@ class TransponderCodes(UniversalBaseModel): """ mode_s: typing_extensions.Annotated[typing.Optional[ModeS], FieldMetadata(alias="modeS")] = pydantic.Field( - default=None + alias="modeS", default=None ) """ The Mode S transponder codes. diff --git a/src/anduril/types/u_int32range.py b/src/anduril/types/u_int32range.py index feee7c1..052568f 100644 --- a/src/anduril/types/u_int32range.py +++ b/src/anduril/types/u_int32range.py @@ -9,8 +9,12 @@ class UInt32Range(UniversalBaseModel): - lower_bound: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="lowerBound")] = None - upper_bound: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="upperBound")] = None + lower_bound: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="lowerBound")] = pydantic.Field( + alias="lowerBound", default=None + ) + upper_bound: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="upperBound")] = pydantic.Field( + alias="upperBound", default=None + ) if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/anduril/types/unauthorized_error_body.py b/src/anduril/types/unauthorized_error_body.py new file mode 100644 index 0000000..d2bce27 --- /dev/null +++ b/src/anduril/types/unauthorized_error_body.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class UnauthorizedErrorBody(UniversalBaseModel): + error: str + error_description: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/anduril/types/user.py b/src/anduril/types/user.py index db5eaa8..d1f3b64 100644 --- a/src/anduril/types/user.py +++ b/src/anduril/types/user.py @@ -14,7 +14,7 @@ class User(UniversalBaseModel): """ user_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="userId")] = pydantic.Field( - default=None + alias="userId", default=None ) """ The User ID associated with this User. diff --git a/src/anduril/types/visual_details.py b/src/anduril/types/visual_details.py index 61d753c..8ca51f3 100644 --- a/src/anduril/types/visual_details.py +++ b/src/anduril/types/visual_details.py @@ -15,7 +15,7 @@ class VisualDetails(UniversalBaseModel): """ range_rings: typing_extensions.Annotated[typing.Optional[RangeRings], FieldMetadata(alias="rangeRings")] = ( - pydantic.Field(default=None) + pydantic.Field(alias="rangeRings", default=None) ) """ The range rings to display around an entity. diff --git a/tests/utils/test_http_client.py b/tests/utils/test_http_client.py index 65c68e4..78a70d4 100644 --- a/tests/utils/test_http_client.py +++ b/tests/utils/test_http_client.py @@ -1,13 +1,57 @@ # This file was auto-generated by Fern from our API Definition. -from anduril.core.http_client import get_request_body +from typing import Any, Dict + +import pytest + +from anduril.core.http_client import ( + AsyncHttpClient, + HttpClient, + _build_url, + get_request_body, + remove_none_from_dict, +) from anduril.core.request_options import RequestOptions +# Stub clients for testing HttpClient and AsyncHttpClient +class _DummySyncClient: + """A minimal stub for httpx.Client that records request arguments.""" + + def __init__(self) -> None: + self.last_request_kwargs: Dict[str, Any] = {} + + def request(self, **kwargs: Any) -> "_DummyResponse": + self.last_request_kwargs = kwargs + return _DummyResponse() + + +class _DummyAsyncClient: + """A minimal stub for httpx.AsyncClient that records request arguments.""" + + def __init__(self) -> None: + self.last_request_kwargs: Dict[str, Any] = {} + + async def request(self, **kwargs: Any) -> "_DummyResponse": + self.last_request_kwargs = kwargs + return _DummyResponse() + + +class _DummyResponse: + """A minimal stub for httpx.Response.""" + + status_code = 200 + headers: Dict[str, str] = {} + + def get_request_options() -> RequestOptions: return {"additional_body_parameters": {"see you": "later"}} +def get_request_options_with_none() -> RequestOptions: + return {"additional_body_parameters": {"see you": "later", "optional": None}} + + def test_get_json_request_body() -> None: json_body, data_body = get_request_body(json={"hello": "world"}, data=None, request_options=None, omit=None) assert json_body == {"hello": "world"} @@ -48,14 +92,209 @@ def test_get_none_request_body() -> None: def test_get_empty_json_request_body() -> None: + """Test that implicit empty bodies (json=None) are collapsed to None.""" unrelated_request_options: RequestOptions = {"max_retries": 3} json_body, data_body = get_request_body(json=None, data=None, request_options=unrelated_request_options, omit=None) assert json_body is None assert data_body is None - json_body_extras, data_body_extras = get_request_body( - json={}, data=None, request_options=unrelated_request_options, omit=None + +def test_explicit_empty_json_body_is_preserved() -> None: + """Test that explicit empty bodies (json={}) are preserved and sent as {}. + + This is important for endpoints where the request body is required but all + fields are optional. The server expects valid JSON ({}) not an empty body. + """ + unrelated_request_options: RequestOptions = {"max_retries": 3} + + # Explicit json={} should be preserved + json_body, data_body = get_request_body(json={}, data=None, request_options=unrelated_request_options, omit=None) + assert json_body == {} + assert data_body is None + + # Explicit data={} should also be preserved + json_body2, data_body2 = get_request_body(json=None, data={}, request_options=unrelated_request_options, omit=None) + assert json_body2 is None + assert data_body2 == {} + + +def test_json_body_preserves_none_values() -> None: + """Test that JSON bodies preserve None values (they become JSON null).""" + json_body, data_body = get_request_body( + json={"hello": "world", "optional": None}, data=None, request_options=None, omit=None ) + # JSON bodies should preserve None values + assert json_body == {"hello": "world", "optional": None} + assert data_body is None - assert json_body_extras is None - assert data_body_extras is None + +def test_data_body_preserves_none_values_without_multipart() -> None: + """Test that data bodies preserve None values when not using multipart. + + The filtering of None values happens in HttpClient.request/stream methods, + not in get_request_body. This test verifies get_request_body doesn't filter None. + """ + json_body, data_body = get_request_body( + json=None, data={"hello": "world", "optional": None}, request_options=None, omit=None + ) + # get_request_body should preserve None values in data body + # The filtering happens later in HttpClient.request when multipart is detected + assert data_body == {"hello": "world", "optional": None} + assert json_body is None + + +def test_remove_none_from_dict_filters_none_values() -> None: + """Test that remove_none_from_dict correctly filters out None values.""" + original = {"hello": "world", "optional": None, "another": "value", "also_none": None} + filtered = remove_none_from_dict(original) + assert filtered == {"hello": "world", "another": "value"} + # Original should not be modified + assert original == {"hello": "world", "optional": None, "another": "value", "also_none": None} + + +def test_remove_none_from_dict_empty_dict() -> None: + """Test that remove_none_from_dict handles empty dict.""" + assert remove_none_from_dict({}) == {} + + +def test_remove_none_from_dict_all_none() -> None: + """Test that remove_none_from_dict handles dict with all None values.""" + assert remove_none_from_dict({"a": None, "b": None}) == {} + + +def test_http_client_does_not_pass_empty_params_list() -> None: + """Test that HttpClient passes params=None when params are empty. + + This prevents httpx from stripping existing query parameters from the URL, + which happens when params=[] or params={} is passed. + """ + dummy_client = _DummySyncClient() + http_client = HttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + ) + + # Use a path with query params (e.g., pagination cursor URL) + http_client.request( + path="resource?after=123", + method="GET", + params=None, + request_options=None, + ) + + # We care that httpx receives params=None, not [] or {} + assert "params" in dummy_client.last_request_kwargs + assert dummy_client.last_request_kwargs["params"] is None + + # Verify the query string in the URL is preserved + url = str(dummy_client.last_request_kwargs["url"]) + assert "after=123" in url, f"Expected query param 'after=123' in URL, got: {url}" + + +def test_http_client_passes_encoded_params_when_present() -> None: + """Test that HttpClient passes encoded params when params are provided.""" + dummy_client = _DummySyncClient() + http_client = HttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com/resource", + ) + + http_client.request( + path="", + method="GET", + params={"after": "456"}, + request_options=None, + ) + + params = dummy_client.last_request_kwargs["params"] + # For a simple dict, encode_query should give a single (key, value) tuple + assert params == [("after", "456")] + + +@pytest.mark.asyncio +async def test_async_http_client_does_not_pass_empty_params_list() -> None: + """Test that AsyncHttpClient passes params=None when params are empty. + + This prevents httpx from stripping existing query parameters from the URL, + which happens when params=[] or params={} is passed. + """ + dummy_client = _DummyAsyncClient() + http_client = AsyncHttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + async_base_headers=None, + ) + + # Use a path with query params (e.g., pagination cursor URL) + await http_client.request( + path="resource?after=123", + method="GET", + params=None, + request_options=None, + ) + + # We care that httpx receives params=None, not [] or {} + assert "params" in dummy_client.last_request_kwargs + assert dummy_client.last_request_kwargs["params"] is None + + # Verify the query string in the URL is preserved + url = str(dummy_client.last_request_kwargs["url"]) + assert "after=123" in url, f"Expected query param 'after=123' in URL, got: {url}" + + +@pytest.mark.asyncio +async def test_async_http_client_passes_encoded_params_when_present() -> None: + """Test that AsyncHttpClient passes encoded params when params are provided.""" + dummy_client = _DummyAsyncClient() + http_client = AsyncHttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com/resource", + async_base_headers=None, + ) + + await http_client.request( + path="", + method="GET", + params={"after": "456"}, + request_options=None, + ) + + params = dummy_client.last_request_kwargs["params"] + # For a simple dict, encode_query should give a single (key, value) tuple + assert params == [("after", "456")] + + +def test_basic_url_joining() -> None: + """Test basic URL joining with a simple base URL and path.""" + result = _build_url("https://api.example.com", "/users") + assert result == "https://api.example.com/users" + + +def test_basic_url_joining_trailing_slash() -> None: + """Test basic URL joining with a simple base URL and path.""" + result = _build_url("https://api.example.com/", "/users") + assert result == "https://api.example.com/users" + + +def test_preserves_base_url_path_prefix() -> None: + """Test that path prefixes in base URL are preserved. + + This is the critical bug fix - urllib.parse.urljoin() would strip + the path prefix when the path starts with '/'. + """ + result = _build_url("https://cloud.example.com/org/tenant/api", "/users") + assert result == "https://cloud.example.com/org/tenant/api/users" + + +def test_preserves_base_url_path_prefix_trailing_slash() -> None: + """Test that path prefixes in base URL are preserved.""" + result = _build_url("https://cloud.example.com/org/tenant/api/", "/users") + assert result == "https://cloud.example.com/org/tenant/api/users"