diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 33a8317..9fa03b9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,3 +33,24 @@ jobs: - name: pylint run: uv run python -m pylint -j=0 src/ttd_data + + tests: + name: Unit tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up uv + uses: astral-sh/setup-uv@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install dependencies + run: uv sync --group dev + + - name: pytest + run: uv run python -m pytest tests/unit -v diff --git a/.speakeasy/gen.lock b/.speakeasy/gen.lock index fbd3259..cca4f56 100644 --- a/.speakeasy/gen.lock +++ b/.speakeasy/gen.lock @@ -1,19 +1,19 @@ lockVersion: 2.0.0 id: 1927f304-b110-462d-9e13-326cfa243f23 management: - docChecksum: fb006fd06e67532d16416cec0e274569 + docChecksum: 0988543598ffd99d9386e7c1c9f68883 docVersion: v0.1 - speakeasyVersion: 1.761.5 - generationVersion: 2.879.13 - releaseVersion: 0.1.7 - configChecksum: 8ebdb49324f528793885a1c33d3f967e + speakeasyVersion: 1.763.1 + generationVersion: 2.884.4 + releaseVersion: 0.2.10 + configChecksum: 800480b0b00be47d7d919186ab6eb792 repoURL: https://github.com/thetradedesk/ttd-data-python.git installationURL: https://github.com/thetradedesk/ttd-data-python.git published: true persistentEdits: - generation_id: ac4ff5be-36ce-4408-83fc-93a897ae09da - pristine_commit_hash: 286e468c28765cacbb31638a378e2d9e39406ec0 - pristine_tree_hash: 2160196b31e01c060e4aa5650df2b3110bd4ec93 + generation_id: 008313c7-9769-406e-adb4-d64d7e3fa900 + pristine_commit_hash: 7bde0e4f230cf3703db7b98f76c3138ae7788f45 + pristine_tree_hash: f7bbac3f3cc3bad51bbb6dff3171d27e34cc8c11 features: python: additionalDependencies: 1.0.0 @@ -29,9 +29,10 @@ features: globalServerURLs: 3.2.1 methodArguments: 1.0.2 methodServerURLs: 3.1.2 + nameOverrides: 3.0.3 nullables: 1.0.2 responseFormat: 1.1.0 - retries: 3.0.4 + retries: 3.0.5 sdkHooks: 1.2.1 trackedFiles: .devcontainer/README.md: @@ -56,8 +57,8 @@ trackedFiles: pristine_git_object: 8d79f0abb72526f1fb34a4c03e5bba612c6ba2ae USAGE.md: id: 3aed33ce6e6f - last_write_checksum: sha1:3ac00d07d7d970aeb34833ccbe2b818e56bd138d - pristine_git_object: b008f38be01b2124098266c1125ab26d8e4497d9 + last_write_checksum: sha1:ad626f91bf720060db8a304d2ab029b003b0ead1 + pristine_git_object: d57ef9032e3c78fb988abb1e9b32f5fdc76bc2af docs/errors/advertiserdataserverresponseerror.md: id: 6995891cb1b0 last_write_checksum: sha1:efb0f825612b14c3f84ddcd57b80a6b4f76b4c41 @@ -86,14 +87,10 @@ trackedFiles: id: d60a483f716a last_write_checksum: sha1:e6fc5df5e3a60f048afc267262bf29da7019d038 pristine_git_object: be2fbb0dbd6cfbc83938a3280cdef129d78d0acc - docs/models/advertiserdataitem.md: - id: eb558e9e260a - last_write_checksum: sha1:88a1e07c88f15f280d60eda18da65bcc09b8542a - pristine_git_object: f6ed68644c0519bf8e03efb77f809d8c9afffa26 docs/models/advertiserdatarequest.md: id: 154c375c92a3 - last_write_checksum: sha1:7793311f1f86023d0406d8cded9154d99ec6815e - pristine_git_object: 4de3a3731f46757f098e4430812ae504cbfa22c8 + last_write_checksum: sha1:c52e129f515454e24cbff63aea18d3ccf5a99ee0 + pristine_git_object: 7fabf56853f463181c0cebe03d8d2123d1d33137 docs/models/advertiserdataresponseerrorcode.md: id: 5f207b4aeb44 last_write_checksum: sha1:c6286cd1b3dc8ef6280ab3f1443db5ea5e8b9a15 @@ -112,12 +109,28 @@ trackedFiles: pristine_git_object: 3787f86195647102a57ab3b7dc0395b10a07bd19 docs/models/advertiserdsrrequest.md: id: cc9b99b40db7 - last_write_checksum: sha1:0cebd2dc1565db6b8c5742c4e8baf0d9db45cba8 - pristine_git_object: 8d7c5492f65b7cf6578ba8e816557f2a266c2f17 + last_write_checksum: sha1:5472aa827ff3f7ec12bf3d0b218ceccf1a40b30a + pristine_git_object: cc6a6291bf512ca3f3ba448ee923a1476222416d docs/models/advertiserdsrresponse.md: id: 3a4985474ff0 last_write_checksum: sha1:532cc8a4549e15f7f86cb291945d589a73a737a3 pristine_git_object: 3b1d22daa091ac98e81dcebc9ce395c927a30462 + docs/models/baseadvertiserdataitem.md: + id: 97e86ecee3d8 + last_write_checksum: sha1:64980fbdc0921ceef19ed62406a12992fde9ed09 + pristine_git_object: 3cfde39c6bb45549c0c941d4e282fc79cd763ddb + docs/models/baseofflineconversiondataitem.md: + id: 13bfd96ba4aa + last_write_checksum: sha1:3a236a50f5d08f4751b96863c110a00f0c800b79 + pristine_git_object: 69e0b5abce0893419f0aec3b3360397825a3b3f8 + docs/models/basepartnerdsrdataitem.md: + id: bb2738e1cf3b + last_write_checksum: sha1:7220e8ad7c7d0ac8035486b98b73149078d22e33 + pristine_git_object: 2d39ff01099ec32980af100d2b2760b4e5a29366 + docs/models/basethirdpartydataitem.md: + id: 20f1f84d4720 + last_write_checksum: sha1:67211335d418fd0c1c7b1c96c461c24067593395 + pristine_git_object: 84a6fe24f107018816eecade533e1caa351d8cd9 docs/models/dataorigin.md: id: 1d98f1297ba8 last_write_checksum: sha1:4c6d538b85d82101a460bf10664af23f61453d83 @@ -188,20 +201,16 @@ trackedFiles: pristine_git_object: 87831454d04711fe80251c056ff0fad3613f43a4 docs/models/merchantdsrrequest.md: id: 58eb9bbd97d8 - last_write_checksum: sha1:e88f98943136e4aa0c428b1e21f0eb6137d50be9 - pristine_git_object: 36386b6cf6e595fd9f7dce4e4cfc21932a7e2390 + last_write_checksum: sha1:66d95504c7c4903fe1dbbf6de54c6babbf3ad16a + pristine_git_object: b86991094893b806cbd65dca4922c0ccbe063e30 docs/models/merchantdsrresponse.md: id: 0c655a98f8a8 last_write_checksum: sha1:96db869276b2e953fb2d87b2acf97a4134d383f0 pristine_git_object: 966094a9aceaf72e702ba375fcc0d2a6d49bbca8 - docs/models/offlineconversiondataitem.md: - id: fe82dd27a28c - last_write_checksum: sha1:d63aa0e9535337a0bf754d8b6ea501ffbb3e19b3 - pristine_git_object: dce79520e7faab92fbacfe947abf683805c1e4e7 docs/models/offlineconversiondatarequest.md: id: df028750a55e - last_write_checksum: sha1:cea5ba0e097890514574cf2c63611b400efdcffe - pristine_git_object: 0a20f8fd35bd46a30c72d3369722ccf494ef1064 + last_write_checksum: sha1:8b7bcb85e465b85e675fd4f99bf49c383497715f + pristine_git_object: cf44e24d1a1bdb4545224a0ce0706b0382273b7d docs/models/offlineconversiondataresponseerrorcode.md: id: 98d40405a074 last_write_checksum: sha1:9f20eb8f388efb02e4c7c48bbe6d690b008f5d68 @@ -212,12 +221,8 @@ trackedFiles: pristine_git_object: 97d79ae6061aa9e0562f27cae8611761d32d5f8f docs/models/offlineconversiondataserverresponseline.md: id: 55416806a184 - last_write_checksum: sha1:6d32ab05e638ae8a6af32ea30477cc672751c7c6 - pristine_git_object: dcec856d3c04a6e515eedc3fac823c6f4e86dadd - docs/models/partnerdsrdataitem.md: - id: aec5a66af0f5 - last_write_checksum: sha1:271a8664c9bbb8779f8a3956b9c7fd01137cb160 - pristine_git_object: 8018d0a5a178c09967914b760f193112e89ec20a + last_write_checksum: sha1:7fcd5b4b1cc49beecc3628be9aa736914b863f30 + pristine_git_object: 8f9cb7907035e8c24bdf79f1075c66fb3883d841 docs/models/partnerdsrrequesttype.md: id: b3c618fce057 last_write_checksum: sha1:e5197df6bec2d0525617c973975aa464ed4d9470 @@ -234,14 +239,10 @@ trackedFiles: id: bc4204726f1d last_write_checksum: sha1:0f89c863c85f83105c6f81d8e4e3ac7606b90aa3 pristine_git_object: 3f39a30828259d5dfc63ac086e35fd408702c69c - docs/models/thirdpartydataitem.md: - id: 2f10d185f4c2 - last_write_checksum: sha1:9c34e5754f5a6005c839e303cb290f3c9bc06d8a - pristine_git_object: 516c15c35cc2feed06ca4fa22589093b50820699 docs/models/thirdpartydatarequest.md: id: dfbab370baa7 - last_write_checksum: sha1:5dc172178a879bbd4669818657f8a1b6b1e33ab0 - pristine_git_object: 0a23821c7a3f8f0c5bece4ebd95008c5b19bd65a + last_write_checksum: sha1:7cab0242ffa1771e5e30290ea1b97d630e09488c + pristine_git_object: d3b0f39293f9f0831e4d61595d7e49162de3ca12 docs/models/thirdpartydataresponseerrorcode.md: id: a686bd16bff1 last_write_checksum: sha1:7e3a32cef065489c78978734ed2f7fdc8ada2721 @@ -260,8 +261,8 @@ trackedFiles: pristine_git_object: fa0e3e10f39730538d20b3f3345ddfd114e90fa5 docs/models/thirdpartydsrrequest.md: id: 922d0673f906 - last_write_checksum: sha1:af20f751f256981834560a571f48723b9b9ccab9 - pristine_git_object: 2589e5f2a8812c4dc8bf302d312de8c278bf9ff5 + last_write_checksum: sha1:e54a515f9e74a2195a1ab3aaaf4955b5cc774ba9 + pristine_git_object: 4c80526ba6332e76d10597082c4418784f5a5b7b docs/models/thirdpartydsrresponse.md: id: 6eb8ed3d0472 last_write_checksum: sha1:fd1ca29bd7c899001a40eaad74f625970c3f7c07 @@ -272,20 +273,20 @@ trackedFiles: pristine_git_object: 69dd549ec7f5f885101d08dd502e25748183aebf docs/sdks/advertiser/README.md: id: 099f9b8a0a2d - last_write_checksum: sha1:e5fbf63819e2c06e47fd561fb5fc44fdb8f02b63 - pristine_git_object: 2c91855c23f7241f968d9d7667720ec997b7ab95 + last_write_checksum: sha1:9d95190b7e3382151be56f08f4de93fd7007df78 + pristine_git_object: d5cc8e44df0abec4d05b1455040db5d429013ebb docs/sdks/deletionoptout/README.md: id: 61acd35826cb - last_write_checksum: sha1:a36d441d1dcd3c9af546afb711db4380028c56ad - pristine_git_object: aa104aa6181dcf4f28ccb96d0aca726aa51d1b8d + last_write_checksum: sha1:beeb8405200cf8393b779b9e1d9509d5ce08f658 + pristine_git_object: 6d6d74e5b9dc5bad0f5a36e5bc42e9deb26fa9c8 docs/sdks/offlineconversion/README.md: id: 3b6eaeea93ac - last_write_checksum: sha1:730838d08e6ee7b886553aae64eadc82dab77181 - pristine_git_object: d248492461ccef96e4f457c8fdc521a9e63d15a1 + last_write_checksum: sha1:2886aea5236252fd87cbb7166172eb1cad92397d + pristine_git_object: 1ba0e98627a163480d17ff80f25b2321eba06c37 docs/sdks/thirdparty/README.md: id: 2ea7384a00fc - last_write_checksum: sha1:70fa920bcfa24f1abf8ab945e04bbe5052523513 - pristine_git_object: 7bfccddc7beddc2a09d52e6fc5004b0dd2e853da + last_write_checksum: sha1:28d5375aab7f830c3605685388726aaf3a63dd8a + pristine_git_object: 040b31dc743465011719a81355a6df997ca08632 py.typed: id: 258c3ed47ae4 last_write_checksum: sha1:8efc425ffe830805ffcc0f3055871bdcdc542c60 @@ -296,360 +297,360 @@ trackedFiles: pristine_git_object: f456032107a9387ba6c98afd1c981df2f4b3d636 pyproject.toml: id: 5d07e7d72637 - last_write_checksum: sha1:fbdd2d81bb4dc3e0647a7ee235eb74dbffca1de6 - pristine_git_object: 646963bccee0e22e07d256953138555a84a99c4c + last_write_checksum: sha1:da74cb3af5b55c80cc9b47fc5b2ef58b7f6a9ad6 + pristine_git_object: dd2dbf7bb0983f837c9747630e1cc7f18110e343 scripts/prepare_readme.py: id: e0c5957a6035 - last_write_checksum: sha1:a2b33b92e3f8fed0f4be1d661d68b6decdf9f525 - pristine_git_object: 1ab3ac1a039b07dd3a6a74d918098647090c8951 + last_write_checksum: sha1:5a79be5a1346a05f9099f1177f4b3605849c3b6b + pristine_git_object: 318c29f0ad177f0852c387e34dbda5f7f645d839 scripts/publish.sh: id: fe273b08f514 last_write_checksum: sha1:b290b25b36dca3d5eb1a2e66a2e1bcf2e7326cf3 pristine_git_object: c35748f360329c2bc370e9b189f49b1a360b2c48 src/ttd_data/__init__.py: id: 48aae23daf2a - last_write_checksum: sha1:da077c0bdfcef64a4a5aea91a17292f72fa2b088 - pristine_git_object: 833c68cd526fe34aab2b7e7c45f974f7f4b9e120 + last_write_checksum: sha1:fc682eed8ff97ad592a19ab534495263176ad10c + pristine_git_object: 25fb88f91c4f26fb66b46e8daaa1d5d718fdab49 src/ttd_data/_hooks/__init__.py: id: e02546b7a6fe - last_write_checksum: sha1:e3111289afd28ad557c21d9e2f918caabfb7037d - pristine_git_object: 2ee66cdd592fe41731c24ddd407c8ca31c50aec1 + last_write_checksum: sha1:5f5636716563ac60b04ee7230106e44fe7732c35 + pristine_git_object: 7edc66ebd0452d6a48f2b27cb3a21b9c061381d9 src/ttd_data/_hooks/sdkhooks.py: id: f305e5b16c4d - last_write_checksum: sha1:6719e8772a9d569d041ab8e898b6385f376871e6 - pristine_git_object: 765a3f212bc33986f23bca22e2f83f79ca1586f7 + last_write_checksum: sha1:9fb890fbb72b3cc58005282d97a5e45de7ac7934 + pristine_git_object: 631ae89669006263153e29627c0927978d59f11f src/ttd_data/_hooks/types.py: id: 9bc3634baba5 - last_write_checksum: sha1:443998ed662ef73b770a29bdba356cd2f6fe351d - pristine_git_object: 232e51401d91ec209d9de0c41a959ffc37c2eadb + last_write_checksum: sha1:cc91afcfbecfc3d5ee50dd65c46f45b5ebab97ab + pristine_git_object: 4056118bd6df7bb0989a9734404c082cfca19e09 src/ttd_data/_version.py: id: 7feb4586507e - last_write_checksum: sha1:c1a0ddf11b192d3c15a98d695cf2243b77257205 - pristine_git_object: 1cb357f02e24126fa9d7a9a05a00b69c7c3c3ee2 + last_write_checksum: sha1:34d41ba3657a46c7a3fc8611ac9335db11d1153d + pristine_git_object: 65cf660d277aeed128d5959f6f1f68d0378de35a src/ttd_data/advertiser.py: id: 392ead635b4f - last_write_checksum: sha1:36d9b5902e62972e93e01502546c9b3b95fe6015 - pristine_git_object: d0cfd507c00fad54640b6033fdf4757101e85d10 + last_write_checksum: sha1:3069a1cb2d2393436adbc5e22cb1aa9961aafdd8 + pristine_git_object: eeb11f9b3e324308e8897e2ba284a3d55241ee26 src/ttd_data/basesdk.py: id: 28e634bcfb11 - last_write_checksum: sha1:008cbd9d2a436d49f143302d5500588c440ccea6 - pristine_git_object: 1b6ea88b9925df9c48461b7270dbc2364698d069 + last_write_checksum: sha1:94fb8380b03b78312f2951f566bab98f1d0bceee + pristine_git_object: b3e7ee7ab69e5875ef7bf64033f4198e51dc8995 src/ttd_data/deletionoptout.py: id: 8da72b51c89e - last_write_checksum: sha1:c415f213bd99412162e2e03e6e074ad0d9236b11 - pristine_git_object: f21fb99a74704dd52e9de6e77938b0b318e6ee42 + last_write_checksum: sha1:d9e4f10eb9506b11f814c0608899fd1a75b68160 + pristine_git_object: 7383244c2e2cbbfc406fee1e6a555bfba09c6c54 src/ttd_data/errors/__init__.py: id: c4bbecf8701c - last_write_checksum: sha1:d9d162398e0fbe29af5f0d7025397e961926a175 - pristine_git_object: 92226d72ca16adc0d2d05f163e79d8daa1f15d8e + last_write_checksum: sha1:735babcd4826aaac1885a6353bcbd0300d56e52a + pristine_git_object: d605f06d49488d19164cfb109f60245970337b9d src/ttd_data/errors/advertiserdataserverresponse_error.py: id: b132387f8548 - last_write_checksum: sha1:cd6dcf78075f3b9c9a10632e6d20b70de86ed34e - pristine_git_object: aef86c47dff4137fb23aa0fccac27662a84ccc48 + last_write_checksum: sha1:37a6c02448ea5387ccd6bbbf90fc99ce9e16400d + pristine_git_object: 83f1ff9cc9fc553f2b147bbb03d298c5a133e2d9 src/ttd_data/errors/advertiserdsrresponse_error.py: id: 72d43ab98596 - last_write_checksum: sha1:dc98275fca9fee199b323f3cedff908204e38912 - pristine_git_object: 2072b2cd50a41587790e0ddbf88e772db56ed8e5 + last_write_checksum: sha1:4069f2ab7f7fb549b0513acb0a980f071c0ce15e + pristine_git_object: f7ec7cb9da684466a1c05c64b1e2337176cc169d src/ttd_data/errors/apierror.py: id: 87e982b3d2ab - last_write_checksum: sha1:78ee0158633fa7d87b35957fbaaead9591754321 - pristine_git_object: 629236152c11f5dda79d0929e996bc25a90e5d92 + last_write_checksum: sha1:612b0c14f0138393b1177c2fed70bcc1307d89f3 + pristine_git_object: ac8b8bc42050f3c259734a167c28bf687373a201 src/ttd_data/errors/dataerror.py: id: 70b3a16b9fdb - last_write_checksum: sha1:6c4f77b5f05c174666a1ea4de0e82d5f90595e43 - pristine_git_object: fe4205a06ee5b68f1b99b94e7e75e89bad13b8d9 + last_write_checksum: sha1:8a1ebbde97b277c62c876d12a9f8dcaa18da8970 + pristine_git_object: 824c24415f26f103be16720c1f150bf9a6effacb src/ttd_data/errors/merchantdsrresponse_error.py: id: a6d87247a795 - last_write_checksum: sha1:9c6abd3375080e7517a1ff14260bbcfa7e29c017 - pristine_git_object: d0e2f525a2e174bdb3be2c7d1d103cdc68e2e315 + last_write_checksum: sha1:cf6973da90553b60876eda885279c34415aa9b55 + pristine_git_object: be5bec8cb7c586dc320aeeff8009ccd6681ae0af src/ttd_data/errors/no_response_error.py: id: ffd7339470a1 - last_write_checksum: sha1:7f326424a7d5ae1bcd5c89a0d6b3dbda9138942f - pristine_git_object: 1deab64bc43e1e65bf3c412d326a4032ce342366 + last_write_checksum: sha1:c99ea9192e70831af512adbe0f2d58cd84d50eb3 + pristine_git_object: 0525bdbfa38e34dae871421c00daeb20735dc071 src/ttd_data/errors/offlineconversiondataserverresponse_error.py: id: 38d3f6702808 - last_write_checksum: sha1:233a4009c5a9a6a0c36370db22f6858235432648 - pristine_git_object: d90c7f8d5065854745c4063bdc4ca44f2db7791e + last_write_checksum: sha1:720c48a960c44c08d24ab4a98a363eb69b846ddf + pristine_git_object: ec14ca677ed4366ba82e1ac8b239e4fd885988bd src/ttd_data/errors/responsevalidationerror.py: id: 59b6bad43c86 - last_write_checksum: sha1:cfc97a6b3c70529440909450f90f0cc877befc59 - pristine_git_object: 1e74559f5dd16a8a5599cedf75f886f3eb949857 + last_write_checksum: sha1:c8b229b1596e0b64b1aa86a3890a30a2447eb9c8 + pristine_git_object: 01363158e593daefce4f4cfc196f6c570f6aac21 src/ttd_data/errors/thirdpartydataserverresponse_error.py: id: 64da9f8c2a4b - last_write_checksum: sha1:84aa973252bd12a1f0a47c89ea787c96203570ae - pristine_git_object: 8566b78c4b508d12026af0a643c0c7c250a2b267 + last_write_checksum: sha1:f9d9dca0c439c85375e40a7fc49de1734ee07dba + pristine_git_object: dd9ab635e19dbabcab1d7fa67e23f648b4e036c8 src/ttd_data/errors/thirdpartydsrresponse_error.py: id: 3bcd84adf118 - last_write_checksum: sha1:f6bdb6d26c16afc0219dab92bdfbc58396f3cea7 - pristine_git_object: 665bfdf28706d8ec6ffa69e91a02805e8285fa12 + last_write_checksum: sha1:18d82e5d3c0afe06a0ce54ea48dc1132d0bb8874 + pristine_git_object: a5fa9470c63705b8c0fc332d732dfaaefca95c02 src/ttd_data/httpclient.py: id: 2be94ea9b30f - last_write_checksum: sha1:5e55338d6ee9f01ab648cad4380201a8a3da7dd7 - pristine_git_object: 89560b566073785535643e694c112bedbd3db13d + last_write_checksum: sha1:3c6164e22e24c774b5f15148c4b05f495d0c175a + pristine_git_object: b7b67f6c923ffef8515a873a28c92927380bef14 src/ttd_data/models/__init__.py: id: 9cb05e16fec0 - last_write_checksum: sha1:e7d00befdb3b975674a1466f0988bc58895a401d - pristine_git_object: 0d27a04b95f906b6ba07a9c38b7159bafc91c290 + last_write_checksum: sha1:f8e1f37166926e4ff703a40ddcb385b3459fc819 + pristine_git_object: 1214dd207e5963ba56862ba70eb1cb0e97e4fd04 src/ttd_data/models/advertiserdata.py: id: 3cb2f224c3a1 - last_write_checksum: sha1:f23c52fe321da112f960560940abed6bbbe72247 - pristine_git_object: 015bd79ce278e94d59163fe1d029f0f18c5dfcea - src/ttd_data/models/advertiserdataitem.py: - id: 5b824803bf02 - last_write_checksum: sha1:f7bbd7a64e8b978d434fed970e035555037b1d7e - pristine_git_object: c94cd969ed4c5563ec94ce32059b4e50bf9d1402 + last_write_checksum: sha1:80a20f1c55d95168af9dcc273d27027462ceec23 + pristine_git_object: 2b6c15e9a67030ffa2980c58bd66c709cd08da2c src/ttd_data/models/advertiserdatarequest.py: id: 11df7e9988cc - last_write_checksum: sha1:98ebb58a2a99c7cbaaa47d63d6a59f0d33d96730 - pristine_git_object: f3658eba0456205aeb69978f1324ccbbdb7887d8 + last_write_checksum: sha1:f7107264dfe83c95f38d0f72d6c718dcc9f76091 + pristine_git_object: 64af06fa7d2742d834ad72c939aa2ab7e2f8d605 src/ttd_data/models/advertiserdataresponseerrorcode.py: id: ffcc1a785853 - last_write_checksum: sha1:9c90f6573dbd76bb36e9cf8e0d37ab3586b2e81d - pristine_git_object: 41ebd67b3354e608bd6e85da991d5d67a1672c76 + last_write_checksum: sha1:4460c790879f740181c14da1f961759aae4c7b7d + pristine_git_object: 16121b6fbd4807550512de26f475482670a9a6c1 src/ttd_data/models/advertiserdataserverresponse.py: id: 532b8698bc5e - last_write_checksum: sha1:f4154c056b3c28b47793129860233dd74d916e4f - pristine_git_object: 46c26ea76fd4dce4ea94cac6a0305c82ae8218a4 + last_write_checksum: sha1:a370392c7148998dfcf2335c3a7b0b3eb8323831 + pristine_git_object: 9e177d5ceb8e8cf7fe5ab664d74205342c556912 src/ttd_data/models/advertiserdataserverresponseline.py: id: d86dcc352741 - last_write_checksum: sha1:a5218e68993192ba7fe1595972810c150c70e558 - pristine_git_object: 7506d80620e698a7bd5631de05a65f9d577cbaeb + last_write_checksum: sha1:e17d2d3572598ff82b355b0f5edd0a91114ef529 + pristine_git_object: 3916abc3b9eac154120bfd24076c4768967c3979 src/ttd_data/models/advertiserdsrfailedline.py: id: bac049718c62 - last_write_checksum: sha1:cdda05a577a77edf741e98b584176605bfb7aa73 - pristine_git_object: 4bf583055ec03d6c548838fd610b834a634c9840 + last_write_checksum: sha1:2be8e6dd13625aae617170d709954934fd72fbb1 + pristine_git_object: bb97732bf2e9ed068fb314273e94bf7bb65bb467 src/ttd_data/models/advertiserdsrrequest.py: id: 547e907d3b1a - last_write_checksum: sha1:da5be9dd23488468f5725986c815ceea307b64d0 - pristine_git_object: 20e357964d5a8ff73af0ad7a066efcdf10c0812a + last_write_checksum: sha1:8e99483bc9ee10e37e62d882ec12c987718e7ac3 + pristine_git_object: 328c59eafae396d034e1674a33019aa151aa4c4a src/ttd_data/models/advertiserdsrresponse.py: id: 6558e631036b - last_write_checksum: sha1:0707e4370537cbdcc27f093d779aed3c4601c067 - pristine_git_object: 0936113f8c13ae38f726ff9c0476d2b40dafdf8a + last_write_checksum: sha1:409148d18256daffb1f89c1d964d44fa19db8f14 + pristine_git_object: 14345dfe926d07f815a6d10d80a2a1265b8f08a5 + src/ttd_data/models/baseadvertiserdataitem.py: + id: df8b7792ffe1 + last_write_checksum: sha1:f062c78b1653630551d8becf749c5ea7ce4d2be0 + pristine_git_object: 70abb146958371f23d53dbcd98813994cbfff211 + src/ttd_data/models/baseofflineconversiondataitem.py: + id: 7922701d5b7f + last_write_checksum: sha1:852315b9cd570acf13a0b126c68a2f839b2fcc62 + pristine_git_object: 11c67b6ccde738b33b453add063aea927fad54ad + src/ttd_data/models/basepartnerdsrdataitem.py: + id: 4be97bf202da + last_write_checksum: sha1:c36ce4a3d9639c8f2d9abb2f3d70172521401e0e + pristine_git_object: 776f5c78b93ef2feda3fe90bf7754432ef9cfd37 + src/ttd_data/models/basethirdpartydataitem.py: + id: 38edda88d345 + last_write_checksum: sha1:4df2372c943fe2caae6dce56df227fdcb149d1c6 + pristine_git_object: bdd21855aec1e1d6242921235a1ed85cc5256e13 src/ttd_data/models/dataorigin.py: id: 01488b30ef74 - last_write_checksum: sha1:3335c975492f6d6bd9c62b916be2f567cdfcd545 - pristine_git_object: a0416b4168ed2798fb296f7468cff91e8fcfa15b + last_write_checksum: sha1:b1ac1ca17a73abdc3fa8f946f35d77e02a37e9a2 + pristine_git_object: ddb92c23883db9430942927660d0663e6ea07f34 src/ttd_data/models/dataorigintype.py: id: c61445332fd2 - last_write_checksum: sha1:dfc7580c28340e467404a0f2ce103483338d074a - pristine_git_object: ccdd8f0ba82a4aea0bf0011f56a64a6b909ee5c2 + last_write_checksum: sha1:6c24ffcd2f5be30423155963684e2048dd6aac0a + pristine_git_object: 18f0cf38c7734e5b35b249545e61862d5e4efcd0 src/ttd_data/models/datasubjectrequestadvertiserdataop.py: id: 32f041f202c4 - last_write_checksum: sha1:10c447b0a251810a0b5c8478ed952b7147e59cdf - pristine_git_object: 040070f8bd146d925ae81f0725545155443c8c91 + last_write_checksum: sha1:a368259fc707e264c14ff18934d4207429df95b0 + pristine_git_object: 3fc6335a7f70d785f52fbe526fd6d7e9ffbc0264 src/ttd_data/models/datasubjectrequestmerchantdataop.py: id: 2eaf568ecc7c - last_write_checksum: sha1:6e04088c800bb60dd7ee2ede48a5d03c3529839b - pristine_git_object: 95f438961e148d8f37c251da2197ca26614f3390 + last_write_checksum: sha1:917a0ceef3a3cdcc80a9f839f8ad5e925c48e80b + pristine_git_object: 37a642b8d3563c21d62b82a98ac0fd1d198cf0dd src/ttd_data/models/datasubjectrequestthirdpartydataop.py: id: 961430b7540f - last_write_checksum: sha1:1b75036525cec86c1b4035b97149f0db4fb3e7c8 - pristine_git_object: e81b19c23725b9aad154f32705eac2b525283db7 + last_write_checksum: sha1:f64d3d8f1e125bd1eb4f80f60e9a9c3ecbc4a21e + pristine_git_object: 39acf52aa7138ce3e6776ae023c0a80e6eede1c2 src/ttd_data/models/dsrerrorcode.py: id: 3d91ba4e034b - last_write_checksum: sha1:b1f9ef50441621cb3c80bb52de16ed854a6835fd - pristine_git_object: a511993e29d9baf00854611df7c34bd34a42fdda + last_write_checksum: sha1:8d7afc2de449244a6fc457ec383aa619fe95c6ba + pristine_git_object: db4420ec903dd0d14a5d321279e680009abdf200 src/ttd_data/models/httpmetadata.py: id: 2b8260801700 - last_write_checksum: sha1:75dd9dfb6895082b8d81332c7f94f2ec09bb08ac - pristine_git_object: 3bc847d3447ce9128e2e80e42b6d125234910905 + last_write_checksum: sha1:be77609bcc6233a2ab0d9aed3c4ecbba55a80db2 + pristine_git_object: a548716829b6b56533a3480684a9d2e05461347c src/ttd_data/models/ingestadvertiserdataop.py: id: 536d8288fda4 - last_write_checksum: sha1:99a9c51d5d525c9dd452b9307ddbd5627cffdc3d - pristine_git_object: 897bc9b7ecee3b4331d7ebf393a6aa4a7b20ddf7 + last_write_checksum: sha1:6aba4806d3ae15ce4fbc0c7e136023a700d66fd4 + pristine_git_object: d2aac1b4482a39c4c32877d6dec3c97897a0e95b src/ttd_data/models/ingestofflineconversiondataop.py: id: 3dff4f5ef0ef - last_write_checksum: sha1:12c7e44fb11e852ad2c81343adaf16899194b600 - pristine_git_object: 54a30f23ef278aac6de43c6cd5ea7627196bcb08 + last_write_checksum: sha1:35c1af7746890286d4a5ff2b6e011f9bca4c4ed8 + pristine_git_object: c580cabd0ebb3b809a1e9817cea2222970def203 src/ttd_data/models/ingestthirdpartydataop.py: id: 7b9550f1c695 - last_write_checksum: sha1:9e37ce20a267e7129aaf98264272dc90159c2ca2 - pristine_git_object: 21fe756375712c2262fe18ecbd341a25363d48cd + last_write_checksum: sha1:2326ad429cd450e21140b6d9eb36671794239b30 + pristine_git_object: 3004d004cd1d869974be34828c0236a78a8be09c src/ttd_data/models/merchantdsrfailedline.py: id: 5100bc2ef724 - last_write_checksum: sha1:c6a45f621aee3fadb3a888b7f6c8f00a88d51a50 - pristine_git_object: 1dd65f1232c270ec5ca202c377f0d27bb2f4dc23 + last_write_checksum: sha1:c60bacb6d41f011441024b58f9ff147c60bc0bc0 + pristine_git_object: 5d0a0c45291eba27f036a82cd68d1b2312950f61 src/ttd_data/models/merchantdsrrequest.py: id: 4f8f69a30b61 - last_write_checksum: sha1:ec1d9374f7f393b1b6064beb6fbbf38d5631af34 - pristine_git_object: 1cc820f748c9a0fa6af381b5b21398d71dbeb8c9 + last_write_checksum: sha1:cbd78b8b553f3bc2ce189cff37af1f9d8ccd8ca5 + pristine_git_object: a673e0a9b6d7d7497820fa5831dec14b01d2ca26 src/ttd_data/models/merchantdsrresponse.py: id: de0ff347dca5 - last_write_checksum: sha1:2dfb1b3d6817cc63846742d04d1bd4593a9c0186 - pristine_git_object: 476acfbbcfec526b4956150693a5a258f5b5c48c - src/ttd_data/models/offlineconversiondataitem.py: - id: fde98c9e187a - last_write_checksum: sha1:b56fc2c9794fb64bea60550645c58e30ef242e74 - pristine_git_object: 9176ed4df85539de44977aa683a21457728460f6 + last_write_checksum: sha1:c3c1f50b9ef214b420e88a811c92003f875ecefe + pristine_git_object: fe410acce238c8d5e993212450f24be10ae4d568 src/ttd_data/models/offlineconversiondatarequest.py: id: 7d81245ab1fc - last_write_checksum: sha1:e70a196e61c175965710c370a2df04f1dbb5ea09 - pristine_git_object: 091dc57d25b02d59e061b94331ae0c790165283b + last_write_checksum: sha1:fc30a93731a16642f187c680709422d466700eb7 + pristine_git_object: 1bc47d55fccde34f459d67e9310824944f6d43b0 src/ttd_data/models/offlineconversiondataresponseerrorcode.py: id: e10b97e7f07c - last_write_checksum: sha1:94411b252e4f588a3f276ec3d0eed05b153642e4 - pristine_git_object: 8e816bcc1f6cb9deb2912055367c875612ab4203 + last_write_checksum: sha1:6ff58f1c01034e69f0e4743486eabfb3c85a4529 + pristine_git_object: 39841b335632f8d966d010c1e70732a4854d2fdf src/ttd_data/models/offlineconversiondataserverresponse.py: id: b4031c03cad3 - last_write_checksum: sha1:b7065fee1a42a477b8f11c0a826266dfbe6eb70e - pristine_git_object: 1482f6ca3ba63d714a24b8451ff8cc3f60fa570e + last_write_checksum: sha1:9d0b3ae90d1cbcbdc95590b74f3056f699feb0df + pristine_git_object: 12d9e7e44ee78c3b6fca5da6072ce72bd7be8405 src/ttd_data/models/offlineconversiondataserverresponseline.py: id: 3f36747f69ac - last_write_checksum: sha1:6c9aae2ba9054f5f260d944b1274735f57c5fa5b - pristine_git_object: b01fd9acbb330dc8fe06bd5000fb6b92943fb0c0 - src/ttd_data/models/partnerdsrdataitem.py: - id: 0a90d692f602 - last_write_checksum: sha1:5b074c646a618793566f9e2babdd663987bb3f11 - pristine_git_object: 7206e4624bf2632d45c323be1b5a23247b933f3f + last_write_checksum: sha1:887d47bf594b168cf71775b33196099a7796880f + pristine_git_object: 06171a21e932f8988a60642e95a9229702740c9c src/ttd_data/models/partnerdsrrequesttype.py: id: cac9dd97133a - last_write_checksum: sha1:d02cf0f9f33e31f11cb2a06df70605451470a340 - pristine_git_object: 5825ec6964e2dacca8f318c3bdcc2cc63922a219 + last_write_checksum: sha1:c6c32036507206675b733abbe7c2b12073b0b937 + pristine_git_object: 1774c4f42026d145067be2b7ba6ef2792f6d6704 src/ttd_data/models/realtimeconversioneventlineitem.py: id: ec4913c7f806 - last_write_checksum: sha1:6cc64abc77d56f4a298fdba522211c8100af59e4 - pristine_git_object: 881a84fe1951201af4f7e36528a4fb9b04380e29 + last_write_checksum: sha1:b81ed0540069b91dfdbcbdadd8bb30407764fa75 + pristine_git_object: dd39b5586ec2b1510c777da610d6bf377f671b05 src/ttd_data/models/realtimeconversioneventsprivacysetting.py: id: 5c7bd069a3c4 - last_write_checksum: sha1:a2f1e81c8c0468d56366df2085ba0846e9283e8a - pristine_git_object: edcc65f92584cdc1edac4595c042a62a17da32c2 + last_write_checksum: sha1:cad40577128878ffab4f379344c18011803c3de3 + pristine_git_object: 2682bf72fe51b96d0d116c4bb52361fc0e420ade src/ttd_data/models/thirdpartydata.py: id: 747d671a1fd0 - last_write_checksum: sha1:2b1d3bda8a9adaa3e27e411e964d2007f90ddb72 - pristine_git_object: 1e82737f2a12254503773d71fe46041a50829902 - src/ttd_data/models/thirdpartydataitem.py: - id: de4678c2732f - last_write_checksum: sha1:f5509c383f9569568bf3e1b10c641febcdd27647 - pristine_git_object: f5e2341fb34dafb0bc5793608a9bfb34139e3843 + last_write_checksum: sha1:07fd2d4c3c2d7a90035839b7b0359b2787e3c5be + pristine_git_object: d2bc365582fb1117aeac3ba12cf9819d01ed12a0 src/ttd_data/models/thirdpartydatarequest.py: id: 51f4e7be4154 - last_write_checksum: sha1:f501183b1c567aafa2835cb69bec0bebf3d0186f - pristine_git_object: 51e0f7f694b9e431f4d06ad31b4f0b3a134cec7e + last_write_checksum: sha1:c895beb53946529f55245718ed9d1d729686babc + pristine_git_object: 08a31df358797a0f36e833027d7ad9b2ec9e729d src/ttd_data/models/thirdpartydataresponseerrorcode.py: id: c79166cc6728 - last_write_checksum: sha1:ae6fdc7fecbccaafe6ff0f74b554a754cce4d0f5 - pristine_git_object: f9ff1a99bfadbd62a33375ed09d2e310bc8e4893 + last_write_checksum: sha1:6bb080a3d41e74350a4cc887e3801f81c1014ff1 + pristine_git_object: 580e6b751ddceb0311094c12e4aec6f606365c9d src/ttd_data/models/thirdpartydataserverresponse.py: id: 031ace715edf - last_write_checksum: sha1:e45519743b4a97ce7c357cbb19a65b69ab2cce3a - pristine_git_object: f08b9fac6ae593c3e9887138a948f71d9bcd1b69 + last_write_checksum: sha1:8ed69feafb034cb907d392b38ba95dccc29f6012 + pristine_git_object: 4c23aeca77104651d719fedb3b6165663fca71d1 src/ttd_data/models/thirdpartydataserverresponseline.py: id: 4a5fa4dcfd73 - last_write_checksum: sha1:ec9099ede8d1b697b7deab4d2050f6e803027418 - pristine_git_object: 2663aea5a968b9ea9daf7c753364e0e5879a220d + last_write_checksum: sha1:cdba2f5937b515ff83b19202b0bbc9fad6a055d5 + pristine_git_object: 5edbbcb11a7256bf7e048f58a5bba262e183b8d7 src/ttd_data/models/thirdpartydsrfailedline.py: id: 349404a49bbf - last_write_checksum: sha1:25ce459daad14a6f1d2ecc48c9443df55c0fe58d - pristine_git_object: d3a615a172d966f3c64b1e6b5c608315960ae356 + last_write_checksum: sha1:c83964c03ce130e3373909440056a0cf229b06a3 + pristine_git_object: 17753ee1fa5336f0cecffbf26a949af12a853572 src/ttd_data/models/thirdpartydsrrequest.py: id: 586497a7c7ed - last_write_checksum: sha1:7524edb0dfff006091c4d5993d51d38bccfc4307 - pristine_git_object: 9f0187f77a50c7a9816884bfa65e7caac934261e + last_write_checksum: sha1:2dcf752fae00f966f16ef6cacb8bd98df042e8cc + pristine_git_object: 9d10d61b2fd5fe579c3dec51a974e04914ae6d9f src/ttd_data/models/thirdpartydsrresponse.py: id: 7d22e340fa71 - last_write_checksum: sha1:3b764c6fb2346a9aa8f470d36d8fce2ac1b8f146 - pristine_git_object: 7522fc385833fc17b9763ce8a9f5cca420be1ab1 + last_write_checksum: sha1:663d618867c3216a366ae9554a6e16a2e53ac45c + pristine_git_object: b8960ee34603991114cae808f15a75001ac2c6a8 src/ttd_data/offlineconversion.py: id: 9cbf832f0f9b - last_write_checksum: sha1:2f3e97f1d558c43d893e2a9c9f789aa7d50dee24 - pristine_git_object: 5d482190fa249b8eed133aecc6c020023c0c8c15 + last_write_checksum: sha1:7bb18e893d1a2fd4aca3d57d6be481fb408dbe5a + pristine_git_object: 6afe42275a5a4f9a2edd552cb3a07b7e604a1296 src/ttd_data/py.typed: id: 6369ae030577 last_write_checksum: sha1:8efc425ffe830805ffcc0f3055871bdcdc542c60 pristine_git_object: 3e38f1a929f7d6b1d6de74604aa87e3d8f010544 src/ttd_data/sdk.py: id: 1472df75b98c - last_write_checksum: sha1:7cd0bbdc4f900cafd92c586eba4993a9e4ab4cb7 - pristine_git_object: ef876ef0c3dedce71077660f368c5ae17d405b26 + last_write_checksum: sha1:7b1ab36e0a103ae361fe8dd99c0a4850e58ebb92 + pristine_git_object: 29034de9fe16bcb29206ad395e4ef5898340bca6 src/ttd_data/sdkconfiguration.py: id: 6c07cde690cd - last_write_checksum: sha1:26e46bae867f574d959614353000d3a9ab0df8e0 - pristine_git_object: 55179c09edd09c46689a1b3d4533c1d3c963a9e8 + last_write_checksum: sha1:b83130d20886d237799ede00df304904e19c582d + pristine_git_object: 914672ad7183223ef1b4fcaf06b06356fb5374c7 src/ttd_data/thirdparty.py: id: ad2d973ed3be - last_write_checksum: sha1:fd9036690ed3a5523f9a4ef8ea8e05d6a466abc1 - pristine_git_object: 2e5cb6e9fed588fcaaea9252e3336ccd6d58fd93 + last_write_checksum: sha1:6e6e8352bf00b6a1bc9c58909b312057b5e5392a + pristine_git_object: e99faab4918d9c60fc387d4c76eedcdba3e2c5fa src/ttd_data/types/__init__.py: id: 219a89572292 - last_write_checksum: sha1:140ebdd01a46f92ffc710c52c958c4eba3cf68ed - pristine_git_object: fc76fe0c5505e29859b5d2bb707d48fd27661b8c + last_write_checksum: sha1:10f6438ecec71ffdbb755ad40fbb3b84ed90d29d + pristine_git_object: bb7adfe2f377c977a69d42f233db698b24c2b381 src/ttd_data/types/basemodel.py: id: 00b5490305bc - last_write_checksum: sha1:10d84aedeb9d35edfdadf2c3020caa1d24d8b584 - pristine_git_object: a9a640a1a7048736383f96c67c6290c86bf536ee + last_write_checksum: sha1:b3399632dd5bc83ae6673822f38b2c1b1fc76dfa + pristine_git_object: 196a66019c9fb86bd7c1de17b51fb6e7178b0918 src/ttd_data/utils/__init__.py: id: 6b87bf7e78f2 - last_write_checksum: sha1:1970816f2234ecb8785798240b0edced961de971 - pristine_git_object: 0498cb8dabf249b39609f81fb10cddc30f1b78b5 + last_write_checksum: sha1:73f4b658e558bb23342bc8d300e0961991be6215 + pristine_git_object: ac6959da4afe7e2e5e5fc32d24bba3d2fb62b75e src/ttd_data/utils/annotations.py: id: d1b7b6530770 - last_write_checksum: sha1:a4824ad65f730303e4e1e3ec1febf87b4eb46dbc - pristine_git_object: 12e0aa4f1151bb52474cc02e88397329b90703f6 + last_write_checksum: sha1:78c8976a64cca1d50bdfff78fa27ebf9d440909f + pristine_git_object: 1f99145c5a558c9f65424dea302e0627161b5fb5 src/ttd_data/utils/datetimes.py: id: 2d794a4852dd - last_write_checksum: sha1:c721e4123000e7dc61ec52b28a739439d9e17341 - pristine_git_object: a6c52cd61bbe2d459046c940ce5e8c469f2f0664 + last_write_checksum: sha1:cccc7681cf106529b5c9fd029e2e5666d33b6b1a + pristine_git_object: 1c414046c9e13409d98ea7af2c4504f73ae26ef9 src/ttd_data/utils/dynamic_imports.py: id: b07f05d40ca2 - last_write_checksum: sha1:a1940c63feb8eddfd8026de53384baf5056d5dcc - pristine_git_object: 673edf82a97d0fea7295625d3e092ea369a36b79 + last_write_checksum: sha1:29ea6dd782488148bb058e19c5bb460e1be386cb + pristine_git_object: f48aa0c66c624f314ea9fe58bddaf7334c8a9990 src/ttd_data/utils/enums.py: id: 04558d0f1a2b - last_write_checksum: sha1:bc8c3c1285ae09ba8a094ee5c3d9c7f41fa1284d - pristine_git_object: 3324e1bc2668c54c4d5f5a1a845675319757a828 + last_write_checksum: sha1:7e684cb319fc5d44245cacb9e501f78862095ea2 + pristine_git_object: 2471fa555a0e355a3fe15eece4e9166a701309b7 src/ttd_data/utils/eventstreaming.py: id: 88cef70df5ca - last_write_checksum: sha1:620d78a8b4e3b854e08d136e02e40a01a786bd70 - pristine_git_object: 3bdcd6d3d4fc772cb7f5fca8685dcdc8c85e13e8 + last_write_checksum: sha1:065c09e76a08a3c04a8d470c3044aca74c1f54c9 + pristine_git_object: 613d2fffbde6adc39b17935b8aa93a034dea9014 src/ttd_data/utils/forms.py: id: 3b8d93e597bb - last_write_checksum: sha1:0ca31459b99f761fcc6d0557a0a38daac4ad50f4 - pristine_git_object: 1e550bd5c2c35d977ddc10f49d77c23cb12c158d + last_write_checksum: sha1:ab6d3f8cf0bfbe0ccebd7d927a25329602fb16ec + pristine_git_object: 7928ef2118082336aad561f80c1fc5ad7815b9ce src/ttd_data/utils/headers.py: id: 4681366f5995 - last_write_checksum: sha1:7c6df233ee006332b566a8afa9ce9a245941d935 - pristine_git_object: 37864cbbbc40d1a47112bbfdd3ba79568fc8818a + last_write_checksum: sha1:bb7097db485e750d0094e2d94ce6ff328a52fb3c + pristine_git_object: e2d3e0009712bad6b3f3c86d0852f6cec38c9190 src/ttd_data/utils/logger.py: id: 4590b38a2392 - last_write_checksum: sha1:bc563e73eb73d03eb65dc1fd9def84717e30a8d1 - pristine_git_object: 9664bc076cfd67c82f6b5169787458c5e462d57d + last_write_checksum: sha1:ef0a40c2f2e7ca520a76e253551cc72d7cf8b9c7 + pristine_git_object: 6b7061821983e9a5d4277f91de1d7e9c17ece250 src/ttd_data/utils/metadata.py: id: 4fc5598faf89 - last_write_checksum: sha1:e703e5cbb5255144aacf86898d1420529afaaff8 - pristine_git_object: 5abddd588837ac297050ca3b543627faadb350a9 + last_write_checksum: sha1:c564e5ac25dfaeeae817c47e952d4b78899dc91f + pristine_git_object: 07e0cc03a9405c8997608b547bc3aed39193c88e src/ttd_data/utils/queryparams.py: id: 8325e172a666 - last_write_checksum: sha1:b94c3f314fd3da0d1d215afc2731f48748e2aa59 - pristine_git_object: c04e0db82b68eca041f2cb2614d748fbac80fd41 + last_write_checksum: sha1:2a93abbf3e5194dd668e9ec96ef2b773e49dfbe6 + pristine_git_object: f18930d011cf31d6fd0bbbaae377c70dc39274e9 src/ttd_data/utils/requestbodies.py: id: 0924f0dfaef1 - last_write_checksum: sha1:41e2d2d2d3ecc394c8122ca4d4b85e1c3e03f054 - pristine_git_object: 1de32b6d26f46590232f398fdba6ce0072f1659c + last_write_checksum: sha1:afa5daada8db1e90d8c96a5ad7488c4cac3887d3 + pristine_git_object: 80c8a957c3bd48cde726d292560ca0ad220f58d1 src/ttd_data/utils/retries.py: id: 04accebbe68a - last_write_checksum: sha1:471372f5c5d1dd5583239c9cf3c75f1b636e5d87 - pristine_git_object: af07d4e941007af4213c5ec9047ef8a2fca04e5e + last_write_checksum: sha1:19e8a7c769ed8aa3d050f91667eba9b811e85a4b + pristine_git_object: 0934a56cbd3eb4dde5b54ac683e5c16730244c5d src/ttd_data/utils/security.py: id: e38af000ccc5 - last_write_checksum: sha1:c11eef495b6aaa249178c24c796940cc540b7a00 - pristine_git_object: 42d8d78e9981eed7507670014d99588e27ab325a + last_write_checksum: sha1:853c8f9f500bcffaf6880e27feafaab0e9146da8 + pristine_git_object: e56eff29be2cf5406dc94fbb1c9d8bd96cd3b257 src/ttd_data/utils/serializers.py: id: 625de2eedcad - last_write_checksum: sha1:61009f2e4ef6613a1a5af813fe020373dae5a492 - pristine_git_object: d2149f8b909cb96628db140ac3cddb1b1e981367 + last_write_checksum: sha1:c29ac2153c26e5566b5b988712b38704b72ef2d1 + pristine_git_object: b3c3d4b55ad65fc3f813ec2521a9e0e63adeccdb src/ttd_data/utils/unmarshal_json_response.py: id: 2f612fb73d96 - last_write_checksum: sha1:599d9379b87a48c14dc7d672a2d0261dee5b7204 - pristine_git_object: 8b476a9566b3ccbdc33c286aa7fdeac8074c46d5 + last_write_checksum: sha1:4a6f87d7632a4364ee64b338e2a7404bc0074885 + pristine_git_object: afab1d80a468ef747026d8202c27e558ea983cb8 src/ttd_data/utils/url.py: id: 31e17c41ed73 - last_write_checksum: sha1:6479961baa90432ca25626f8e40a7bbc32e73b41 - pristine_git_object: c78ccbae426ce6d385709d97ce0b1c2813ea2418 + last_write_checksum: sha1:206e50f71027c3e0bb4589de778b4534d8cd04df + pristine_git_object: 178fae653a8b5c5e166f124ad4ce590f87085cb9 src/ttd_data/utils/values.py: id: 45979a7770a1 - last_write_checksum: sha1:acaa178a7c41ddd000f58cc691e4632d925b2553 - pristine_git_object: dae01a44384ac3bc13ae07453a053bf6c898ebe3 + last_write_checksum: sha1:3a4ec34b2221dd825f7432e2d913b21d8ff92506 + pristine_git_object: 53c869685607f3756168d82e0cfaa581d333efbc examples: IngestAdvertiserData: speakeasy-default-ingest-advertiser-data: diff --git a/.speakeasy/gen.yaml b/.speakeasy/gen.yaml index 40bc73a..65c41ac 100644 --- a/.speakeasy/gen.yaml +++ b/.speakeasy/gen.yaml @@ -3,7 +3,7 @@ generation: devContainers: enabled: true schemaPath: .speakeasy/out.openapi.yaml - sdkClassName: DataClient + sdkClassName: BaseDataClient maintainOpenAPIOrder: true usageSnippets: optionalPropertyRendering: withExample @@ -17,7 +17,7 @@ generation: securityFeb2025: true sharedErrorComponentsApr2025: true sharedNestedComponentsJan2026: true - nameOverrideFeb2026: false + nameOverrideFeb2026: true auth: oAuth2ClientCredentialsEnabled: true oAuth2PasswordEnabled: true @@ -28,16 +28,18 @@ generation: allOfMergeStrategy: shallowMerge requestBodyFieldName: body versioningStrategy: automatic - persistentEdits: {} + persistentEdits: + enabled: "true" tests: generateTests: true generateNewTests: false skipResponseBodyAssertions: false python: - version: 0.1.7 + version: 0.2.10 additionalDependencies: dev: {} - main: {} + main: + uid2-client: '>=2.9.0,<3.0.0' allowedRedefinedBuiltins: - id - object diff --git a/.speakeasy/out.openapi.yaml b/.speakeasy/out.openapi.yaml index 1830efc..4950c4f 100644 --- a/.speakeasy/out.openapi.yaml +++ b/.speakeasy/out.openapi.yaml @@ -433,6 +433,7 @@ components: type: "string" nullable: true additionalProperties: false + x-speakeasy-name-override: BaseAdvertiserDataItem AdvertiserDataRequest: required: - "AdvertiserId" @@ -718,6 +719,7 @@ components: type: "string" nullable: true additionalProperties: false + x-speakeasy-name-override: BaseOfflineConversionDataItem OfflineConversionDataRequest: required: - "DataProviderId" @@ -836,6 +838,7 @@ components: type: "string" nullable: true additionalProperties: false + x-speakeasy-name-override: BasePartnerDsrDataItem PartnerDsrRequestType: enum: - "OptOut" @@ -954,6 +957,7 @@ components: type: "string" nullable: true additionalProperties: false + x-speakeasy-name-override: BaseThirdPartyDataItem ThirdPartyDataRequest: required: - "DataProviderId" diff --git a/.speakeasy/overlay.yaml b/.speakeasy/overlay.yaml index 719cc4a..c74fa69 100644 --- a/.speakeasy/overlay.yaml +++ b/.speakeasy/overlay.yaml @@ -61,4 +61,20 @@ actions: type: array items: $ref: "#/components/schemas/DataOrigin" - nullable: true \ No newline at end of file + nullable: true + + # Rename the generated schema classes so the public names are free for the + # hand-written wrappers in `ttd_data.uid2_ext` that add raw email / phone / + # hashed_email / hashed_phone fields. + - target: "$.components.schemas.AdvertiserDataItem" + update: + x-speakeasy-name-override: BaseAdvertiserDataItem + - target: "$.components.schemas.ThirdPartyDataItem" + update: + x-speakeasy-name-override: BaseThirdPartyDataItem + - target: "$.components.schemas.PartnerDsrDataItem" + update: + x-speakeasy-name-override: BasePartnerDsrDataItem + - target: "$.components.schemas.OfflineConversionDataItem" + update: + x-speakeasy-name-override: BaseOfflineConversionDataItem \ No newline at end of file diff --git a/.speakeasy/workflow.lock b/.speakeasy/workflow.lock index c3990be..89c42a8 100644 --- a/.speakeasy/workflow.lock +++ b/.speakeasy/workflow.lock @@ -1,9 +1,16 @@ -speakeasyVersion: 1.761.5 +speakeasyVersion: 1.763.1 sources: Data API: sourceNamespace: data-api - sourceRevisionDigest: sha256:ddc22a3c5d0bbf4810aa9555cd193bfdf6bee2c5d0843e01e7d0006fc505730f - sourceBlobDigest: sha256:c86d70812b7b8fb35ad731b6755a72a8c5d8a1bd62af3f1e64883c81264cd1fe + sourceRevisionDigest: sha256:0daf241a54a03955ac31c7434f554ddab15bd58de0eba952e0b9ae6a34f2eac7 + sourceBlobDigest: sha256:c7806fbc0c6ce5c9db56204d8ad142e32949242388ae9b9f6cae3df05590ed30 + tags: + - latest + - v0.1 + Data API Local: + sourceNamespace: data-api-local + sourceRevisionDigest: sha256:4d928ad86686f71fb18f025908926675e9a4db92003d6db4b9261715295fe5bf + sourceBlobDigest: sha256:f937fae3db68ebe534207c942a10f8d6c2ba00b4da1097f0455f2dd3dabb3140 tags: - latest - v0.1 @@ -11,10 +18,17 @@ targets: data-api: source: Data API sourceNamespace: data-api - sourceRevisionDigest: sha256:ddc22a3c5d0bbf4810aa9555cd193bfdf6bee2c5d0843e01e7d0006fc505730f - sourceBlobDigest: sha256:c86d70812b7b8fb35ad731b6755a72a8c5d8a1bd62af3f1e64883c81264cd1fe + sourceRevisionDigest: sha256:0daf241a54a03955ac31c7434f554ddab15bd58de0eba952e0b9ae6a34f2eac7 + sourceBlobDigest: sha256:c7806fbc0c6ce5c9db56204d8ad142e32949242388ae9b9f6cae3df05590ed30 codeSamplesNamespace: data-api-python-code-samples - codeSamplesRevisionDigest: sha256:17559c1f3685fb8ada33a85032c16f1204816196020b860301f8852965c12233 + codeSamplesRevisionDigest: sha256:399ba1e4b8d171fa5c1b27e26548dba753b770623eff87e51181cd84eba837a8 + data-api-local: + source: Data API Local + sourceNamespace: data-api-local + sourceRevisionDigest: sha256:4d928ad86686f71fb18f025908926675e9a4db92003d6db4b9261715295fe5bf + sourceBlobDigest: sha256:f937fae3db68ebe534207c942a10f8d6c2ba00b4da1097f0455f2dd3dabb3140 + codeSamplesNamespace: data-api-local-python-code-samples + codeSamplesRevisionDigest: sha256:0698d00298b9de56934c0d07e1b3893b38a4b2a2a2525efd5f5df34fe264f765 workflow: workflowVersion: 1.0.0 speakeasyVersion: latest @@ -31,9 +45,6 @@ workflow: data-api: target: python source: Data API - publish: - pypi: - token: $pypi_token codeSamples: registry: location: registry.speakeasyapi.dev/thetradedesk/data-api/data-api-python-code-samples diff --git a/.speakeasy/workflow.yaml b/.speakeasy/workflow.yaml index cf16f69..8e03da7 100644 --- a/.speakeasy/workflow.yaml +++ b/.speakeasy/workflow.yaml @@ -13,9 +13,6 @@ targets: data-api: target: python source: Data API - publish: - pypi: - token: $pypi_token codeSamples: registry: location: registry.speakeasyapi.dev/thetradedesk/data-api/data-api-python-code-samples diff --git a/README-PYPI.md b/README-PYPI.md index bf7217c..7d51fdc 100644 --- a/README-PYPI.md +++ b/README-PYPI.md @@ -166,18 +166,29 @@ with DataClient() as client: ### 3. Offline Conversions Data (CAPI) ```python -from ttd_data import DataClient, models +from datetime import datetime, timezone +from ttd_data import DataClient, UserIdType, models with DataClient() as client: response = client.offline_conversion.ingest_offline_conversion_data( ttd_auth=TTD_AUTH_TOKEN, data_provider_id=DATA_PROVIDER_ID, items=[ + # Pre-resolved TDID models.OfflineConversionDataItem( tracking_tag_id=TRACKING_TAG_ID, timestamp_utc=datetime.now(timezone.utc), tdid="", - ) + ), + # Multiple identifiers via UserIdArray + models.OfflineConversionDataItem( + tracking_tag_id=TRACKING_TAG_ID, + timestamp_utc=datetime.now(timezone.utc), + user_id_array=[ + [UserIdType.TDID, ""], + [UserIdType.UID2, ""], + ], + ), ], ) ``` @@ -234,7 +245,57 @@ with DataClient() as client: ``` -### 7. Async usage +### 7. UID2 Identity Mapping + +Supply a `UID2Config` to resolve raw PII (email / phone) to UID2 before ingest. Per-item mapping failures (opted-out or unmapped identifiers) appear in `failed_lines` with `ErrorCode = "Uid2Error"`. A complete UID2 service failure raises `UID2ServiceError`. + +```python +from ttd_data import DataClient, IdentityScope, UID2Config, UID2ServiceError +from ttd_data.models import AdvertiserData, AdvertiserDataItem + +uid2_config = UID2Config( + base_url="", + api_key="", + client_secret="", + identity_scope=IdentityScope.UID2, +) + +try: + with DataClient(uid2_config=uid2_config, server_url="") as client: + response = client.advertiser.ingest_advertiser_data( + ttd_auth=TTD_AUTH_TOKEN, + advertiser_id=ADVERTISER_ID, + items=[ + # Raw email — resolved to UID2 before ingest + AdvertiserDataItem( + data=[AdvertiserData(name="loyalty_members")], + email="user@example.com", + ), + # Pre-hashed email + AdvertiserDataItem( + data=[AdvertiserData(name="loyalty_members")], + hashed_email="", + ), + # Pre-resolved TDID — no UID2 work needed + AdvertiserDataItem( + data=[AdvertiserData(name="loyalty_members")], + tdid="", + ), + ], + ) + + # Check for per-item mapping failures + server_response = response.advertiser_data_server_response + if server_response and server_response.failed_lines: + for line in server_response.failed_lines: + print(f"Item {line.item_number} failed: {line.error_code} — {line.message}") + +except UID2ServiceError as e: + # The UID2 identity-map service itself failed — no items were sent + print(f"UID2 service error: {e}") +``` + +### 8. Async usage The same SDK client can also be used to make asynchronous requests by importing asyncio. diff --git a/README.md b/README.md index 02ee786..022f9bb 100644 --- a/README.md +++ b/README.md @@ -166,18 +166,29 @@ with DataClient() as client: ### 3. Offline Conversions Data (CAPI) ```python -from ttd_data import DataClient, models +from datetime import datetime, timezone +from ttd_data import DataClient, UserIdType, models with DataClient() as client: response = client.offline_conversion.ingest_offline_conversion_data( ttd_auth=TTD_AUTH_TOKEN, data_provider_id=DATA_PROVIDER_ID, items=[ + # Pre-resolved TDID models.OfflineConversionDataItem( tracking_tag_id=TRACKING_TAG_ID, timestamp_utc=datetime.now(timezone.utc), tdid="", - ) + ), + # Multiple identifiers via UserIdArray + models.OfflineConversionDataItem( + tracking_tag_id=TRACKING_TAG_ID, + timestamp_utc=datetime.now(timezone.utc), + user_id_array=[ + [UserIdType.TDID, ""], + [UserIdType.UID2, ""], + ], + ), ], ) ``` @@ -234,7 +245,57 @@ with DataClient() as client: ``` -### 7. Async usage +### 7. UID2 Identity Mapping + +Supply a `UID2Config` to resolve raw PII (email / phone) to UID2 before ingest. Per-item mapping failures (opted-out or unmapped identifiers) appear in `failed_lines` with `ErrorCode = "Uid2Error"`. A complete UID2 service failure raises `UID2ServiceError`. + +```python +from ttd_data import DataClient, IdentityScope, UID2Config, UID2ServiceError +from ttd_data.models import AdvertiserData, AdvertiserDataItem + +uid2_config = UID2Config( + base_url="", + api_key="", + client_secret="", + identity_scope=IdentityScope.UID2, +) + +try: + with DataClient(uid2_config=uid2_config, server_url="") as client: + response = client.advertiser.ingest_advertiser_data( + ttd_auth=TTD_AUTH_TOKEN, + advertiser_id=ADVERTISER_ID, + items=[ + # Raw email — resolved to UID2 before ingest + AdvertiserDataItem( + data=[AdvertiserData(name="loyalty_members")], + email="user@example.com", + ), + # Pre-hashed email + AdvertiserDataItem( + data=[AdvertiserData(name="loyalty_members")], + hashed_email="", + ), + # Pre-resolved TDID — no UID2 work needed + AdvertiserDataItem( + data=[AdvertiserData(name="loyalty_members")], + tdid="", + ), + ], + ) + + # Check for per-item mapping failures + server_response = response.advertiser_data_server_response + if server_response and server_response.failed_lines: + for line in server_response.failed_lines: + print(f"Item {line.item_number} failed: {line.error_code} — {line.message}") + +except UID2ServiceError as e: + # The UID2 identity-map service itself failed — no items were sent + print(f"UID2 service error: {e}") +``` + +### 8. Async usage The same SDK client can also be used to make asynchronous requests by importing asyncio. diff --git a/USAGE.md b/USAGE.md index b008f38..1eb758e 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1,17 +1,28 @@ ```python -# Synchronous Example -from ttd_data import DataClient - - -with DataClient() as data_client: - - res = data_client.advertiser.ingest_advertiser_data(ttd_auth="", advertiser_id="") - - assert res.advertiser_data_server_response is not None - - # Handle response - print(res.advertiser_data_server_response) +# Synchronous Example — with UID2 identity mapping +from ttd_data import DataClient, IdentityScope, UID2Config +from ttd_data.models import AdvertiserData, AdvertiserDataItem + +uid2_config = UID2Config( + base_url="", + api_key="", + client_secret="", + identity_scope=IdentityScope.UID2, +) + +with DataClient(uid2_config=uid2_config, server_url="") as client: + response = client.advertiser.ingest_advertiser_data( + ttd_auth="", + advertiser_id="", + items=[ + AdvertiserDataItem( + data=[AdvertiserData(name="loyalty_members")], + email="user@example.com", + ) + ], + ) + print(response.advertiser_data_server_response) ```
@@ -19,21 +30,32 @@ with DataClient() as data_client: The same SDK client can also be used to make asynchronous requests by importing asyncio. ```python -# Asynchronous Example +# Asynchronous Example — with UID2 identity mapping import asyncio -from ttd_data import DataClient +from ttd_data import DataClient, IdentityScope, UID2Config +from ttd_data.models import AdvertiserData, AdvertiserDataItem -async def main(): - - async with DataClient() as data_client: - - res = await data_client.advertiser.ingest_advertiser_data_async(ttd_auth="", advertiser_id="") +uid2_config = UID2Config( + base_url="", + api_key="", + client_secret="", + identity_scope=IdentityScope.UID2, +) - assert res.advertiser_data_server_response is not None - - # Handle response - print(res.advertiser_data_server_response) +async def main(): + async with DataClient(uid2_config=uid2_config, server_url="") as client: + response = await client.advertiser.ingest_advertiser_data_async( + ttd_auth="", + advertiser_id="", + items=[ + AdvertiserDataItem( + data=[AdvertiserData(name="loyalty_members")], + email="user@example.com", + ) + ], + ) + print(response.advertiser_data_server_response) asyncio.run(main()) ``` - \ No newline at end of file + diff --git a/data-api-local/.speakeasy/gen.yaml b/data-api-local/.speakeasy/gen.yaml index 3af43d5..8687470 100644 --- a/data-api-local/.speakeasy/gen.yaml +++ b/data-api-local/.speakeasy/gen.yaml @@ -2,8 +2,8 @@ configVersion: 2.0.0 generation: devContainers: enabled: true - schemaPath: .speakeasy/swagger.json - sdkClassName: DataClient + schemaPath: .speakeasy/out.openapi.yaml + sdkClassName: BaseDataClient maintainOpenAPIOrder: true usageSnippets: optionalPropertyRendering: withExample @@ -17,7 +17,7 @@ generation: securityFeb2025: true sharedErrorComponentsApr2025: true sharedNestedComponentsJan2026: true - nameOverrideFeb2026: false + nameOverrideFeb2026: true auth: oAuth2ClientCredentialsEnabled: true oAuth2PasswordEnabled: true @@ -28,16 +28,18 @@ generation: allOfMergeStrategy: shallowMerge requestBodyFieldName: body versioningStrategy: automatic - persistentEdits: {} + persistentEdits: + enabled: "true" tests: generateTests: true generateNewTests: false skipResponseBodyAssertions: false python: - version: 0.0.1 + version: 0.1.0 additionalDependencies: dev: {} - main: {} + main: + uid2-client: '>=2.9.0,<3.0.0' allowedRedefinedBuiltins: - id - object @@ -49,19 +51,19 @@ python: clientServerStatusCodesAsErrors: true constFieldCasing: normal defaultErrorName: APIError - description: Python Client SDK for TTD Data API (Local Testing). + description: Python Client SDK for TTD Data API. enableCustomCodeRegions: false enumFormat: enum - envVarPrefix: TTD_DATA_LOCAL + envVarPrefix: TTD_DATA fixFlags: asyncPaginationSep2025: true - conflictResistantModelImportsFeb2026: true + conflictResistantModelImportsFeb2026: false responseRequiredSep2024: true flattenGlobalSecurity: true flattenRequests: true flatteningOrder: parameters-first forwardCompatibleEnumsByDefault: false - forwardCompatibleUnionsByDefault: tagged-only + forwardCompatibleUnionsByDefault: "false" imports: option: openapi paths: @@ -73,9 +75,9 @@ python: inferUnionDiscriminators: true inputModelSuffix: input license: - name: The MIT License (MIT) - shortName: MIT - url: https://mit-license.org/ + name: Apache License 2.0 + shortName: Apache-2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 maxMethodParams: 999 methodArguments: infer-optional-args moduleName: "" diff --git a/data-api-local/.speakeasy/overlay.yaml b/data-api-local/.speakeasy/overlay.yaml new file mode 100644 index 0000000..c74fa69 --- /dev/null +++ b/data-api-local/.speakeasy/overlay.yaml @@ -0,0 +1,80 @@ +overlay: 1.0.0 +info: + title: Add DataOrigins field + version: 1.0.0 +actions: + - target: "$.info" + update: + description: | + Python SDK for The Trade Desk Data API. Provides operations for ingesting advertiser data, + third-party data, and offline conversions, as well as handling data subject deletion and opt-out requests. + + For more information, see the official API documentation: + - [Advertiser targeting data (1PD)](https://open.thetradedesk.com/advertiser/docsApp/GuidesAdvertiser/data/doc/post-data-advertiser-firstparty) + - [Third-party targeting data (3PD)](https://open.thetradedesk.com/provider/docsApp/GuidesProvider/audience/doc/post-data-thirdparty) + - [Offline conversions (CAPI)](https://open.thetradedesk.com/advertiser/docsApp/GuidesAdvertiser/data/doc/post-providerapi-offlineconversion) + + Deletions and opt-outs: + - [Advertiser](https://open.thetradedesk.com/advertiser/docsApp/GuidesAdvertiser/data/doc/post-data-deletion-optout-advertiser) + - [Third party](https://open.thetradedesk.com/provider/docsApp/GuidesProvider/audience/doc/post-data-deletion-optout-thirdparty) + - [Merchant](https://open.thetradedesk.com/provider/docsApp/GuidesProvider/retail/doc/post-data-deletion-optout-merchant) + - target: "$.components.schemas" + update: + DataOriginType: + type: string + enum: + - "Advertiser" + - "DataProvider" + - "Integration" + - "Onboarder" + DataOrigin: + required: + - "Id" + - "Type" + type: object + properties: + Id: + type: string + Type: + $ref: "#/components/schemas/DataOriginType" + additionalProperties: false + + - target: "$.components.schemas.AdvertiserDataRequest.properties" + update: + DataOrigins: + type: array + items: + $ref: "#/components/schemas/DataOrigin" + nullable: true + + - target: "$.components.schemas.ThirdPartyDataRequest.properties" + update: + DataOrigins: + type: array + items: + $ref: "#/components/schemas/DataOrigin" + nullable: true + + - target: "$.components.schemas.OfflineConversionDataRequest.properties" + update: + DataOrigins: + type: array + items: + $ref: "#/components/schemas/DataOrigin" + nullable: true + + # Rename the generated schema classes so the public names are free for the + # hand-written wrappers in `ttd_data.uid2_ext` that add raw email / phone / + # hashed_email / hashed_phone fields. + - target: "$.components.schemas.AdvertiserDataItem" + update: + x-speakeasy-name-override: BaseAdvertiserDataItem + - target: "$.components.schemas.ThirdPartyDataItem" + update: + x-speakeasy-name-override: BaseThirdPartyDataItem + - target: "$.components.schemas.PartnerDsrDataItem" + update: + x-speakeasy-name-override: BasePartnerDsrDataItem + - target: "$.components.schemas.OfflineConversionDataItem" + update: + x-speakeasy-name-override: BaseOfflineConversionDataItem \ No newline at end of file diff --git a/data-api-local/.speakeasy/workflow.yaml b/data-api-local/.speakeasy/workflow.yaml index 33aac4e..bbeb02b 100644 --- a/data-api-local/.speakeasy/workflow.yaml +++ b/data-api-local/.speakeasy/workflow.yaml @@ -5,7 +5,7 @@ sources: inputs: - location: https://usw-data.adsrvr.org/swagger/v1/swagger.json overlays: - - location: ../.speakeasy/overlay.yaml + - location: .speakeasy/overlay.yaml registry: location: registry.speakeasyapi.dev/thetradedesk/data-api/data-api-local targets: diff --git a/data-api-local/local_uid2_ingest_e2e.py b/data-api-local/local_uid2_ingest_e2e.py new file mode 100644 index 0000000..c67388c --- /dev/null +++ b/data-api-local/local_uid2_ingest_e2e.py @@ -0,0 +1,360 @@ +"""Live end-to-end test for `ttd_data.DataClient` against TTD's +UID2-aware endpoints. + +Each call sends a UID2AdvertiserDataItem-style payload with `email=...`; +the client resolves it via UID2 `POST /identity/map` and forwards a +TDID/UID2-only request to TTD. + +Endpoints exercised: + 1. ingest_advertiser_data + 2. ingest_third_party_data + 3. ingest_offline_conversion_data + 4. data_subject_request_advertiser_data + 5. data_subject_request_merchant_data + 6. data_subject_request_third_party_data + +Fill in CONFIG below, then run: + python data-api-local/local_uid2_ingest_e2e.py + +Notes: + - UID2 integ env URL: https://operator-integ.uidapi.com (prod: https://prod.uidapi.com). + - The TTD Data API call hits the real ingestion endpoint. Use a test + advertiser/data-provider/merchant ID you have permission to write to. +""" + +from __future__ import annotations + +import os +from datetime import datetime, timezone +from typing import Any, Callable + +import httpx + +from ttd_data import errors +from ttd_data._hooks.types import BeforeRequestContext, BeforeRequestHook +from ttd_data.models import AdvertiserData, ThirdPartyData, PartnerDsrRequestType +from ttd_data import DataClient, IdentityScope, UID2Config, UserIdType +from ttd_data.uid2 import ( + AdvertiserDataItem, + OfflineConversionDataItem, + PartnerDsrDataItem, + ThirdPartyDataItem, +) + + +# --------------------------------------------------------------------------- +# CONFIG — fill these in +# --------------------------------------------------------------------------- + +# Secrets and per-account identifiers come from the environment — source +# `.env.local` (see `.env.example`) before running this script. +UID2_BASE_URL = os.getenv("UID2_BASE_URL", "") +UID2_API_KEY = os.getenv("UID2_API_KEY", "") +UID2_CLIENT_SECRET = os.getenv("UID2_CLIENT_SECRET", "") + +TTD_DATA_SERVER_URL = os.getenv("TTD_DATA_SERVER_URL", "") +TTD_AUTH_TOKEN = os.getenv("TTD_AUTH_TOKEN", "") +ADVERTISER_ID = os.getenv("TEST_ADVERTISER_ID", "") +DATA_PROVIDER_ID = os.getenv("TEST_DATA_PROVIDER_ID", "") +MERCHANT_ID = int(os.getenv("TEST_MERCHANT_ID", "0")) +TRACKING_TAG_ID = os.getenv("TEST_TRACKING_TAG_ID", "") + +# Segment names (only used by the ingest endpoints; DSR doesn't carry data) +ADVERTISER_SEGMENT = "uid2_resolver_smoke_test" +THIRD_PARTY_SEGMENT = "uid2_resolver_smoke_test" + +# Test identifiers +RAW_EMAIL = "test129863@example.com" +HASHED_EMAIL = "tMmiiTI7IaAcPpQPFQ65uMVCWH8av9jw4cwf/F5HVRQ=" # sha256(b"test@example.com").b64 +OPTOUT_EMAIL = "optout123@unifiedid.com" +PASSTHROUGH_TDID = "00000000-0000-0000-0000-000000000001" + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _section(title: str) -> None: + bar = "=" * 70 + print(f"\n{bar}\n {title}\n{bar}") + + +def _print_items(label: str, items: list) -> None: + print(f"\n{label}:") + for i, item in enumerate(items): + print(f" [{i}] {item.model_dump(by_alias=True, exclude_none=True)}") + + +def _print_response(label: str, response: Any, server_attr: str) -> None: + print(f"\n{label}:") + server_response = getattr(response, server_attr, None) + failed_lines = getattr(server_response, "failed_lines", None) if server_response else None + if failed_lines: + print(f" failed_lines ({len(failed_lines)}):") + for line in failed_lines: + print( + f" item_number={getattr(line, 'item_number', None)} " + f"error_code={getattr(line, 'error_code', None)} " + f"message={getattr(line, 'message', None)!r}" + ) + else: + print(" no failed lines") + + print("\n identity_resolutions:") + for raw_id, resolution in response.identity_resolutions.items(): + print(f" {raw_id}") + print(f" current_raw_uid : {resolution.current_raw_uid}") + print(f" previous_raw_uid: {resolution.previous_raw_uid}") + print(f" refresh_from : {resolution.refresh_from}") + print(f" unmapped_reason : {resolution.unmapped_reason}") + + +_REDACT_HEADERS = {"ttd-auth", "authorization", "ttdsignature"} + + +class _RawRequestPrinter(BeforeRequestHook): + """Logs method, URL, headers (sensitive values redacted) and body of every + outgoing HTTP request right before httpx sends it.""" + + def before_request( + self, hook_ctx: BeforeRequestContext, request: httpx.Request + ) -> httpx.Request: + print(f"\n --- raw request ({hook_ctx.operation_id}) ---") + print(f" {request.method} {request.url}") + print(" headers:") + for k, v in request.headers.items(): + redacted = "" if k.lower() in _REDACT_HEADERS else v + print(f" {k}: {redacted}") + body = request.content + try: + body_str = body.decode("utf-8") if isinstance(body, bytes) else str(body) + except UnicodeDecodeError: + body_str = repr(body) + print(f" body ({len(body)} bytes): {body_str}") + print(" --- end raw request ---") + return request + + +def _run(name: str, action: Callable[[], None]) -> None: + """Run one endpoint's exercise inside a unified error-printing wrapper.""" + _section(name) + try: + action() + except errors.DataError as exc: # base class for all speakeasy-mapped errors + print(f"\nServer error: {getattr(exc, 'message', exc)}") + if hasattr(exc, "status_code"): + print(f"status: {exc.status_code}") + data = getattr(exc, "data", None) + if data is not None and getattr(data, "failed_lines", None): + print(f"failed_lines: {data.failed_lines}") + + +# --------------------------------------------------------------------------- +# Endpoint exercises +# --------------------------------------------------------------------------- + + +def exercise_advertiser_ingest(client: DataClient) -> None: + # Expected outcomes per item under collect-all mode: + # [0] email resolves → UID2 set, line accepted. + # [1] hashed_email resolves → UID2 set, line accepted. + # [2] optout email → UID2='*' substituted; the Trade Desk Data APIs reject + # (no fallback) → failed_lines entry with ErrorCode=Uid2Error (merged + # from failed_mappings). + # [3] TDID passthrough → no UID2 work, line accepted. + # [4] optout email + valid TDID: UID2='*' substituted, but the Trade Desk + # Data APIs accept on TDID via first-valid-wins. No failed_lines entry. + items = [ + AdvertiserDataItem(data=[AdvertiserData(name=ADVERTISER_SEGMENT)], email=RAW_EMAIL), + AdvertiserDataItem(data=[AdvertiserData(name=ADVERTISER_SEGMENT)], hashed_email=HASHED_EMAIL), + AdvertiserDataItem(data=[AdvertiserData(name=ADVERTISER_SEGMENT)], email=OPTOUT_EMAIL), + AdvertiserDataItem(data=[AdvertiserData(name=ADVERTISER_SEGMENT)], tdid=PASSTHROUGH_TDID), + AdvertiserDataItem( + data=[AdvertiserData(name=ADVERTISER_SEGMENT)], + email=OPTOUT_EMAIL, + tdid=PASSTHROUGH_TDID, + ), + ] + _print_items("Submitting items", items) + response = client.advertiser.ingest_advertiser_data( + advertiser_id=ADVERTISER_ID, + ttd_auth=TTD_AUTH_TOKEN, + items=items, + ) + _print_items("Items after resolution", items) + _print_response("TTD response", response, "advertiser_data_server_response") + + +def exercise_third_party_ingest(client: DataClient) -> None: + # Same expected outcomes as exercise_advertiser_ingest — third-party handler + # uses the same first-valid-wins priority chain, so item [4] (optout + + # fallback TDID) is accepted on TDID. + items = [ + ThirdPartyDataItem(data=[ThirdPartyData(name=THIRD_PARTY_SEGMENT)], email=RAW_EMAIL), + ThirdPartyDataItem(data=[ThirdPartyData(name=THIRD_PARTY_SEGMENT)], hashed_email=HASHED_EMAIL), + ThirdPartyDataItem(data=[ThirdPartyData(name=THIRD_PARTY_SEGMENT)], email=OPTOUT_EMAIL), + ThirdPartyDataItem(data=[ThirdPartyData(name=THIRD_PARTY_SEGMENT)], tdid=PASSTHROUGH_TDID), + ThirdPartyDataItem( + data=[ThirdPartyData(name=THIRD_PARTY_SEGMENT)], + email=OPTOUT_EMAIL, + tdid=PASSTHROUGH_TDID, + ), + ] + _print_items("Submitting items", items) + response = client.third_party.ingest_third_party_data( + data_provider_id=DATA_PROVIDER_ID, + ttd_auth=TTD_AUTH_TOKEN, + items=items, + ) + _print_items("Items after resolution", items) + _print_response("TTD response", response, "third_party_data_server_response") + + +def exercise_offline_conversion_ingest(client: DataClient) -> None: + # Expected outcomes per item under collect-all mode: + # [0]–[3] same shape as advertiser: resolve / resolve / optout-no-fallback / + # TDID passthrough. + # [4] UserIdArray with two resolvable entries — both rewritten in place to + # ["2", uid2]; line accepted. + # [5] optout email + TDID fallback: UID2='*', the Trade Desk Data APIs' + # offline conversion priority chain falls through to TDID. No + # failed_lines entry. + # [6] UserIdArray containing an optout email at -3. The Trade Desk Data APIs' + # TryParseUserIdArray rejects the whole line on the first invalid + # entry — failed_lines entry expected with ErrorCode=Uid2Error. + now = datetime.now(timezone.utc) + items = [ + OfflineConversionDataItem(tracking_tag_id=TRACKING_TAG_ID, timestamp_utc=now, email=RAW_EMAIL), + OfflineConversionDataItem(tracking_tag_id=TRACKING_TAG_ID, timestamp_utc=now, hashed_email=HASHED_EMAIL), + OfflineConversionDataItem(tracking_tag_id=TRACKING_TAG_ID, timestamp_utc=now, email=OPTOUT_EMAIL), + OfflineConversionDataItem(tracking_tag_id=TRACKING_TAG_ID, timestamp_utc=now, tdid=PASSTHROUGH_TDID), + OfflineConversionDataItem( + tracking_tag_id=TRACKING_TAG_ID, + timestamp_utc=now, + user_id_array=[[UserIdType.HASHED_EMAIL, HASHED_EMAIL], [UserIdType.EMAIL, RAW_EMAIL]], + ), + OfflineConversionDataItem( + tracking_tag_id=TRACKING_TAG_ID, + timestamp_utc=now, + email=OPTOUT_EMAIL, + tdid=PASSTHROUGH_TDID, + ), + OfflineConversionDataItem( + tracking_tag_id=TRACKING_TAG_ID, + timestamp_utc=now, + user_id_array=[[UserIdType.EMAIL, OPTOUT_EMAIL]], + ), + ] + _print_items("Submitting items", items) + response = client.offline_conversion.ingest_offline_conversion_data( + ttd_auth=TTD_AUTH_TOKEN, + data_provider_id=DATA_PROVIDER_ID, + items=items, + user_id_array_metadata_format=["type", "id"], + ) + _print_items("Items after resolution", items) + _print_response("TTD response", response, "offline_conversion_data_server_response") + + +def exercise_dsr_advertiser(client: DataClient) -> None: + # Expected outcomes per item: + # [0] email resolves → UID2 set, line accepted. + # [1] TDID passthrough → line accepted. + # [2] optout email (no fallback) → UID2='*', the Trade Desk Data APIs + # reject, merged to ErrorCode=Uid2Error. + # [3] optout email + TDID fallback: line accepted on TDID. + items = [ + PartnerDsrDataItem(email=RAW_EMAIL), + PartnerDsrDataItem(tdid=PASSTHROUGH_TDID), + PartnerDsrDataItem(email=OPTOUT_EMAIL), + PartnerDsrDataItem(email=OPTOUT_EMAIL, tdid=PASSTHROUGH_TDID), + ] + _print_items("Submitting items", items) + response = client.deletion_opt_out.data_subject_request_advertiser_data( + advertiser_id=ADVERTISER_ID, + ttd_auth=TTD_AUTH_TOKEN, + items=items, + request_type=PartnerDsrRequestType.OPT_OUT, + ) + _print_items("Items after resolution", items) + _print_response("TTD response", response, "advertiser_dsr_response") + + +def exercise_dsr_merchant(client: DataClient) -> None: + # Expected outcomes per item: + # [0] email resolves → UID2 set, line accepted. + # [1] TDID passthrough → line accepted. + # [2] optout email (no fallback) → UID2='*', the Trade Desk Data APIs + # reject, merged to ErrorCode=Uid2Error. + # [3] optout email + TDID fallback: line accepted on TDID. + items = [ + PartnerDsrDataItem(email=RAW_EMAIL), + PartnerDsrDataItem(tdid=PASSTHROUGH_TDID), + PartnerDsrDataItem(email=OPTOUT_EMAIL), + PartnerDsrDataItem(email=OPTOUT_EMAIL, tdid=PASSTHROUGH_TDID), + ] + _print_items("Submitting items", items) + response = client.deletion_opt_out.data_subject_request_merchant_data( + merchant_id=MERCHANT_ID, + ttd_auth=TTD_AUTH_TOKEN, + items=items, + request_type=PartnerDsrRequestType.OPT_OUT, + ) + _print_items("Items after resolution", items) + _print_response("TTD response", response, "merchant_dsr_response") + + +def exercise_dsr_third_party(client: DataClient) -> None: + # Expected outcomes per item: + # [0] email resolves → UID2 set, line accepted. + # [1] TDID passthrough → line accepted. + # [2] optout email (no fallback) → UID2='*', the Trade Desk Data APIs + # reject, merged to ErrorCode=Uid2Error. + # [3] optout email + TDID fallback: line accepted on TDID. + items = [ + PartnerDsrDataItem(email=RAW_EMAIL), + PartnerDsrDataItem(tdid=PASSTHROUGH_TDID), + PartnerDsrDataItem(email=OPTOUT_EMAIL), + PartnerDsrDataItem(email=OPTOUT_EMAIL, tdid=PASSTHROUGH_TDID), + ] + _print_items("Submitting items", items) + response = client.deletion_opt_out.data_subject_request_third_party_data( + data_provider_id=DATA_PROVIDER_ID, + ttd_auth=TTD_AUTH_TOKEN, + items=items, + request_type=PartnerDsrRequestType.OPT_OUT, + ) + _print_items("Items after resolution", items) + _print_response("TTD response", response, "third_party_dsr_response") + + +# --------------------------------------------------------------------------- +# Driver +# --------------------------------------------------------------------------- + + +def main() -> None: + config = UID2Config( + base_url=UID2_BASE_URL, + api_key=UID2_API_KEY, + client_secret=UID2_CLIENT_SECRET, + identity_scope=IdentityScope.UID2, + ) + client = DataClient(config, server_url=TTD_DATA_SERVER_URL) + + # `_hooks` is a private attribute on the speakeasy SDK config; `register_before_request_hook` + # appends to its hook list so all subsequent requests run through it. + hooks = client.data_client.sdk_configuration.__dict__["_hooks"] + hooks.register_before_request_hook(_RawRequestPrinter()) + + _run("Advertiser ingest", lambda: exercise_advertiser_ingest(client)) + _run("Third-party ingest", lambda: exercise_third_party_ingest(client)) + _run("Offline conversion ingest", lambda: exercise_offline_conversion_ingest(client)) + _run("DSR — advertiser", lambda: exercise_dsr_advertiser(client)) + _run("DSR — merchant", lambda: exercise_dsr_merchant(client)) + _run("DSR — third-party", lambda: exercise_dsr_third_party(client)) + + +if __name__ == "__main__": + main() diff --git a/docs/models/advertiserdatarequest.md b/docs/models/advertiserdatarequest.md index 4de3a37..7fabf56 100644 --- a/docs/models/advertiserdatarequest.md +++ b/docs/models/advertiserdatarequest.md @@ -3,10 +3,10 @@ ## Fields -| Field | Type | Required | Description | -| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | -| `advertiser_id` | *str* | :heavy_check_mark: | N/A | -| `data_provider_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | -| `items` | List[[models.AdvertiserDataItem](../models/advertiserdataitem.md)] | :heavy_minus_sign: | N/A | -| `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | -| `data_origins` | List[[models.DataOrigin](../models/dataorigin.md)] | :heavy_minus_sign: | N/A | \ No newline at end of file +| Field | Type | Required | Description | +| -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| `advertiser_id` | *str* | :heavy_check_mark: | N/A | +| `data_provider_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | +| `items` | List[[models.BaseAdvertiserDataItem](../models/baseadvertiserdataitem.md)] | :heavy_minus_sign: | N/A | +| `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | +| `data_origins` | List[[models.DataOrigin](../models/dataorigin.md)] | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/models/advertiserdsrrequest.md b/docs/models/advertiserdsrrequest.md index 8d7c549..cc6a629 100644 --- a/docs/models/advertiserdsrrequest.md +++ b/docs/models/advertiserdsrrequest.md @@ -7,6 +7,6 @@ | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | `advertiser_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | | `data_provider_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | -| `items` | List[[models.PartnerDsrDataItem](../models/partnerdsrdataitem.md)] | :heavy_minus_sign: | N/A | +| `items` | List[[models.BasePartnerDsrDataItem](../models/basepartnerdsrdataitem.md)] | :heavy_minus_sign: | N/A | | `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | | `request_type` | [Optional[models.PartnerDsrRequestType]](../models/partnerdsrrequesttype.md) | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/models/advertiserdataitem.md b/docs/models/baseadvertiserdataitem.md similarity index 99% rename from docs/models/advertiserdataitem.md rename to docs/models/baseadvertiserdataitem.md index f6ed686..3cfde39 100644 --- a/docs/models/advertiserdataitem.md +++ b/docs/models/baseadvertiserdataitem.md @@ -1,4 +1,4 @@ -# AdvertiserDataItem +# BaseAdvertiserDataItem ## Fields diff --git a/docs/models/offlineconversiondataitem.md b/docs/models/baseofflineconversiondataitem.md similarity index 99% rename from docs/models/offlineconversiondataitem.md rename to docs/models/baseofflineconversiondataitem.md index dce7952..69e0b5a 100644 --- a/docs/models/offlineconversiondataitem.md +++ b/docs/models/baseofflineconversiondataitem.md @@ -1,4 +1,4 @@ -# OfflineConversionDataItem +# BaseOfflineConversionDataItem ## Fields diff --git a/docs/models/partnerdsrdataitem.md b/docs/models/basepartnerdsrdataitem.md similarity index 98% rename from docs/models/partnerdsrdataitem.md rename to docs/models/basepartnerdsrdataitem.md index 8018d0a..2d39ff0 100644 --- a/docs/models/partnerdsrdataitem.md +++ b/docs/models/basepartnerdsrdataitem.md @@ -1,4 +1,4 @@ -# PartnerDsrDataItem +# BasePartnerDsrDataItem ## Fields diff --git a/docs/models/thirdpartydataitem.md b/docs/models/basethirdpartydataitem.md similarity index 99% rename from docs/models/thirdpartydataitem.md rename to docs/models/basethirdpartydataitem.md index 516c15c..84a6fe2 100644 --- a/docs/models/thirdpartydataitem.md +++ b/docs/models/basethirdpartydataitem.md @@ -1,4 +1,4 @@ -# ThirdPartyDataItem +# BaseThirdPartyDataItem ## Fields diff --git a/docs/models/merchantdsrrequest.md b/docs/models/merchantdsrrequest.md index 36386b6..b869910 100644 --- a/docs/models/merchantdsrrequest.md +++ b/docs/models/merchantdsrrequest.md @@ -6,6 +6,6 @@ | Field | Type | Required | Description | | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | `merchant_id` | *OptionalNullable[int]* | :heavy_minus_sign: | N/A | -| `items` | List[[models.PartnerDsrDataItem](../models/partnerdsrdataitem.md)] | :heavy_minus_sign: | N/A | +| `items` | List[[models.BasePartnerDsrDataItem](../models/basepartnerdsrdataitem.md)] | :heavy_minus_sign: | N/A | | `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | | `request_type` | [Optional[models.PartnerDsrRequestType]](../models/partnerdsrrequesttype.md) | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/models/offlineconversiondatarequest.md b/docs/models/offlineconversiondatarequest.md index 0a20f8f..cf44e24 100644 --- a/docs/models/offlineconversiondatarequest.md +++ b/docs/models/offlineconversiondatarequest.md @@ -3,10 +3,10 @@ ## Fields -| Field | Type | Required | Description | -| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -| `data_provider_id` | *str* | :heavy_check_mark: | N/A | -| `user_id_array_metadata_format` | List[*str*] | :heavy_minus_sign: | N/A | -| `items` | List[[models.OfflineConversionDataItem](../models/offlineconversiondataitem.md)] | :heavy_minus_sign: | N/A | -| `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | -| `data_origins` | List[[models.DataOrigin](../models/dataorigin.md)] | :heavy_minus_sign: | N/A | \ No newline at end of file +| Field | Type | Required | Description | +| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| `data_provider_id` | *str* | :heavy_check_mark: | N/A | +| `user_id_array_metadata_format` | List[*str*] | :heavy_minus_sign: | N/A | +| `items` | List[[models.BaseOfflineConversionDataItem](../models/baseofflineconversiondataitem.md)] | :heavy_minus_sign: | N/A | +| `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | +| `data_origins` | List[[models.DataOrigin](../models/dataorigin.md)] | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/models/offlineconversiondataserverresponseline.md b/docs/models/offlineconversiondataserverresponseline.md index dcec856..8f9cb79 100644 --- a/docs/models/offlineconversiondataserverresponseline.md +++ b/docs/models/offlineconversiondataserverresponseline.md @@ -13,4 +13,4 @@ | `error_code` | [Optional[models.OfflineConversionDataResponseErrorCode]](../models/offlineconversiondataresponseerrorcode.md) | :heavy_minus_sign: | N/A | | `message` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | | `item_number` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | -| `original_request` | [Optional[models.OfflineConversionDataItem]](../models/offlineconversiondataitem.md) | :heavy_minus_sign: | N/A | \ No newline at end of file +| `original_request` | [Optional[models.BaseOfflineConversionDataItem]](../models/baseofflineconversiondataitem.md) | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/models/thirdpartydatarequest.md b/docs/models/thirdpartydatarequest.md index 0a23821..d3b0f39 100644 --- a/docs/models/thirdpartydatarequest.md +++ b/docs/models/thirdpartydatarequest.md @@ -3,10 +3,10 @@ ## Fields -| Field | Type | Required | Description | -| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | -| `data_provider_id` | *str* | :heavy_check_mark: | N/A | -| `items` | List[[models.ThirdPartyDataItem](../models/thirdpartydataitem.md)] | :heavy_minus_sign: | N/A | -| `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | -| `is_user_id_already_hashed` | *Optional[bool]* | :heavy_minus_sign: | N/A | -| `data_origins` | List[[models.DataOrigin](../models/dataorigin.md)] | :heavy_minus_sign: | N/A | \ No newline at end of file +| Field | Type | Required | Description | +| -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| `data_provider_id` | *str* | :heavy_check_mark: | N/A | +| `items` | List[[models.BaseThirdPartyDataItem](../models/basethirdpartydataitem.md)] | :heavy_minus_sign: | N/A | +| `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | +| `is_user_id_already_hashed` | *Optional[bool]* | :heavy_minus_sign: | N/A | +| `data_origins` | List[[models.DataOrigin](../models/dataorigin.md)] | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/models/thirdpartydsrrequest.md b/docs/models/thirdpartydsrrequest.md index 2589e5f..4c80526 100644 --- a/docs/models/thirdpartydsrrequest.md +++ b/docs/models/thirdpartydsrrequest.md @@ -7,6 +7,6 @@ | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | `data_provider_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | | `brand_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | -| `items` | List[[models.PartnerDsrDataItem](../models/partnerdsrdataitem.md)] | :heavy_minus_sign: | N/A | +| `items` | List[[models.BasePartnerDsrDataItem](../models/basepartnerdsrdataitem.md)] | :heavy_minus_sign: | N/A | | `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | | `request_type` | [Optional[models.PartnerDsrRequestType]](../models/partnerdsrrequesttype.md) | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/sdks/advertiser/README.md b/docs/sdks/advertiser/README.md index 2c91855..d5cc8e4 100644 --- a/docs/sdks/advertiser/README.md +++ b/docs/sdks/advertiser/README.md @@ -14,12 +14,12 @@ Upload first-party data for the specified ID for use in audience targeting. ```python -from ttd_data import DataClient +from ttd_data import BaseDataClient -with DataClient() as data_client: +with BaseDataClient() as base_data_client: - res = data_client.advertiser.ingest_advertiser_data(ttd_auth="", advertiser_id="") + res = base_data_client.advertiser.ingest_advertiser_data(ttd_auth="", advertiser_id="") assert res.advertiser_data_server_response is not None @@ -30,16 +30,16 @@ with DataClient() as data_client: ### Parameters -| Parameter | Type | Required | Description | -| --------------------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------- | -| `ttd_auth` | *str* | :heavy_check_mark: | Data API token for authentication. | -| `advertiser_id` | *str* | :heavy_check_mark: | N/A | -| `data_provider_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | -| `items` | List[[models.AdvertiserDataItem](../../models/advertiserdataitem.md)] | :heavy_minus_sign: | N/A | -| `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | -| `data_origins` | List[[models.DataOrigin](../../models/dataorigin.md)] | :heavy_minus_sign: | N/A | -| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | -| `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | +| Parameter | Type | Required | Description | +| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| `ttd_auth` | *str* | :heavy_check_mark: | Data API token for authentication. | +| `advertiser_id` | *str* | :heavy_check_mark: | N/A | +| `data_provider_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | +| `items` | List[[models.BaseAdvertiserDataItem](../../models/baseadvertiserdataitem.md)] | :heavy_minus_sign: | N/A | +| `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | +| `data_origins` | List[[models.DataOrigin](../../models/dataorigin.md)] | :heavy_minus_sign: | N/A | +| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | +| `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | ### Response diff --git a/docs/sdks/deletionoptout/README.md b/docs/sdks/deletionoptout/README.md index aa104aa..6d6d74e 100644 --- a/docs/sdks/deletionoptout/README.md +++ b/docs/sdks/deletionoptout/README.md @@ -16,12 +16,12 @@ Delete IDs shared with The Trade Desk for the specified advertiser ID. ```python -from ttd_data import DataClient +from ttd_data import BaseDataClient -with DataClient() as data_client: +with BaseDataClient() as base_data_client: - res = data_client.deletion_opt_out.data_subject_request_advertiser_data(ttd_auth="") + res = base_data_client.deletion_opt_out.data_subject_request_advertiser_data(ttd_auth="") assert res.advertiser_dsr_response is not None @@ -37,7 +37,7 @@ with DataClient() as data_client: | `ttd_auth` | *str* | :heavy_check_mark: | Data API token for authentication. | | `advertiser_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | | `data_provider_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | -| `items` | List[[models.PartnerDsrDataItem](../../models/partnerdsrdataitem.md)] | :heavy_minus_sign: | N/A | +| `items` | List[[models.BasePartnerDsrDataItem](../../models/basepartnerdsrdataitem.md)] | :heavy_minus_sign: | N/A | | `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | | `request_type` | [Optional[models.PartnerDsrRequestType]](../../models/partnerdsrrequesttype.md) | :heavy_minus_sign: | N/A | | `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | @@ -62,12 +62,12 @@ Delete IDs shared with The Trade Desk via a product catalog for the specified me ```python -from ttd_data import DataClient +from ttd_data import BaseDataClient -with DataClient() as data_client: +with BaseDataClient() as base_data_client: - res = data_client.deletion_opt_out.data_subject_request_merchant_data(ttd_auth="") + res = base_data_client.deletion_opt_out.data_subject_request_merchant_data(ttd_auth="") assert res.merchant_dsr_response is not None @@ -82,7 +82,7 @@ with DataClient() as data_client: | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | `ttd_auth` | *str* | :heavy_check_mark: | Data API token for authentication. | | `merchant_id` | *OptionalNullable[int]* | :heavy_minus_sign: | N/A | -| `items` | List[[models.PartnerDsrDataItem](../../models/partnerdsrdataitem.md)] | :heavy_minus_sign: | N/A | +| `items` | List[[models.BasePartnerDsrDataItem](../../models/basepartnerdsrdataitem.md)] | :heavy_minus_sign: | N/A | | `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | | `request_type` | [Optional[models.PartnerDsrRequestType]](../../models/partnerdsrrequesttype.md) | :heavy_minus_sign: | N/A | | `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | @@ -107,12 +107,12 @@ Delete IDs shared with The Trade Desk for the specified data provider ID. ```python -from ttd_data import DataClient +from ttd_data import BaseDataClient -with DataClient() as data_client: +with BaseDataClient() as base_data_client: - res = data_client.deletion_opt_out.data_subject_request_third_party_data(ttd_auth="") + res = base_data_client.deletion_opt_out.data_subject_request_third_party_data(ttd_auth="") assert res.third_party_dsr_response is not None @@ -128,7 +128,7 @@ with DataClient() as data_client: | `ttd_auth` | *str* | :heavy_check_mark: | Data API token for authentication. | | `data_provider_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | | `brand_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | -| `items` | List[[models.PartnerDsrDataItem](../../models/partnerdsrdataitem.md)] | :heavy_minus_sign: | N/A | +| `items` | List[[models.BasePartnerDsrDataItem](../../models/basepartnerdsrdataitem.md)] | :heavy_minus_sign: | N/A | | `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | | `request_type` | [Optional[models.PartnerDsrRequestType]](../../models/partnerdsrrequesttype.md) | :heavy_minus_sign: | N/A | | `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | diff --git a/docs/sdks/offlineconversion/README.md b/docs/sdks/offlineconversion/README.md index d248492..1ba0e98 100644 --- a/docs/sdks/offlineconversion/README.md +++ b/docs/sdks/offlineconversion/README.md @@ -14,12 +14,12 @@ Upload offline conversion data for the specified data provider. ```python -from ttd_data import DataClient +from ttd_data import BaseDataClient -with DataClient() as data_client: +with BaseDataClient() as base_data_client: - res = data_client.offline_conversion.ingest_offline_conversion_data(ttd_auth="", data_provider_id="") + res = base_data_client.offline_conversion.ingest_offline_conversion_data(ttd_auth="", data_provider_id="") assert res.offline_conversion_data_server_response is not None @@ -30,16 +30,16 @@ with DataClient() as data_client: ### Parameters -| Parameter | Type | Required | Description | -| ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -| `ttd_auth` | *str* | :heavy_check_mark: | Data API token for authentication. | -| `data_provider_id` | *str* | :heavy_check_mark: | N/A | -| `user_id_array_metadata_format` | List[*str*] | :heavy_minus_sign: | N/A | -| `items` | List[[models.OfflineConversionDataItem](../../models/offlineconversiondataitem.md)] | :heavy_minus_sign: | N/A | -| `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | -| `data_origins` | List[[models.DataOrigin](../../models/dataorigin.md)] | :heavy_minus_sign: | N/A | -| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | -| `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | +| Parameter | Type | Required | Description | +| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| `ttd_auth` | *str* | :heavy_check_mark: | Data API token for authentication. | +| `data_provider_id` | *str* | :heavy_check_mark: | N/A | +| `user_id_array_metadata_format` | List[*str*] | :heavy_minus_sign: | N/A | +| `items` | List[[models.BaseOfflineConversionDataItem](../../models/baseofflineconversiondataitem.md)] | :heavy_minus_sign: | N/A | +| `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | +| `data_origins` | List[[models.DataOrigin](../../models/dataorigin.md)] | :heavy_minus_sign: | N/A | +| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | +| `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | ### Response diff --git a/docs/sdks/thirdparty/README.md b/docs/sdks/thirdparty/README.md index 7bfccdd..040b31d 100644 --- a/docs/sdks/thirdparty/README.md +++ b/docs/sdks/thirdparty/README.md @@ -14,12 +14,12 @@ Upload third-party data for the specified data provider for use in audience targ ```python -from ttd_data import DataClient +from ttd_data import BaseDataClient -with DataClient() as data_client: +with BaseDataClient() as base_data_client: - res = data_client.third_party.ingest_third_party_data(ttd_auth="", data_provider_id="", is_user_id_already_hashed=False) + res = base_data_client.third_party.ingest_third_party_data(ttd_auth="", data_provider_id="", is_user_id_already_hashed=False) assert res.third_party_data_server_response is not None @@ -30,16 +30,16 @@ with DataClient() as data_client: ### Parameters -| Parameter | Type | Required | Description | -| --------------------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------- | -| `ttd_auth` | *str* | :heavy_check_mark: | Data API token for authentication. | -| `data_provider_id` | *str* | :heavy_check_mark: | N/A | -| `items` | List[[models.ThirdPartyDataItem](../../models/thirdpartydataitem.md)] | :heavy_minus_sign: | N/A | -| `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | -| `is_user_id_already_hashed` | *Optional[bool]* | :heavy_minus_sign: | N/A | -| `data_origins` | List[[models.DataOrigin](../../models/dataorigin.md)] | :heavy_minus_sign: | N/A | -| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | -| `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | +| Parameter | Type | Required | Description | +| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| `ttd_auth` | *str* | :heavy_check_mark: | Data API token for authentication. | +| `data_provider_id` | *str* | :heavy_check_mark: | N/A | +| `items` | List[[models.BaseThirdPartyDataItem](../../models/basethirdpartydataitem.md)] | :heavy_minus_sign: | N/A | +| `data_load_trace_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A | +| `is_user_id_already_hashed` | *Optional[bool]* | :heavy_minus_sign: | N/A | +| `data_origins` | List[[models.DataOrigin](../../models/dataorigin.md)] | :heavy_minus_sign: | N/A | +| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | +| `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | ### Response diff --git a/pyproject.toml b/pyproject.toml index 646963b..a8a9da7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ttd-data" -version = "0.1.7" +version = "0.2.8" description = "Python Client SDK for TTD Data API." authors = [{ name = "Speakeasy" },] readme = "README-PYPI.md" @@ -9,6 +9,7 @@ dependencies = [ "httpcore >=1.0.9", "httpx >=0.28.1", "pydantic >=2.11.2", + "uid2-client >=2.9.0,<3.0.0", ] urls.repository = "https://github.com/thetradedesk/ttd-data-python.git" license = { text = "map[name:Apache License 2.0 shortName:Apache-2.0 url:https://www.apache.org/licenses/LICENSE-2.0]" } @@ -18,6 +19,8 @@ dev = [ "mypy ==1.15.0", "pylint ==3.2.3", "pyright ==1.1.398", + "pytest >=8.0.0", + "pytest-asyncio >=0.23.0", ] [tool.setuptools.packages.find] diff --git a/scripts/prepare_readme.py b/scripts/prepare_readme.py index 1ab3ac1..318c29f 100644 --- a/scripts/prepare_readme.py +++ b/scripts/prepare_readme.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: e0c5957a6035 import re import shutil diff --git a/src/ttd_data/__init__.py b/src/ttd_data/__init__.py index 833c68c..003248e 100644 --- a/src/ttd_data/__init__.py +++ b/src/ttd_data/__init__.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 48aae23daf2a from ._version import ( __title__, @@ -7,8 +8,9 @@ __gen_version__, __user_agent__, ) -from .sdk import * from .sdkconfiguration import * +from .client import DataClient +from .uid2 import IdentityScope, UID2Config, UID2ServiceError, UserIdType VERSION: str = __version__ diff --git a/src/ttd_data/_hooks/__init__.py b/src/ttd_data/_hooks/__init__.py index 2ee66cd..7edc66e 100644 --- a/src/ttd_data/_hooks/__init__.py +++ b/src/ttd_data/_hooks/__init__.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: e02546b7a6fe from .sdkhooks import * from .types import * diff --git a/src/ttd_data/_hooks/sdkhooks.py b/src/ttd_data/_hooks/sdkhooks.py index 765a3f2..631ae89 100644 --- a/src/ttd_data/_hooks/sdkhooks.py +++ b/src/ttd_data/_hooks/sdkhooks.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: f305e5b16c4d import httpx from .types import ( diff --git a/src/ttd_data/_hooks/types.py b/src/ttd_data/_hooks/types.py index 232e514..4056118 100644 --- a/src/ttd_data/_hooks/types.py +++ b/src/ttd_data/_hooks/types.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 9bc3634baba5 from abc import ABC, abstractmethod import httpx diff --git a/src/ttd_data/_version.py b/src/ttd_data/_version.py index 1cb357f..65cf660 100644 --- a/src/ttd_data/_version.py +++ b/src/ttd_data/_version.py @@ -1,12 +1,13 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 7feb4586507e import importlib.metadata __title__: str = "ttd-data" -__version__: str = "0.1.7" +__version__: str = "0.2.10" __openapi_doc_version__: str = "v0.1" -__gen_version__: str = "2.879.13" -__user_agent__: str = "speakeasy-sdk/python 0.1.7 2.879.13 v0.1 ttd-data" +__gen_version__: str = "2.884.4" +__user_agent__: str = "speakeasy-sdk/python 0.2.10 2.884.4 v0.1 ttd-data" try: if __package__ is not None: diff --git a/src/ttd_data/advertiser.py b/src/ttd_data/advertiser.py index d0cfd50..eeb11f9 100644 --- a/src/ttd_data/advertiser.py +++ b/src/ttd_data/advertiser.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 392ead635b4f from .basesdk import BaseSDK from ttd_data import errors, models, utils @@ -17,8 +18,8 @@ def ingest_advertiser_data( data_provider_id: OptionalNullable[str] = UNSET, items: OptionalNullable[ Union[ - List[models.AdvertiserDataItem], - List[models.AdvertiserDataItemTypedDict], + List[models.BaseAdvertiserDataItem], + List[models.BaseAdvertiserDataItemTypedDict], ] ] = UNSET, data_load_trace_id: OptionalNullable[str] = UNSET, @@ -59,7 +60,7 @@ def ingest_advertiser_data( advertiser_id=advertiser_id, data_provider_id=data_provider_id, items=utils.get_pydantic_model( - items, OptionalNullable[List[models.AdvertiserDataItem]] + items, OptionalNullable[List[models.BaseAdvertiserDataItem]] ), data_load_trace_id=data_load_trace_id, data_origins=utils.get_pydantic_model( @@ -104,7 +105,7 @@ def ingest_advertiser_data( security_source=None, ), request=req, - error_status_codes=["400", "403", "413", "429", "4XX", "500", "503", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) @@ -141,8 +142,8 @@ async def ingest_advertiser_data_async( data_provider_id: OptionalNullable[str] = UNSET, items: OptionalNullable[ Union[ - List[models.AdvertiserDataItem], - List[models.AdvertiserDataItemTypedDict], + List[models.BaseAdvertiserDataItem], + List[models.BaseAdvertiserDataItemTypedDict], ] ] = UNSET, data_load_trace_id: OptionalNullable[str] = UNSET, @@ -183,7 +184,7 @@ async def ingest_advertiser_data_async( advertiser_id=advertiser_id, data_provider_id=data_provider_id, items=utils.get_pydantic_model( - items, OptionalNullable[List[models.AdvertiserDataItem]] + items, OptionalNullable[List[models.BaseAdvertiserDataItem]] ), data_load_trace_id=data_load_trace_id, data_origins=utils.get_pydantic_model( @@ -228,7 +229,7 @@ async def ingest_advertiser_data_async( security_source=None, ), request=req, - error_status_codes=["400", "403", "413", "429", "4XX", "500", "503", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) diff --git a/src/ttd_data/basesdk.py b/src/ttd_data/basesdk.py index 1b6ea88..b3e7ee7 100644 --- a/src/ttd_data/basesdk.py +++ b/src/ttd_data/basesdk.py @@ -1,9 +1,15 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 28e634bcfb11 from .sdkconfiguration import SDKConfiguration import httpx from ttd_data import errors, utils -from ttd_data._hooks import AfterErrorContext, AfterSuccessContext, BeforeRequestContext +from ttd_data._hooks import ( + AfterErrorContext, + AfterSuccessContext, + BeforeRequestContext, + HookContext, +) from ttd_data.utils import ( RetryConfig, SerializedRequestBody, @@ -233,10 +239,10 @@ def _build_request_with_client( def do_request( self, - hook_ctx, - request, - error_status_codes, - stream=False, + hook_ctx: HookContext, + request: httpx.Request, + is_error_status_code: Callable[[int], bool], + stream: bool = False, retry_config: Optional[Tuple[RetryConfig, List[str]]] = None, ) -> httpx.Response: client = self.sdk_configuration.client @@ -280,19 +286,6 @@ def do(): "" if stream else http_res.text, ) - if utils.match_status_codes(error_status_codes, http_res.status_code): - result, err = hooks.after_error( - AfterErrorContext(hook_ctx), http_res, None - ) - if err is not None: - logger.debug("Request Exception", exc_info=True) - raise err - if result is not None: - http_res = result - else: - logger.debug("Raising unexpected SDK error") - raise errors.APIError("Unexpected error occurred", http_res) - return http_res if retry_config is not None: @@ -300,17 +293,27 @@ def do(): else: http_res = do() - if not utils.match_status_codes(error_status_codes, http_res.status_code): + if is_error_status_code(http_res.status_code): + result, err = hooks.after_error(AfterErrorContext(hook_ctx), http_res, None) + if err is not None: + logger.debug("Request Exception", exc_info=True) + raise err + if result is not None: + http_res = result + else: + logger.debug("Raising unexpected SDK error") + raise errors.APIError("Unexpected error occurred", http_res) + else: http_res = hooks.after_success(AfterSuccessContext(hook_ctx), http_res) return http_res async def do_request_async( self, - hook_ctx, - request, - error_status_codes, - stream=False, + hook_ctx: HookContext, + request: httpx.Request, + is_error_status_code: Callable[[int], bool], + stream: bool = False, retry_config: Optional[Tuple[RetryConfig, List[str]]] = None, ) -> httpx.Response: client = self.sdk_configuration.async_client @@ -360,20 +363,6 @@ async def do(): "" if stream else http_res.text, ) - if utils.match_status_codes(error_status_codes, http_res.status_code): - result, err = await run_sync_in_thread( - hooks.after_error, AfterErrorContext(hook_ctx), http_res, None - ) - - if err is not None: - logger.debug("Request Exception", exc_info=True) - raise err - if result is not None: - http_res = result - else: - logger.debug("Raising unexpected SDK error") - raise errors.APIError("Unexpected error occurred", http_res) - return http_res if retry_config is not None: @@ -383,7 +372,20 @@ async def do(): else: http_res = await do() - if not utils.match_status_codes(error_status_codes, http_res.status_code): + if is_error_status_code(http_res.status_code): + result, err = await run_sync_in_thread( + hooks.after_error, AfterErrorContext(hook_ctx), http_res, None + ) + + if err is not None: + logger.debug("Request Exception", exc_info=True) + raise err + if result is not None: + http_res = result + else: + logger.debug("Raising unexpected SDK error") + raise errors.APIError("Unexpected error occurred", http_res) + else: http_res = await run_sync_in_thread( hooks.after_success, AfterSuccessContext(hook_ctx), http_res ) diff --git a/src/ttd_data/client.py b/src/ttd_data/client.py new file mode 100644 index 0000000..a328b55 --- /dev/null +++ b/src/ttd_data/client.py @@ -0,0 +1,500 @@ +from __future__ import annotations + +# DataClient wraps BaseDataClient and adds optional UID2 identity mapping. +# Supply `uid2_config` to resolve PII (email/phone) to UID2 before ingest; +# pre-resolved identifiers (TDID, UID2, DAID, etc.) work without it. +# pylint: disable=protected-access + +import asyncio +from functools import cached_property +from typing import Any, Dict, Iterable, List, Optional, Tuple, Type + +from uid2_client import IdentityMapV3Client, IdentityMapV3Input # type: ignore[import-not-found,import-untyped] + +from ttd_data.sdk import BaseDataClient +from ttd_data.types import BaseModel +from ttd_data.models.baseadvertiserdataitem import BaseAdvertiserDataItem +from ttd_data.models.basethirdpartydataitem import BaseThirdPartyDataItem +from ttd_data.models.basepartnerdsrdataitem import BasePartnerDsrDataItem +from ttd_data.models.baseofflineconversiondataitem import ( + BaseOfflineConversionDataItem, +) + +from .uid2.config import UID2Config +from .uid2.models import ( + AdvertiserDataItem, + DataSubjectRequestAdvertiserDataResponse, + DataSubjectRequestMerchantDataResponse, + DataSubjectRequestThirdPartyDataResponse, + IngestAdvertiserDataResponse, + IngestOfflineConversionDataResponse, + IngestThirdPartyDataResponse, + OfflineConversionDataItem, + PartnerDsrDataItem, + ThirdPartyDataItem, +) +from .uid2.resolver import ( + UID2FailedMapping, + UID2Resolution, + resolve_uid2_identifiers_in_place, +) + + +_UID2_ERROR_CODE = "Uid2Error" + + +class DataClient: + """DataClient for the ttd-data SDK — enables ingesting advertiser, + third-party, offline-conversion, and deletion-opt-out data to the + Trade Desk Data API endpoints. + + When `uid2_config` is supplied, raw email/phone ids on each item are + resolved to UID2 (or EUID) before the request leaves; per-item mapping + failures appear in `failed_lines` with `ErrorCode = "Uid2Error"`. + """ + + def __init__( + self, + uid2_config: Optional[UID2Config] = None, + data_client: Optional[BaseDataClient] = None, + **data_client_kwargs: Any, + ) -> None: + self.uid2_config = uid2_config + self.data_client = data_client or BaseDataClient(**data_client_kwargs) + + # ----- UID2 identity-map wiring (internal) ----- + + @cached_property + def _identity_map_client(self) -> Any: + """`uid2_client.IdentityMapV3Client`, built on first access. Requires `uid2_config`.""" + if self.uid2_config is None: + raise RuntimeError( + "UID2 identity-map access requires a UID2Config on the DataClient." + ) + return IdentityMapV3Client( + self.uid2_config.base_url, + self.uid2_config.api_key, + self.uid2_config.client_secret, + ) + + def _generate_identity_map( + self, + emails: Optional[Iterable[str]] = None, + phones: Optional[Iterable[str]] = None, + hashed_emails: Optional[Iterable[str]] = None, + hashed_phones: Optional[Iterable[str]] = None, + ) -> Any: + """Call `POST /identity/map` via the UID2 SDK. + + All four identifier kinds may be supplied in the same call — UID2 + resolves each identifier independently and the response carries a + flat map from raw identifier to resolved UID2 / unmapped reason. + """ + emails_list = list(emails) if emails else [] + hashed_emails_list = list(hashed_emails) if hashed_emails else [] + phones_list = list(phones) if phones else [] + hashed_phones_list = list(hashed_phones) if hashed_phones else [] + + if not (emails_list or hashed_emails_list or phones_list or hashed_phones_list): + raise ValueError( + "At least one of emails / phones / hashed_emails / hashed_phones " + "must be provided." + ) + + identity_map_input = IdentityMapV3Input() + if emails_list: + identity_map_input.with_emails(emails_list) + if hashed_emails_list: + identity_map_input.with_hashed_emails(hashed_emails_list) + if phones_list: + identity_map_input.with_phones(phones_list) + if hashed_phones_list: + identity_map_input.with_hashed_phones(hashed_phones_list) + + return self._identity_map_client.generate_identity_map(identity_map_input) + + # ----- Internal pipeline helpers (used by sub-SDK proxies) ----- + + def _prepare_items_for_request( + self, + items: Any, + wrapper_cls: Type[BaseModel], + base_cls: Type[BaseModel], + ) -> Tuple[Any, Dict[str, UID2Resolution], Dict[int, UID2FailedMapping]]: + """Resolve raw identifiers on `items`, then convert subclass + instances to `base_cls`. The resolver converts emails / hashed + emails / phones / hashed phones and sets the appropriate value + in the UID2 / EUID fields on the `base_cls` object. + + The wrapper_cls → base_cls conversion always runs; UID2 resolution + is skipped when uid2_config is None. + """ + if not items: + return items, {}, {} + + resolutions: Dict[str, UID2Resolution] = {} + failed_mappings: Dict[int, UID2FailedMapping] = {} + + if self.uid2_config is not None: + resolutions, failed_mappings = resolve_uid2_identifiers_in_place( + items, + self._identity_map_client, + self.uid2_config.identity_scope, + ) + + converted: List[Any] = [ + base_cls.model_validate(it.model_dump(by_alias=True)) + if isinstance(it, wrapper_cls) + else it + for it in items + ] + return converted, resolutions, failed_mappings + + async def _prepare_items_for_request_async( + self, + items: Any, + wrapper_cls: Type[BaseModel], + base_cls: Type[BaseModel], + ) -> Tuple[Any, Dict[str, UID2Resolution], Dict[int, UID2FailedMapping]]: + """Async variant: runs the synchronous UID2 SDK call in a worker + thread so the event loop stays free.""" + if not items: + return items, {}, {} + + resolutions: Dict[str, UID2Resolution] = {} + failed_mappings: Dict[int, UID2FailedMapping] = {} + + if self.uid2_config is not None: + resolutions, failed_mappings = await asyncio.to_thread( + self._resolve_items_sync, items + ) + + converted: List[Any] = [ + base_cls.model_validate(it.model_dump(by_alias=True)) + if isinstance(it, wrapper_cls) + else it + for it in items + ] + return converted, resolutions, failed_mappings + + def _resolve_items_sync( + self, items: Any + ) -> Tuple[Dict[str, UID2Resolution], Dict[int, UID2FailedMapping]]: + """Network-bound piece, isolated for `asyncio.to_thread`. Callers must + already have verified `self.uid2_config is not None`.""" + assert self.uid2_config is not None + return resolve_uid2_identifiers_in_place( + items, + self._identity_map_client, + self.uid2_config.identity_scope, + ) + + @staticmethod + def _merge_failures_into_response( + response: Any, + server_response_attr: str, + failed_mappings: Dict[int, UID2FailedMapping], + ) -> None: + """For each `failed_lines` entry whose `ItemNumber` matches a UID2 + mapping failure, set the UID2 reason and `"Uid2Error"` error code. + Mutates `response` in place. + """ + if not failed_mappings: + return + server_response = getattr(response, server_response_attr, None) + if server_response is None: + return + lines = getattr(server_response, "failed_lines", None) + if not lines: + return + for line in lines: + item_number = getattr(line, "item_number", None) + if item_number is None: + continue + try: + idx = int(item_number) - 1 + except (TypeError, ValueError): + continue + failure = failed_mappings.get(idx) + if failure is None: + continue + line.message = failure.reason + line.error_code = _UID2_ERROR_CODE + + @staticmethod + def _build_wrapped_response( + response: Any, + wrapper_cls: Type[BaseModel], + resolutions: Dict[str, UID2Resolution], + ) -> Any: + """Build a `wrapper_cls` from an already-validated speakeasy response, + attaching `identity_resolutions`. + """ + return wrapper_cls.model_construct( + **{f: getattr(response, f) for f in type(response).model_fields}, + identity_resolutions=resolutions, + ) + + # ----- Sub-SDK proxies ----- + + @cached_property + def advertiser(self) -> "_AdvertiserProxy": + return _AdvertiserProxy(self) + + @cached_property + def third_party(self) -> "_ThirdPartyProxy": + return _ThirdPartyProxy(self) + + @cached_property + def offline_conversion(self) -> "_OfflineConversionProxy": + return _OfflineConversionProxy(self) + + @cached_property + def deletion_opt_out(self) -> "_DeletionOptOutProxy": + return _DeletionOptOutProxy(self) + + # ----- Pass-through for any sub-SDK without a UID2 wrapper ----- + + def __getattr__(self, name: str) -> Any: + return getattr(self.data_client, name) + + +# --------------------------------------------------------------------------- +# Sub-SDK proxies — `client..(...)` runs the resolve / convert / +# request / merge / wrap pipeline, then forwards to the speakeasy sub-SDK. +# Any method or attribute not explicitly wrapped (e.g., future endpoints +# added by speakeasy regeneration) falls through to the inner sub-SDK via +# `__getattr__`. +# --------------------------------------------------------------------------- + + +class _AdvertiserProxy: + _SERVER_RESPONSE_ATTR = "advertiser_data_server_response" + + def __init__(self, outer: DataClient) -> None: + self._client = outer + self._sub_sdk = outer.data_client.advertiser + + def ingest_advertiser_data( + self, *, items: Any = None, **kwargs: Any + ) -> IngestAdvertiserDataResponse: + items, resolutions, failures = self._client._prepare_items_for_request( + items, AdvertiserDataItem, BaseAdvertiserDataItem + ) + response = self._sub_sdk.ingest_advertiser_data(items=items, **kwargs) + self._client._merge_failures_into_response( + response, self._SERVER_RESPONSE_ATTR, failures + ) + return self._client._build_wrapped_response( + response, IngestAdvertiserDataResponse, resolutions + ) + + async def ingest_advertiser_data_async( + self, *, items: Any = None, **kwargs: Any + ) -> IngestAdvertiserDataResponse: + items, resolutions, failures = await self._client._prepare_items_for_request_async( + items, AdvertiserDataItem, BaseAdvertiserDataItem + ) + response = await self._sub_sdk.ingest_advertiser_data_async( + items=items, **kwargs + ) + self._client._merge_failures_into_response( + response, self._SERVER_RESPONSE_ATTR, failures + ) + return self._client._build_wrapped_response( + response, IngestAdvertiserDataResponse, resolutions + ) + + def __getattr__(self, name: str) -> Any: + return getattr(self._sub_sdk, name) + + +class _ThirdPartyProxy: + _SERVER_RESPONSE_ATTR = "third_party_data_server_response" + + def __init__(self, outer: DataClient) -> None: + self._client = outer + self._sub_sdk = outer.data_client.third_party + + def ingest_third_party_data( + self, *, items: Any = None, **kwargs: Any + ) -> IngestThirdPartyDataResponse: + items, resolutions, failures = self._client._prepare_items_for_request( + items, ThirdPartyDataItem, BaseThirdPartyDataItem + ) + response = self._sub_sdk.ingest_third_party_data(items=items, **kwargs) + self._client._merge_failures_into_response( + response, self._SERVER_RESPONSE_ATTR, failures + ) + return self._client._build_wrapped_response( + response, IngestThirdPartyDataResponse, resolutions + ) + + async def ingest_third_party_data_async( + self, *, items: Any = None, **kwargs: Any + ) -> IngestThirdPartyDataResponse: + items, resolutions, failures = await self._client._prepare_items_for_request_async( + items, ThirdPartyDataItem, BaseThirdPartyDataItem + ) + response = await self._sub_sdk.ingest_third_party_data_async( + items=items, **kwargs + ) + self._client._merge_failures_into_response( + response, self._SERVER_RESPONSE_ATTR, failures + ) + return self._client._build_wrapped_response( + response, IngestThirdPartyDataResponse, resolutions + ) + + def __getattr__(self, name: str) -> Any: + return getattr(self._sub_sdk, name) + + +class _OfflineConversionProxy: + _SERVER_RESPONSE_ATTR = "offline_conversion_data_server_response" + + def __init__(self, outer: DataClient) -> None: + self._client = outer + self._sub_sdk = outer.data_client.offline_conversion + + def ingest_offline_conversion_data( + self, *, items: Any = None, **kwargs: Any + ) -> IngestOfflineConversionDataResponse: + items, resolutions, failures = self._client._prepare_items_for_request( + items, OfflineConversionDataItem, BaseOfflineConversionDataItem + ) + response = self._sub_sdk.ingest_offline_conversion_data(items=items, **kwargs) + self._client._merge_failures_into_response( + response, self._SERVER_RESPONSE_ATTR, failures + ) + return self._client._build_wrapped_response( + response, IngestOfflineConversionDataResponse, resolutions + ) + + async def ingest_offline_conversion_data_async( + self, *, items: Any = None, **kwargs: Any + ) -> IngestOfflineConversionDataResponse: + items, resolutions, failures = await self._client._prepare_items_for_request_async( + items, OfflineConversionDataItem, BaseOfflineConversionDataItem + ) + response = await self._sub_sdk.ingest_offline_conversion_data_async( + items=items, **kwargs + ) + self._client._merge_failures_into_response( + response, self._SERVER_RESPONSE_ATTR, failures + ) + return self._client._build_wrapped_response( + response, IngestOfflineConversionDataResponse, resolutions + ) + + def __getattr__(self, name: str) -> Any: + return getattr(self._sub_sdk, name) + + +class _DeletionOptOutProxy: + _ADVERTISER_DSR_ATTR = "advertiser_dsr_response" + _MERCHANT_DSR_ATTR = "merchant_dsr_response" + _THIRD_PARTY_DSR_ATTR = "third_party_dsr_response" + + def __init__(self, outer: DataClient) -> None: + self._client = outer + self._sub_sdk = outer.data_client.deletion_opt_out + + def data_subject_request_advertiser_data( + self, *, items: Any = None, **kwargs: Any + ) -> DataSubjectRequestAdvertiserDataResponse: + items, resolutions, failures = self._client._prepare_items_for_request( + items, PartnerDsrDataItem, BasePartnerDsrDataItem + ) + response = self._sub_sdk.data_subject_request_advertiser_data( + items=items, **kwargs + ) + self._client._merge_failures_into_response( + response, self._ADVERTISER_DSR_ATTR, failures + ) + return self._client._build_wrapped_response( + response, DataSubjectRequestAdvertiserDataResponse, resolutions + ) + + def data_subject_request_merchant_data( + self, *, items: Any = None, **kwargs: Any + ) -> DataSubjectRequestMerchantDataResponse: + items, resolutions, failures = self._client._prepare_items_for_request( + items, PartnerDsrDataItem, BasePartnerDsrDataItem + ) + response = self._sub_sdk.data_subject_request_merchant_data( + items=items, **kwargs + ) + self._client._merge_failures_into_response( + response, self._MERCHANT_DSR_ATTR, failures + ) + return self._client._build_wrapped_response( + response, DataSubjectRequestMerchantDataResponse, resolutions + ) + + def data_subject_request_third_party_data( + self, *, items: Any = None, **kwargs: Any + ) -> DataSubjectRequestThirdPartyDataResponse: + items, resolutions, failures = self._client._prepare_items_for_request( + items, PartnerDsrDataItem, BasePartnerDsrDataItem + ) + response = self._sub_sdk.data_subject_request_third_party_data( + items=items, **kwargs + ) + self._client._merge_failures_into_response( + response, self._THIRD_PARTY_DSR_ATTR, failures + ) + return self._client._build_wrapped_response( + response, DataSubjectRequestThirdPartyDataResponse, resolutions + ) + + async def data_subject_request_advertiser_data_async( + self, *, items: Any = None, **kwargs: Any + ) -> DataSubjectRequestAdvertiserDataResponse: + items, resolutions, failures = await self._client._prepare_items_for_request_async( + items, PartnerDsrDataItem, BasePartnerDsrDataItem + ) + response = await self._sub_sdk.data_subject_request_advertiser_data_async( + items=items, **kwargs + ) + self._client._merge_failures_into_response( + response, self._ADVERTISER_DSR_ATTR, failures + ) + return self._client._build_wrapped_response( + response, DataSubjectRequestAdvertiserDataResponse, resolutions + ) + + async def data_subject_request_merchant_data_async( + self, *, items: Any = None, **kwargs: Any + ) -> DataSubjectRequestMerchantDataResponse: + items, resolutions, failures = await self._client._prepare_items_for_request_async( + items, PartnerDsrDataItem, BasePartnerDsrDataItem + ) + response = await self._sub_sdk.data_subject_request_merchant_data_async( + items=items, **kwargs + ) + self._client._merge_failures_into_response( + response, self._MERCHANT_DSR_ATTR, failures + ) + return self._client._build_wrapped_response( + response, DataSubjectRequestMerchantDataResponse, resolutions + ) + + async def data_subject_request_third_party_data_async( + self, *, items: Any = None, **kwargs: Any + ) -> DataSubjectRequestThirdPartyDataResponse: + items, resolutions, failures = await self._client._prepare_items_for_request_async( + items, PartnerDsrDataItem, BasePartnerDsrDataItem + ) + response = await self._sub_sdk.data_subject_request_third_party_data_async( + items=items, **kwargs + ) + self._client._merge_failures_into_response( + response, self._THIRD_PARTY_DSR_ATTR, failures + ) + return self._client._build_wrapped_response( + response, DataSubjectRequestThirdPartyDataResponse, resolutions + ) + + def __getattr__(self, name: str) -> Any: + return getattr(self._sub_sdk, name) diff --git a/src/ttd_data/deletionoptout.py b/src/ttd_data/deletionoptout.py index f21fb99..7383244 100644 --- a/src/ttd_data/deletionoptout.py +++ b/src/ttd_data/deletionoptout.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 8da72b51c89e from .basesdk import BaseSDK from ttd_data import errors, models, utils @@ -17,8 +18,8 @@ def data_subject_request_advertiser_data( data_provider_id: OptionalNullable[str] = UNSET, items: OptionalNullable[ Union[ - List[models.PartnerDsrDataItem], - List[models.PartnerDsrDataItemTypedDict], + List[models.BasePartnerDsrDataItem], + List[models.BasePartnerDsrDataItemTypedDict], ] ] = UNSET, data_load_trace_id: OptionalNullable[str] = UNSET, @@ -57,7 +58,7 @@ def data_subject_request_advertiser_data( advertiser_id=advertiser_id, data_provider_id=data_provider_id, items=utils.get_pydantic_model( - items, OptionalNullable[List[models.PartnerDsrDataItem]] + items, OptionalNullable[List[models.BasePartnerDsrDataItem]] ), data_load_trace_id=data_load_trace_id, request_type=request_type, @@ -100,7 +101,7 @@ def data_subject_request_advertiser_data( security_source=None, ), request=req, - error_status_codes=["400", "403", "413", "429", "4XX", "500", "503", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) @@ -137,8 +138,8 @@ async def data_subject_request_advertiser_data_async( data_provider_id: OptionalNullable[str] = UNSET, items: OptionalNullable[ Union[ - List[models.PartnerDsrDataItem], - List[models.PartnerDsrDataItemTypedDict], + List[models.BasePartnerDsrDataItem], + List[models.BasePartnerDsrDataItemTypedDict], ] ] = UNSET, data_load_trace_id: OptionalNullable[str] = UNSET, @@ -177,7 +178,7 @@ async def data_subject_request_advertiser_data_async( advertiser_id=advertiser_id, data_provider_id=data_provider_id, items=utils.get_pydantic_model( - items, OptionalNullable[List[models.PartnerDsrDataItem]] + items, OptionalNullable[List[models.BasePartnerDsrDataItem]] ), data_load_trace_id=data_load_trace_id, request_type=request_type, @@ -220,7 +221,7 @@ async def data_subject_request_advertiser_data_async( security_source=None, ), request=req, - error_status_codes=["400", "403", "413", "429", "4XX", "500", "503", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) @@ -256,8 +257,8 @@ def data_subject_request_merchant_data( merchant_id: OptionalNullable[int] = UNSET, items: OptionalNullable[ Union[ - List[models.PartnerDsrDataItem], - List[models.PartnerDsrDataItemTypedDict], + List[models.BasePartnerDsrDataItem], + List[models.BasePartnerDsrDataItemTypedDict], ] ] = UNSET, data_load_trace_id: OptionalNullable[str] = UNSET, @@ -294,7 +295,7 @@ def data_subject_request_merchant_data( body=models.MerchantDsrRequest( merchant_id=merchant_id, items=utils.get_pydantic_model( - items, OptionalNullable[List[models.PartnerDsrDataItem]] + items, OptionalNullable[List[models.BasePartnerDsrDataItem]] ), data_load_trace_id=data_load_trace_id, request_type=request_type, @@ -337,7 +338,7 @@ def data_subject_request_merchant_data( security_source=None, ), request=req, - error_status_codes=["400", "403", "413", "429", "4XX", "500", "503", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) @@ -373,8 +374,8 @@ async def data_subject_request_merchant_data_async( merchant_id: OptionalNullable[int] = UNSET, items: OptionalNullable[ Union[ - List[models.PartnerDsrDataItem], - List[models.PartnerDsrDataItemTypedDict], + List[models.BasePartnerDsrDataItem], + List[models.BasePartnerDsrDataItemTypedDict], ] ] = UNSET, data_load_trace_id: OptionalNullable[str] = UNSET, @@ -411,7 +412,7 @@ async def data_subject_request_merchant_data_async( body=models.MerchantDsrRequest( merchant_id=merchant_id, items=utils.get_pydantic_model( - items, OptionalNullable[List[models.PartnerDsrDataItem]] + items, OptionalNullable[List[models.BasePartnerDsrDataItem]] ), data_load_trace_id=data_load_trace_id, request_type=request_type, @@ -454,7 +455,7 @@ async def data_subject_request_merchant_data_async( security_source=None, ), request=req, - error_status_codes=["400", "403", "413", "429", "4XX", "500", "503", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) @@ -491,8 +492,8 @@ def data_subject_request_third_party_data( brand_id: OptionalNullable[str] = UNSET, items: OptionalNullable[ Union[ - List[models.PartnerDsrDataItem], - List[models.PartnerDsrDataItemTypedDict], + List[models.BasePartnerDsrDataItem], + List[models.BasePartnerDsrDataItemTypedDict], ] ] = UNSET, data_load_trace_id: OptionalNullable[str] = UNSET, @@ -531,7 +532,7 @@ def data_subject_request_third_party_data( data_provider_id=data_provider_id, brand_id=brand_id, items=utils.get_pydantic_model( - items, OptionalNullable[List[models.PartnerDsrDataItem]] + items, OptionalNullable[List[models.BasePartnerDsrDataItem]] ), data_load_trace_id=data_load_trace_id, request_type=request_type, @@ -574,7 +575,7 @@ def data_subject_request_third_party_data( security_source=None, ), request=req, - error_status_codes=["400", "403", "413", "429", "4XX", "500", "503", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) @@ -611,8 +612,8 @@ async def data_subject_request_third_party_data_async( brand_id: OptionalNullable[str] = UNSET, items: OptionalNullable[ Union[ - List[models.PartnerDsrDataItem], - List[models.PartnerDsrDataItemTypedDict], + List[models.BasePartnerDsrDataItem], + List[models.BasePartnerDsrDataItemTypedDict], ] ] = UNSET, data_load_trace_id: OptionalNullable[str] = UNSET, @@ -651,7 +652,7 @@ async def data_subject_request_third_party_data_async( data_provider_id=data_provider_id, brand_id=brand_id, items=utils.get_pydantic_model( - items, OptionalNullable[List[models.PartnerDsrDataItem]] + items, OptionalNullable[List[models.BasePartnerDsrDataItem]] ), data_load_trace_id=data_load_trace_id, request_type=request_type, @@ -694,7 +695,7 @@ async def data_subject_request_third_party_data_async( security_source=None, ), request=req, - error_status_codes=["400", "403", "413", "429", "4XX", "500", "503", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) diff --git a/src/ttd_data/errors/__init__.py b/src/ttd_data/errors/__init__.py index 92226d7..14543db 100644 --- a/src/ttd_data/errors/__init__.py +++ b/src/ttd_data/errors/__init__.py @@ -1,6 +1,8 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: c4bbecf8701c from .dataerror import DataError +from .uid2serviceerror import UID2ServiceError from typing import Any, TYPE_CHECKING from ttd_data.utils.dynamic_imports import lazy_getattr, lazy_dir @@ -47,6 +49,7 @@ "OfflineConversionDataServerResponseError", "OfflineConversionDataServerResponseErrorData", "ResponseValidationError", + "UID2ServiceError", "ThirdPartyDataServerResponseError", "ThirdPartyDataServerResponseErrorData", "ThirdPartyDsrResponseError", diff --git a/src/ttd_data/errors/advertiserdataserverresponse_error.py b/src/ttd_data/errors/advertiserdataserverresponse_error.py index aef86c4..83f1ff9 100644 --- a/src/ttd_data/errors/advertiserdataserverresponse_error.py +++ b/src/ttd_data/errors/advertiserdataserverresponse_error.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: b132387f8548 from __future__ import annotations from dataclasses import dataclass, field diff --git a/src/ttd_data/errors/advertiserdsrresponse_error.py b/src/ttd_data/errors/advertiserdsrresponse_error.py index 2072b2c..f7ec7cb 100644 --- a/src/ttd_data/errors/advertiserdsrresponse_error.py +++ b/src/ttd_data/errors/advertiserdsrresponse_error.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 72d43ab98596 from __future__ import annotations from dataclasses import dataclass, field diff --git a/src/ttd_data/errors/apierror.py b/src/ttd_data/errors/apierror.py index 6292361..ac8b8bc 100644 --- a/src/ttd_data/errors/apierror.py +++ b/src/ttd_data/errors/apierror.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 87e982b3d2ab import httpx from typing import Optional diff --git a/src/ttd_data/errors/dataerror.py b/src/ttd_data/errors/dataerror.py index fe4205a..824c244 100644 --- a/src/ttd_data/errors/dataerror.py +++ b/src/ttd_data/errors/dataerror.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 70b3a16b9fdb import httpx from typing import Optional diff --git a/src/ttd_data/errors/merchantdsrresponse_error.py b/src/ttd_data/errors/merchantdsrresponse_error.py index d0e2f52..be5bec8 100644 --- a/src/ttd_data/errors/merchantdsrresponse_error.py +++ b/src/ttd_data/errors/merchantdsrresponse_error.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: a6d87247a795 from __future__ import annotations from dataclasses import dataclass, field diff --git a/src/ttd_data/errors/no_response_error.py b/src/ttd_data/errors/no_response_error.py index 1deab64..0525bdb 100644 --- a/src/ttd_data/errors/no_response_error.py +++ b/src/ttd_data/errors/no_response_error.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: ffd7339470a1 from dataclasses import dataclass diff --git a/src/ttd_data/errors/offlineconversiondataserverresponse_error.py b/src/ttd_data/errors/offlineconversiondataserverresponse_error.py index d90c7f8..ec14ca6 100644 --- a/src/ttd_data/errors/offlineconversiondataserverresponse_error.py +++ b/src/ttd_data/errors/offlineconversiondataserverresponse_error.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 38d3f6702808 from __future__ import annotations from dataclasses import dataclass, field diff --git a/src/ttd_data/errors/responsevalidationerror.py b/src/ttd_data/errors/responsevalidationerror.py index 1e74559..0136315 100644 --- a/src/ttd_data/errors/responsevalidationerror.py +++ b/src/ttd_data/errors/responsevalidationerror.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 59b6bad43c86 import httpx from typing import Optional diff --git a/src/ttd_data/errors/thirdpartydataserverresponse_error.py b/src/ttd_data/errors/thirdpartydataserverresponse_error.py index 8566b78..dd9ab63 100644 --- a/src/ttd_data/errors/thirdpartydataserverresponse_error.py +++ b/src/ttd_data/errors/thirdpartydataserverresponse_error.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 64da9f8c2a4b from __future__ import annotations from dataclasses import dataclass, field diff --git a/src/ttd_data/errors/thirdpartydsrresponse_error.py b/src/ttd_data/errors/thirdpartydsrresponse_error.py index 665bfdf..a5fa947 100644 --- a/src/ttd_data/errors/thirdpartydsrresponse_error.py +++ b/src/ttd_data/errors/thirdpartydsrresponse_error.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 3bcd84adf118 from __future__ import annotations from dataclasses import dataclass, field diff --git a/src/ttd_data/errors/uid2serviceerror.py b/src/ttd_data/errors/uid2serviceerror.py new file mode 100644 index 0000000..bba61c3 --- /dev/null +++ b/src/ttd_data/errors/uid2serviceerror.py @@ -0,0 +1,2 @@ +class UID2ServiceError(Exception): + """Raised when the UID2 identity-map service fails in a way that prevents the entire batch from being processed.""" diff --git a/src/ttd_data/httpclient.py b/src/ttd_data/httpclient.py index 89560b5..b7b67f6 100644 --- a/src/ttd_data/httpclient.py +++ b/src/ttd_data/httpclient.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 2be94ea9b30f # pyright: reportReturnType = false import asyncio diff --git a/src/ttd_data/models/__init__.py b/src/ttd_data/models/__init__.py index 0d27a04..45e9df1 100644 --- a/src/ttd_data/models/__init__.py +++ b/src/ttd_data/models/__init__.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 9cb05e16fec0 from typing import Any, TYPE_CHECKING @@ -6,7 +7,16 @@ if TYPE_CHECKING: from .advertiserdata import AdvertiserData, AdvertiserDataTypedDict - from .advertiserdataitem import AdvertiserDataItem, AdvertiserDataItemTypedDict + from ttd_data.uid2.models import ( + AdvertiserDataItem, + AdvertiserDataItemTypedDict, + OfflineConversionDataItem, + OfflineConversionDataItemTypedDict, + PartnerDsrDataItem, + PartnerDsrDataItemTypedDict, + ThirdPartyDataItem, + ThirdPartyDataItemTypedDict, + ) from .advertiserdatarequest import ( AdvertiserDataRequest, AdvertiserDataRequestTypedDict, @@ -32,6 +42,22 @@ AdvertiserDsrResponse, AdvertiserDsrResponseTypedDict, ) + from .baseadvertiserdataitem import ( + BaseAdvertiserDataItem, + BaseAdvertiserDataItemTypedDict, + ) + from .baseofflineconversiondataitem import ( + BaseOfflineConversionDataItem, + BaseOfflineConversionDataItemTypedDict, + ) + from .basepartnerdsrdataitem import ( + BasePartnerDsrDataItem, + BasePartnerDsrDataItemTypedDict, + ) + from .basethirdpartydataitem import ( + BaseThirdPartyDataItem, + BaseThirdPartyDataItemTypedDict, + ) from .dataorigin import DataOrigin, DataOriginTypedDict from .dataorigintype import DataOriginType from .datasubjectrequestadvertiserdataop import ( @@ -84,10 +110,6 @@ ) from .merchantdsrrequest import MerchantDsrRequest, MerchantDsrRequestTypedDict from .merchantdsrresponse import MerchantDsrResponse, MerchantDsrResponseTypedDict - from .offlineconversiondataitem import ( - OfflineConversionDataItem, - OfflineConversionDataItemTypedDict, - ) from .offlineconversiondatarequest import ( OfflineConversionDataRequest, OfflineConversionDataRequestTypedDict, @@ -103,7 +125,6 @@ OfflineConversionDataServerResponseLine, OfflineConversionDataServerResponseLineTypedDict, ) - from .partnerdsrdataitem import PartnerDsrDataItem, PartnerDsrDataItemTypedDict from .partnerdsrrequesttype import PartnerDsrRequestType from .realtimeconversioneventlineitem import ( RealTimeConversionEventLineItem, @@ -114,7 +135,6 @@ RealTimeConversionEventsPrivacySettingTypedDict, ) from .thirdpartydata import ThirdPartyData, ThirdPartyDataTypedDict - from .thirdpartydataitem import ThirdPartyDataItem, ThirdPartyDataItemTypedDict from .thirdpartydatarequest import ( ThirdPartyDataRequest, ThirdPartyDataRequestTypedDict, @@ -159,6 +179,14 @@ "AdvertiserDsrRequestTypedDict", "AdvertiserDsrResponse", "AdvertiserDsrResponseTypedDict", + "BaseAdvertiserDataItem", + "BaseAdvertiserDataItemTypedDict", + "BaseOfflineConversionDataItem", + "BaseOfflineConversionDataItemTypedDict", + "BasePartnerDsrDataItem", + "BasePartnerDsrDataItemTypedDict", + "BaseThirdPartyDataItem", + "BaseThirdPartyDataItemTypedDict", "DATA_SUBJECT_REQUEST_ADVERTISER_DATA_OP_SERVERS", "DATA_SUBJECT_REQUEST_MERCHANT_DATA_OP_SERVERS", "DATA_SUBJECT_REQUEST_THIRD_PARTY_DATA_OP_SERVERS", @@ -239,8 +267,14 @@ _dynamic_imports: dict[str, str] = { "AdvertiserData": ".advertiserdata", "AdvertiserDataTypedDict": ".advertiserdata", - "AdvertiserDataItem": ".advertiserdataitem", - "AdvertiserDataItemTypedDict": ".advertiserdataitem", + "AdvertiserDataItem": "ttd_data.uid2.models", + "AdvertiserDataItemTypedDict": "ttd_data.uid2.models", + "OfflineConversionDataItem": "ttd_data.uid2.models", + "OfflineConversionDataItemTypedDict": "ttd_data.uid2.models", + "PartnerDsrDataItem": "ttd_data.uid2.models", + "PartnerDsrDataItemTypedDict": "ttd_data.uid2.models", + "ThirdPartyDataItem": "ttd_data.uid2.models", + "ThirdPartyDataItemTypedDict": "ttd_data.uid2.models", "AdvertiserDataRequest": ".advertiserdatarequest", "AdvertiserDataRequestTypedDict": ".advertiserdatarequest", "AdvertiserDataResponseErrorCode": ".advertiserdataresponseerrorcode", @@ -254,6 +288,14 @@ "AdvertiserDsrRequestTypedDict": ".advertiserdsrrequest", "AdvertiserDsrResponse": ".advertiserdsrresponse", "AdvertiserDsrResponseTypedDict": ".advertiserdsrresponse", + "BaseAdvertiserDataItem": ".baseadvertiserdataitem", + "BaseAdvertiserDataItemTypedDict": ".baseadvertiserdataitem", + "BaseOfflineConversionDataItem": ".baseofflineconversiondataitem", + "BaseOfflineConversionDataItemTypedDict": ".baseofflineconversiondataitem", + "BasePartnerDsrDataItem": ".basepartnerdsrdataitem", + "BasePartnerDsrDataItemTypedDict": ".basepartnerdsrdataitem", + "BaseThirdPartyDataItem": ".basethirdpartydataitem", + "BaseThirdPartyDataItemTypedDict": ".basethirdpartydataitem", "DataOrigin": ".dataorigin", "DataOriginTypedDict": ".dataorigin", "DataOriginType": ".dataorigintype", @@ -296,8 +338,6 @@ "MerchantDsrRequestTypedDict": ".merchantdsrrequest", "MerchantDsrResponse": ".merchantdsrresponse", "MerchantDsrResponseTypedDict": ".merchantdsrresponse", - "OfflineConversionDataItem": ".offlineconversiondataitem", - "OfflineConversionDataItemTypedDict": ".offlineconversiondataitem", "OfflineConversionDataRequest": ".offlineconversiondatarequest", "OfflineConversionDataRequestTypedDict": ".offlineconversiondatarequest", "OfflineConversionDataResponseErrorCode": ".offlineconversiondataresponseerrorcode", @@ -305,8 +345,6 @@ "OfflineConversionDataServerResponseTypedDict": ".offlineconversiondataserverresponse", "OfflineConversionDataServerResponseLine": ".offlineconversiondataserverresponseline", "OfflineConversionDataServerResponseLineTypedDict": ".offlineconversiondataserverresponseline", - "PartnerDsrDataItem": ".partnerdsrdataitem", - "PartnerDsrDataItemTypedDict": ".partnerdsrdataitem", "PartnerDsrRequestType": ".partnerdsrrequesttype", "RealTimeConversionEventLineItem": ".realtimeconversioneventlineitem", "RealTimeConversionEventLineItemTypedDict": ".realtimeconversioneventlineitem", @@ -314,8 +352,6 @@ "RealTimeConversionEventsPrivacySettingTypedDict": ".realtimeconversioneventsprivacysetting", "ThirdPartyData": ".thirdpartydata", "ThirdPartyDataTypedDict": ".thirdpartydata", - "ThirdPartyDataItem": ".thirdpartydataitem", - "ThirdPartyDataItemTypedDict": ".thirdpartydataitem", "ThirdPartyDataRequest": ".thirdpartydatarequest", "ThirdPartyDataRequestTypedDict": ".thirdpartydatarequest", "ThirdPartyDataResponseErrorCode": ".thirdpartydataresponseerrorcode", diff --git a/src/ttd_data/models/advertiserdata.py b/src/ttd_data/models/advertiserdata.py index 015bd79..2b6c15e 100644 --- a/src/ttd_data/models/advertiserdata.py +++ b/src/ttd_data/models/advertiserdata.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 3cb2f224c3a1 from __future__ import annotations from datetime import datetime diff --git a/src/ttd_data/models/advertiserdatarequest.py b/src/ttd_data/models/advertiserdatarequest.py index f3658eb..64af06f 100644 --- a/src/ttd_data/models/advertiserdatarequest.py +++ b/src/ttd_data/models/advertiserdatarequest.py @@ -1,7 +1,11 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 11df7e9988cc from __future__ import annotations -from .advertiserdataitem import AdvertiserDataItem, AdvertiserDataItemTypedDict +from .baseadvertiserdataitem import ( + BaseAdvertiserDataItem, + BaseAdvertiserDataItemTypedDict, +) from .dataorigin import DataOrigin, DataOriginTypedDict import pydantic from pydantic import model_serializer @@ -13,7 +17,7 @@ class AdvertiserDataRequestTypedDict(TypedDict): advertiser_id: str data_provider_id: NotRequired[Nullable[str]] - items: NotRequired[Nullable[List[AdvertiserDataItemTypedDict]]] + items: NotRequired[Nullable[List[BaseAdvertiserDataItemTypedDict]]] data_load_trace_id: NotRequired[Nullable[str]] data_origins: NotRequired[Nullable[List[DataOriginTypedDict]]] @@ -26,7 +30,7 @@ class AdvertiserDataRequest(BaseModel): ] = UNSET items: Annotated[ - OptionalNullable[List[AdvertiserDataItem]], pydantic.Field(alias="Items") + OptionalNullable[List[BaseAdvertiserDataItem]], pydantic.Field(alias="Items") ] = UNSET data_load_trace_id: Annotated[ diff --git a/src/ttd_data/models/advertiserdataresponseerrorcode.py b/src/ttd_data/models/advertiserdataresponseerrorcode.py index 41ebd67..16121b6 100644 --- a/src/ttd_data/models/advertiserdataresponseerrorcode.py +++ b/src/ttd_data/models/advertiserdataresponseerrorcode.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: ffcc1a785853 from __future__ import annotations from enum import Enum diff --git a/src/ttd_data/models/advertiserdataserverresponse.py b/src/ttd_data/models/advertiserdataserverresponse.py index 46c26ea..9e177d5 100644 --- a/src/ttd_data/models/advertiserdataserverresponse.py +++ b/src/ttd_data/models/advertiserdataserverresponse.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 532b8698bc5e from __future__ import annotations from .advertiserdataserverresponseline import ( diff --git a/src/ttd_data/models/advertiserdataserverresponseline.py b/src/ttd_data/models/advertiserdataserverresponseline.py index 7506d80..3916abc 100644 --- a/src/ttd_data/models/advertiserdataserverresponseline.py +++ b/src/ttd_data/models/advertiserdataserverresponseline.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: d86dcc352741 from __future__ import annotations from .advertiserdataresponseerrorcode import AdvertiserDataResponseErrorCode diff --git a/src/ttd_data/models/advertiserdsrfailedline.py b/src/ttd_data/models/advertiserdsrfailedline.py index 4bf5830..bb97732 100644 --- a/src/ttd_data/models/advertiserdsrfailedline.py +++ b/src/ttd_data/models/advertiserdsrfailedline.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: bac049718c62 from __future__ import annotations from .dsrerrorcode import DsrErrorCode diff --git a/src/ttd_data/models/advertiserdsrrequest.py b/src/ttd_data/models/advertiserdsrrequest.py index 20e3579..328c59e 100644 --- a/src/ttd_data/models/advertiserdsrrequest.py +++ b/src/ttd_data/models/advertiserdsrrequest.py @@ -1,7 +1,11 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 547e907d3b1a from __future__ import annotations -from .partnerdsrdataitem import PartnerDsrDataItem, PartnerDsrDataItemTypedDict +from .basepartnerdsrdataitem import ( + BasePartnerDsrDataItem, + BasePartnerDsrDataItemTypedDict, +) from .partnerdsrrequesttype import PartnerDsrRequestType import pydantic from pydantic import model_serializer @@ -13,7 +17,7 @@ class AdvertiserDsrRequestTypedDict(TypedDict): advertiser_id: NotRequired[Nullable[str]] data_provider_id: NotRequired[Nullable[str]] - items: NotRequired[Nullable[List[PartnerDsrDataItemTypedDict]]] + items: NotRequired[Nullable[List[BasePartnerDsrDataItemTypedDict]]] data_load_trace_id: NotRequired[Nullable[str]] request_type: NotRequired[PartnerDsrRequestType] @@ -28,7 +32,7 @@ class AdvertiserDsrRequest(BaseModel): ] = UNSET items: Annotated[ - OptionalNullable[List[PartnerDsrDataItem]], pydantic.Field(alias="Items") + OptionalNullable[List[BasePartnerDsrDataItem]], pydantic.Field(alias="Items") ] = UNSET data_load_trace_id: Annotated[ diff --git a/src/ttd_data/models/advertiserdsrresponse.py b/src/ttd_data/models/advertiserdsrresponse.py index 0936113..14345df 100644 --- a/src/ttd_data/models/advertiserdsrresponse.py +++ b/src/ttd_data/models/advertiserdsrresponse.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 6558e631036b from __future__ import annotations from .advertiserdsrfailedline import ( diff --git a/src/ttd_data/models/advertiserdataitem.py b/src/ttd_data/models/baseadvertiserdataitem.py similarity index 96% rename from src/ttd_data/models/advertiserdataitem.py rename to src/ttd_data/models/baseadvertiserdataitem.py index c94cd96..70abb14 100644 --- a/src/ttd_data/models/advertiserdataitem.py +++ b/src/ttd_data/models/baseadvertiserdataitem.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: df8b7792ffe1 from __future__ import annotations from .advertiserdata import AdvertiserData, AdvertiserDataTypedDict @@ -9,7 +10,7 @@ from typing_extensions import Annotated, NotRequired, TypedDict -class AdvertiserDataItemTypedDict(TypedDict): +class BaseAdvertiserDataItemTypedDict(TypedDict): data: List[AdvertiserDataTypedDict] tdid: NotRequired[Nullable[str]] daid: NotRequired[Nullable[str]] @@ -27,7 +28,7 @@ class AdvertiserDataItemTypedDict(TypedDict): cookie_mapping_partner_id: NotRequired[Nullable[str]] -class AdvertiserDataItem(BaseModel): +class BaseAdvertiserDataItem(BaseModel): data: Annotated[List[AdvertiserData], pydantic.Field(alias="Data")] tdid: Annotated[OptionalNullable[str], pydantic.Field(alias="TDID")] = UNSET @@ -129,6 +130,6 @@ def serialize_model(self, handler): try: - AdvertiserDataItem.model_rebuild() + BaseAdvertiserDataItem.model_rebuild() except NameError: pass diff --git a/src/ttd_data/models/offlineconversiondataitem.py b/src/ttd_data/models/baseofflineconversiondataitem.py similarity index 97% rename from src/ttd_data/models/offlineconversiondataitem.py rename to src/ttd_data/models/baseofflineconversiondataitem.py index 9176ed4..11c67b6 100644 --- a/src/ttd_data/models/offlineconversiondataitem.py +++ b/src/ttd_data/models/baseofflineconversiondataitem.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 7922701d5b7f from __future__ import annotations from .realtimeconversioneventlineitem import ( @@ -17,7 +18,7 @@ from typing_extensions import Annotated, NotRequired, TypedDict -class OfflineConversionDataItemTypedDict(TypedDict): +class BaseOfflineConversionDataItemTypedDict(TypedDict): tracking_tag_id: str timestamp_utc: datetime tdid: NotRequired[Nullable[str]] @@ -57,7 +58,7 @@ class OfflineConversionDataItemTypedDict(TypedDict): td10: NotRequired[Nullable[str]] -class OfflineConversionDataItem(BaseModel): +class BaseOfflineConversionDataItem(BaseModel): tracking_tag_id: Annotated[str, pydantic.Field(alias="TrackingTagId")] timestamp_utc: Annotated[datetime, pydantic.Field(alias="TimestampUtc")] @@ -248,6 +249,6 @@ def serialize_model(self, handler): try: - OfflineConversionDataItem.model_rebuild() + BaseOfflineConversionDataItem.model_rebuild() except NameError: pass diff --git a/src/ttd_data/models/partnerdsrdataitem.py b/src/ttd_data/models/basepartnerdsrdataitem.py similarity index 95% rename from src/ttd_data/models/partnerdsrdataitem.py rename to src/ttd_data/models/basepartnerdsrdataitem.py index 7206e46..776f5c7 100644 --- a/src/ttd_data/models/partnerdsrdataitem.py +++ b/src/ttd_data/models/basepartnerdsrdataitem.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 4be97bf202da from __future__ import annotations import pydantic @@ -7,7 +8,7 @@ from typing_extensions import Annotated, NotRequired, TypedDict -class PartnerDsrDataItemTypedDict(TypedDict): +class BasePartnerDsrDataItemTypedDict(TypedDict): tdid: NotRequired[Nullable[str]] daid: NotRequired[Nullable[str]] uid2: NotRequired[Nullable[str]] @@ -23,7 +24,7 @@ class PartnerDsrDataItemTypedDict(TypedDict): iqvia_ppid: NotRequired[Nullable[str]] -class PartnerDsrDataItem(BaseModel): +class BasePartnerDsrDataItem(BaseModel): tdid: Annotated[OptionalNullable[str], pydantic.Field(alias="TDID")] = UNSET daid: Annotated[OptionalNullable[str], pydantic.Field(alias="DAID")] = UNSET @@ -117,6 +118,6 @@ def serialize_model(self, handler): try: - PartnerDsrDataItem.model_rebuild() + BasePartnerDsrDataItem.model_rebuild() except NameError: pass diff --git a/src/ttd_data/models/thirdpartydataitem.py b/src/ttd_data/models/basethirdpartydataitem.py similarity index 96% rename from src/ttd_data/models/thirdpartydataitem.py rename to src/ttd_data/models/basethirdpartydataitem.py index f5e2341..bdd2185 100644 --- a/src/ttd_data/models/thirdpartydataitem.py +++ b/src/ttd_data/models/basethirdpartydataitem.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 38edda88d345 from __future__ import annotations from .thirdpartydata import ThirdPartyData, ThirdPartyDataTypedDict @@ -9,7 +10,7 @@ from typing_extensions import Annotated, NotRequired, TypedDict -class ThirdPartyDataItemTypedDict(TypedDict): +class BaseThirdPartyDataItemTypedDict(TypedDict): data: List[ThirdPartyDataTypedDict] data_provider_user_id: NotRequired[Nullable[str]] tdid: NotRequired[Nullable[str]] @@ -28,7 +29,7 @@ class ThirdPartyDataItemTypedDict(TypedDict): cookie_mapping_partner_id: NotRequired[Nullable[str]] -class ThirdPartyDataItem(BaseModel): +class BaseThirdPartyDataItem(BaseModel): data: Annotated[List[ThirdPartyData], pydantic.Field(alias="Data")] data_provider_user_id: Annotated[ @@ -136,6 +137,6 @@ def serialize_model(self, handler): try: - ThirdPartyDataItem.model_rebuild() + BaseThirdPartyDataItem.model_rebuild() except NameError: pass diff --git a/src/ttd_data/models/dataorigin.py b/src/ttd_data/models/dataorigin.py index a0416b4..ddb92c2 100644 --- a/src/ttd_data/models/dataorigin.py +++ b/src/ttd_data/models/dataorigin.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 01488b30ef74 from __future__ import annotations from .dataorigintype import DataOriginType diff --git a/src/ttd_data/models/dataorigintype.py b/src/ttd_data/models/dataorigintype.py index ccdd8f0..18f0cf3 100644 --- a/src/ttd_data/models/dataorigintype.py +++ b/src/ttd_data/models/dataorigintype.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: c61445332fd2 from __future__ import annotations from enum import Enum diff --git a/src/ttd_data/models/datasubjectrequestadvertiserdataop.py b/src/ttd_data/models/datasubjectrequestadvertiserdataop.py index 040070f..3fc6335 100644 --- a/src/ttd_data/models/datasubjectrequestadvertiserdataop.py +++ b/src/ttd_data/models/datasubjectrequestadvertiserdataop.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 32f041f202c4 from __future__ import annotations from .advertiserdsrrequest import AdvertiserDsrRequest, AdvertiserDsrRequestTypedDict diff --git a/src/ttd_data/models/datasubjectrequestmerchantdataop.py b/src/ttd_data/models/datasubjectrequestmerchantdataop.py index 95f4389..37a642b 100644 --- a/src/ttd_data/models/datasubjectrequestmerchantdataop.py +++ b/src/ttd_data/models/datasubjectrequestmerchantdataop.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 2eaf568ecc7c from __future__ import annotations from .httpmetadata import HTTPMetadata, HTTPMetadataTypedDict diff --git a/src/ttd_data/models/datasubjectrequestthirdpartydataop.py b/src/ttd_data/models/datasubjectrequestthirdpartydataop.py index e81b19c..39acf52 100644 --- a/src/ttd_data/models/datasubjectrequestthirdpartydataop.py +++ b/src/ttd_data/models/datasubjectrequestthirdpartydataop.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 961430b7540f from __future__ import annotations from .httpmetadata import HTTPMetadata, HTTPMetadataTypedDict diff --git a/src/ttd_data/models/dsrerrorcode.py b/src/ttd_data/models/dsrerrorcode.py index a511993..db4420e 100644 --- a/src/ttd_data/models/dsrerrorcode.py +++ b/src/ttd_data/models/dsrerrorcode.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 3d91ba4e034b from __future__ import annotations from enum import Enum diff --git a/src/ttd_data/models/httpmetadata.py b/src/ttd_data/models/httpmetadata.py index 3bc847d..a548716 100644 --- a/src/ttd_data/models/httpmetadata.py +++ b/src/ttd_data/models/httpmetadata.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 2b8260801700 from __future__ import annotations import httpx diff --git a/src/ttd_data/models/ingestadvertiserdataop.py b/src/ttd_data/models/ingestadvertiserdataop.py index 897bc9b..d2aac1b 100644 --- a/src/ttd_data/models/ingestadvertiserdataop.py +++ b/src/ttd_data/models/ingestadvertiserdataop.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 536d8288fda4 from __future__ import annotations from .advertiserdatarequest import AdvertiserDataRequest, AdvertiserDataRequestTypedDict diff --git a/src/ttd_data/models/ingestofflineconversiondataop.py b/src/ttd_data/models/ingestofflineconversiondataop.py index 54a30f2..c580cab 100644 --- a/src/ttd_data/models/ingestofflineconversiondataop.py +++ b/src/ttd_data/models/ingestofflineconversiondataop.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 3dff4f5ef0ef from __future__ import annotations from .httpmetadata import HTTPMetadata, HTTPMetadataTypedDict diff --git a/src/ttd_data/models/ingestthirdpartydataop.py b/src/ttd_data/models/ingestthirdpartydataop.py index 21fe756..3004d00 100644 --- a/src/ttd_data/models/ingestthirdpartydataop.py +++ b/src/ttd_data/models/ingestthirdpartydataop.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 7b9550f1c695 from __future__ import annotations from .httpmetadata import HTTPMetadata, HTTPMetadataTypedDict diff --git a/src/ttd_data/models/merchantdsrfailedline.py b/src/ttd_data/models/merchantdsrfailedline.py index 1dd65f1..5d0a0c4 100644 --- a/src/ttd_data/models/merchantdsrfailedline.py +++ b/src/ttd_data/models/merchantdsrfailedline.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 5100bc2ef724 from __future__ import annotations from .dsrerrorcode import DsrErrorCode diff --git a/src/ttd_data/models/merchantdsrrequest.py b/src/ttd_data/models/merchantdsrrequest.py index 1cc820f..a673e0a 100644 --- a/src/ttd_data/models/merchantdsrrequest.py +++ b/src/ttd_data/models/merchantdsrrequest.py @@ -1,7 +1,11 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 4f8f69a30b61 from __future__ import annotations -from .partnerdsrdataitem import PartnerDsrDataItem, PartnerDsrDataItemTypedDict +from .basepartnerdsrdataitem import ( + BasePartnerDsrDataItem, + BasePartnerDsrDataItemTypedDict, +) from .partnerdsrrequesttype import PartnerDsrRequestType import pydantic from pydantic import model_serializer @@ -12,7 +16,7 @@ class MerchantDsrRequestTypedDict(TypedDict): merchant_id: NotRequired[Nullable[int]] - items: NotRequired[Nullable[List[PartnerDsrDataItemTypedDict]]] + items: NotRequired[Nullable[List[BasePartnerDsrDataItemTypedDict]]] data_load_trace_id: NotRequired[Nullable[str]] request_type: NotRequired[PartnerDsrRequestType] @@ -23,7 +27,7 @@ class MerchantDsrRequest(BaseModel): ] = UNSET items: Annotated[ - OptionalNullable[List[PartnerDsrDataItem]], pydantic.Field(alias="Items") + OptionalNullable[List[BasePartnerDsrDataItem]], pydantic.Field(alias="Items") ] = UNSET data_load_trace_id: Annotated[ diff --git a/src/ttd_data/models/merchantdsrresponse.py b/src/ttd_data/models/merchantdsrresponse.py index 476acfb..fe410ac 100644 --- a/src/ttd_data/models/merchantdsrresponse.py +++ b/src/ttd_data/models/merchantdsrresponse.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: de0ff347dca5 from __future__ import annotations from .merchantdsrfailedline import MerchantDsrFailedLine, MerchantDsrFailedLineTypedDict diff --git a/src/ttd_data/models/offlineconversiondatarequest.py b/src/ttd_data/models/offlineconversiondatarequest.py index 091dc57..1bc47d5 100644 --- a/src/ttd_data/models/offlineconversiondatarequest.py +++ b/src/ttd_data/models/offlineconversiondatarequest.py @@ -1,11 +1,12 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 7d81245ab1fc from __future__ import annotations -from .dataorigin import DataOrigin, DataOriginTypedDict -from .offlineconversiondataitem import ( - OfflineConversionDataItem, - OfflineConversionDataItemTypedDict, +from .baseofflineconversiondataitem import ( + BaseOfflineConversionDataItem, + BaseOfflineConversionDataItemTypedDict, ) +from .dataorigin import DataOrigin, DataOriginTypedDict import pydantic from pydantic import model_serializer from ttd_data.types import BaseModel, Nullable, OptionalNullable, UNSET, UNSET_SENTINEL @@ -16,7 +17,7 @@ class OfflineConversionDataRequestTypedDict(TypedDict): data_provider_id: str user_id_array_metadata_format: NotRequired[Nullable[List[str]]] - items: NotRequired[Nullable[List[OfflineConversionDataItemTypedDict]]] + items: NotRequired[Nullable[List[BaseOfflineConversionDataItemTypedDict]]] data_load_trace_id: NotRequired[Nullable[str]] data_origins: NotRequired[Nullable[List[DataOriginTypedDict]]] @@ -29,7 +30,8 @@ class OfflineConversionDataRequest(BaseModel): ] = UNSET items: Annotated[ - OptionalNullable[List[OfflineConversionDataItem]], pydantic.Field(alias="Items") + OptionalNullable[List[BaseOfflineConversionDataItem]], + pydantic.Field(alias="Items"), ] = UNSET data_load_trace_id: Annotated[ diff --git a/src/ttd_data/models/offlineconversiondataresponseerrorcode.py b/src/ttd_data/models/offlineconversiondataresponseerrorcode.py index 8e816bc..39841b3 100644 --- a/src/ttd_data/models/offlineconversiondataresponseerrorcode.py +++ b/src/ttd_data/models/offlineconversiondataresponseerrorcode.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: e10b97e7f07c from __future__ import annotations from enum import Enum diff --git a/src/ttd_data/models/offlineconversiondataserverresponse.py b/src/ttd_data/models/offlineconversiondataserverresponse.py index 1482f6c..12d9e7e 100644 --- a/src/ttd_data/models/offlineconversiondataserverresponse.py +++ b/src/ttd_data/models/offlineconversiondataserverresponse.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: b4031c03cad3 from __future__ import annotations from .offlineconversiondataserverresponseline import ( diff --git a/src/ttd_data/models/offlineconversiondataserverresponseline.py b/src/ttd_data/models/offlineconversiondataserverresponseline.py index b01fd9a..06171a2 100644 --- a/src/ttd_data/models/offlineconversiondataserverresponseline.py +++ b/src/ttd_data/models/offlineconversiondataserverresponseline.py @@ -1,9 +1,10 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 3f36747f69ac from __future__ import annotations -from .offlineconversiondataitem import ( - OfflineConversionDataItem, - OfflineConversionDataItemTypedDict, +from .baseofflineconversiondataitem import ( + BaseOfflineConversionDataItem, + BaseOfflineConversionDataItemTypedDict, ) from .offlineconversiondataresponseerrorcode import ( OfflineConversionDataResponseErrorCode, @@ -25,7 +26,7 @@ class OfflineConversionDataServerResponseLineTypedDict(TypedDict): error_code: NotRequired[OfflineConversionDataResponseErrorCode] message: NotRequired[Nullable[str]] item_number: NotRequired[Nullable[str]] - original_request: NotRequired[OfflineConversionDataItemTypedDict] + original_request: NotRequired[BaseOfflineConversionDataItemTypedDict] class OfflineConversionDataServerResponseLine(BaseModel): @@ -57,7 +58,7 @@ class OfflineConversionDataServerResponseLine(BaseModel): ] = UNSET original_request: Annotated[ - Optional[OfflineConversionDataItem], pydantic.Field(alias="OriginalRequest") + Optional[BaseOfflineConversionDataItem], pydantic.Field(alias="OriginalRequest") ] = None @model_serializer(mode="wrap") diff --git a/src/ttd_data/models/partnerdsrrequesttype.py b/src/ttd_data/models/partnerdsrrequesttype.py index 5825ec6..1774c4f 100644 --- a/src/ttd_data/models/partnerdsrrequesttype.py +++ b/src/ttd_data/models/partnerdsrrequesttype.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: cac9dd97133a from __future__ import annotations from enum import Enum diff --git a/src/ttd_data/models/realtimeconversioneventlineitem.py b/src/ttd_data/models/realtimeconversioneventlineitem.py index 881a84f..dd39b55 100644 --- a/src/ttd_data/models/realtimeconversioneventlineitem.py +++ b/src/ttd_data/models/realtimeconversioneventlineitem.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: ec4913c7f806 from __future__ import annotations from pydantic import model_serializer diff --git a/src/ttd_data/models/realtimeconversioneventsprivacysetting.py b/src/ttd_data/models/realtimeconversioneventsprivacysetting.py index edcc65f..2682bf7 100644 --- a/src/ttd_data/models/realtimeconversioneventsprivacysetting.py +++ b/src/ttd_data/models/realtimeconversioneventsprivacysetting.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 5c7bd069a3c4 from __future__ import annotations from pydantic import model_serializer diff --git a/src/ttd_data/models/thirdpartydata.py b/src/ttd_data/models/thirdpartydata.py index 1e82737..d2bc365 100644 --- a/src/ttd_data/models/thirdpartydata.py +++ b/src/ttd_data/models/thirdpartydata.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 747d671a1fd0 from __future__ import annotations from datetime import datetime diff --git a/src/ttd_data/models/thirdpartydatarequest.py b/src/ttd_data/models/thirdpartydatarequest.py index 51e0f7f..08a31df 100644 --- a/src/ttd_data/models/thirdpartydatarequest.py +++ b/src/ttd_data/models/thirdpartydatarequest.py @@ -1,8 +1,12 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 51f4e7be4154 from __future__ import annotations +from .basethirdpartydataitem import ( + BaseThirdPartyDataItem, + BaseThirdPartyDataItemTypedDict, +) from .dataorigin import DataOrigin, DataOriginTypedDict -from .thirdpartydataitem import ThirdPartyDataItem, ThirdPartyDataItemTypedDict import pydantic from pydantic import model_serializer from ttd_data.types import BaseModel, Nullable, OptionalNullable, UNSET, UNSET_SENTINEL @@ -12,7 +16,7 @@ class ThirdPartyDataRequestTypedDict(TypedDict): data_provider_id: str - items: NotRequired[Nullable[List[ThirdPartyDataItemTypedDict]]] + items: NotRequired[Nullable[List[BaseThirdPartyDataItemTypedDict]]] data_load_trace_id: NotRequired[Nullable[str]] is_user_id_already_hashed: NotRequired[bool] data_origins: NotRequired[Nullable[List[DataOriginTypedDict]]] @@ -22,7 +26,7 @@ class ThirdPartyDataRequest(BaseModel): data_provider_id: Annotated[str, pydantic.Field(alias="DataProviderId")] items: Annotated[ - OptionalNullable[List[ThirdPartyDataItem]], pydantic.Field(alias="Items") + OptionalNullable[List[BaseThirdPartyDataItem]], pydantic.Field(alias="Items") ] = UNSET data_load_trace_id: Annotated[ diff --git a/src/ttd_data/models/thirdpartydataresponseerrorcode.py b/src/ttd_data/models/thirdpartydataresponseerrorcode.py index f9ff1a9..580e6b7 100644 --- a/src/ttd_data/models/thirdpartydataresponseerrorcode.py +++ b/src/ttd_data/models/thirdpartydataresponseerrorcode.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: c79166cc6728 from __future__ import annotations from enum import Enum diff --git a/src/ttd_data/models/thirdpartydataserverresponse.py b/src/ttd_data/models/thirdpartydataserverresponse.py index f08b9fa..4c23aec 100644 --- a/src/ttd_data/models/thirdpartydataserverresponse.py +++ b/src/ttd_data/models/thirdpartydataserverresponse.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 031ace715edf from __future__ import annotations from .thirdpartydataserverresponseline import ( diff --git a/src/ttd_data/models/thirdpartydataserverresponseline.py b/src/ttd_data/models/thirdpartydataserverresponseline.py index 2663aea..5edbbcb 100644 --- a/src/ttd_data/models/thirdpartydataserverresponseline.py +++ b/src/ttd_data/models/thirdpartydataserverresponseline.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 4a5fa4dcfd73 from __future__ import annotations from .thirdpartydataresponseerrorcode import ThirdPartyDataResponseErrorCode diff --git a/src/ttd_data/models/thirdpartydsrfailedline.py b/src/ttd_data/models/thirdpartydsrfailedline.py index d3a615a..17753ee 100644 --- a/src/ttd_data/models/thirdpartydsrfailedline.py +++ b/src/ttd_data/models/thirdpartydsrfailedline.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 349404a49bbf from __future__ import annotations from .dsrerrorcode import DsrErrorCode diff --git a/src/ttd_data/models/thirdpartydsrrequest.py b/src/ttd_data/models/thirdpartydsrrequest.py index 9f0187f..9d10d61 100644 --- a/src/ttd_data/models/thirdpartydsrrequest.py +++ b/src/ttd_data/models/thirdpartydsrrequest.py @@ -1,7 +1,11 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 586497a7c7ed from __future__ import annotations -from .partnerdsrdataitem import PartnerDsrDataItem, PartnerDsrDataItemTypedDict +from .basepartnerdsrdataitem import ( + BasePartnerDsrDataItem, + BasePartnerDsrDataItemTypedDict, +) from .partnerdsrrequesttype import PartnerDsrRequestType import pydantic from pydantic import model_serializer @@ -13,7 +17,7 @@ class ThirdPartyDsrRequestTypedDict(TypedDict): data_provider_id: NotRequired[Nullable[str]] brand_id: NotRequired[Nullable[str]] - items: NotRequired[Nullable[List[PartnerDsrDataItemTypedDict]]] + items: NotRequired[Nullable[List[BasePartnerDsrDataItemTypedDict]]] data_load_trace_id: NotRequired[Nullable[str]] request_type: NotRequired[PartnerDsrRequestType] @@ -26,7 +30,7 @@ class ThirdPartyDsrRequest(BaseModel): brand_id: Annotated[OptionalNullable[str], pydantic.Field(alias="BrandId")] = UNSET items: Annotated[ - OptionalNullable[List[PartnerDsrDataItem]], pydantic.Field(alias="Items") + OptionalNullable[List[BasePartnerDsrDataItem]], pydantic.Field(alias="Items") ] = UNSET data_load_trace_id: Annotated[ diff --git a/src/ttd_data/models/thirdpartydsrresponse.py b/src/ttd_data/models/thirdpartydsrresponse.py index 7522fc3..b8960ee 100644 --- a/src/ttd_data/models/thirdpartydsrresponse.py +++ b/src/ttd_data/models/thirdpartydsrresponse.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 7d22e340fa71 from __future__ import annotations from .thirdpartydsrfailedline import ( diff --git a/src/ttd_data/offlineconversion.py b/src/ttd_data/offlineconversion.py index 5d48219..6afe422 100644 --- a/src/ttd_data/offlineconversion.py +++ b/src/ttd_data/offlineconversion.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 9cbf832f0f9b from .basesdk import BaseSDK from ttd_data import errors, models, utils @@ -17,8 +18,8 @@ def ingest_offline_conversion_data( user_id_array_metadata_format: OptionalNullable[List[str]] = UNSET, items: OptionalNullable[ Union[ - List[models.OfflineConversionDataItem], - List[models.OfflineConversionDataItemTypedDict], + List[models.BaseOfflineConversionDataItem], + List[models.BaseOfflineConversionDataItemTypedDict], ] ] = UNSET, data_load_trace_id: OptionalNullable[str] = UNSET, @@ -59,7 +60,7 @@ def ingest_offline_conversion_data( data_provider_id=data_provider_id, user_id_array_metadata_format=user_id_array_metadata_format, items=utils.get_pydantic_model( - items, OptionalNullable[List[models.OfflineConversionDataItem]] + items, OptionalNullable[List[models.BaseOfflineConversionDataItem]] ), data_load_trace_id=data_load_trace_id, data_origins=utils.get_pydantic_model( @@ -104,7 +105,7 @@ def ingest_offline_conversion_data( security_source=None, ), request=req, - error_status_codes=["400", "403", "413", "429", "4XX", "500", "503", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) @@ -143,8 +144,8 @@ async def ingest_offline_conversion_data_async( user_id_array_metadata_format: OptionalNullable[List[str]] = UNSET, items: OptionalNullable[ Union[ - List[models.OfflineConversionDataItem], - List[models.OfflineConversionDataItemTypedDict], + List[models.BaseOfflineConversionDataItem], + List[models.BaseOfflineConversionDataItemTypedDict], ] ] = UNSET, data_load_trace_id: OptionalNullable[str] = UNSET, @@ -185,7 +186,7 @@ async def ingest_offline_conversion_data_async( data_provider_id=data_provider_id, user_id_array_metadata_format=user_id_array_metadata_format, items=utils.get_pydantic_model( - items, OptionalNullable[List[models.OfflineConversionDataItem]] + items, OptionalNullable[List[models.BaseOfflineConversionDataItem]] ), data_load_trace_id=data_load_trace_id, data_origins=utils.get_pydantic_model( @@ -230,7 +231,7 @@ async def ingest_offline_conversion_data_async( security_source=None, ), request=req, - error_status_codes=["400", "403", "413", "429", "4XX", "500", "503", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) diff --git a/src/ttd_data/sdk.py b/src/ttd_data/sdk.py index ef876ef..29034de 100644 --- a/src/ttd_data/sdk.py +++ b/src/ttd_data/sdk.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 1472df75b98c from .basesdk import BaseSDK from .httpclient import AsyncHttpClient, ClientOwner, HttpClient, close_clients @@ -20,7 +21,7 @@ from ttd_data.thirdparty import ThirdParty -class DataClient(BaseSDK): +class BaseDataClient(BaseSDK): r"""TTD Data API: Python SDK for The Trade Desk Data API. Provides operations for ingesting advertiser data, third-party data, and offline conversions, as well as handling data subject deletion and opt-out requests. diff --git a/src/ttd_data/sdkconfiguration.py b/src/ttd_data/sdkconfiguration.py index 55179c0..914672a 100644 --- a/src/ttd_data/sdkconfiguration.py +++ b/src/ttd_data/sdkconfiguration.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 6c07cde690cd from ._version import ( __gen_version__, diff --git a/src/ttd_data/thirdparty.py b/src/ttd_data/thirdparty.py index 2e5cb6e..e99faab 100644 --- a/src/ttd_data/thirdparty.py +++ b/src/ttd_data/thirdparty.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: ad2d973ed3be from .basesdk import BaseSDK from ttd_data import errors, models, utils @@ -16,8 +17,8 @@ def ingest_third_party_data( data_provider_id: str, items: OptionalNullable[ Union[ - List[models.ThirdPartyDataItem], - List[models.ThirdPartyDataItemTypedDict], + List[models.BaseThirdPartyDataItem], + List[models.BaseThirdPartyDataItemTypedDict], ] ] = UNSET, data_load_trace_id: OptionalNullable[str] = UNSET, @@ -58,7 +59,7 @@ def ingest_third_party_data( body=models.ThirdPartyDataRequest( data_provider_id=data_provider_id, items=utils.get_pydantic_model( - items, OptionalNullable[List[models.ThirdPartyDataItem]] + items, OptionalNullable[List[models.BaseThirdPartyDataItem]] ), data_load_trace_id=data_load_trace_id, is_user_id_already_hashed=is_user_id_already_hashed, @@ -104,7 +105,7 @@ def ingest_third_party_data( security_source=None, ), request=req, - error_status_codes=["400", "403", "413", "429", "4XX", "500", "503", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) @@ -140,8 +141,8 @@ async def ingest_third_party_data_async( data_provider_id: str, items: OptionalNullable[ Union[ - List[models.ThirdPartyDataItem], - List[models.ThirdPartyDataItemTypedDict], + List[models.BaseThirdPartyDataItem], + List[models.BaseThirdPartyDataItemTypedDict], ] ] = UNSET, data_load_trace_id: OptionalNullable[str] = UNSET, @@ -182,7 +183,7 @@ async def ingest_third_party_data_async( body=models.ThirdPartyDataRequest( data_provider_id=data_provider_id, items=utils.get_pydantic_model( - items, OptionalNullable[List[models.ThirdPartyDataItem]] + items, OptionalNullable[List[models.BaseThirdPartyDataItem]] ), data_load_trace_id=data_load_trace_id, is_user_id_already_hashed=is_user_id_already_hashed, @@ -228,7 +229,7 @@ async def ingest_third_party_data_async( security_source=None, ), request=req, - error_status_codes=["400", "403", "413", "429", "4XX", "500", "503", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) diff --git a/src/ttd_data/types/__init__.py b/src/ttd_data/types/__init__.py index fc76fe0..bb7adfe 100644 --- a/src/ttd_data/types/__init__.py +++ b/src/ttd_data/types/__init__.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 219a89572292 from .basemodel import ( BaseModel, diff --git a/src/ttd_data/types/basemodel.py b/src/ttd_data/types/basemodel.py index a9a640a..196a660 100644 --- a/src/ttd_data/types/basemodel.py +++ b/src/ttd_data/types/basemodel.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 00b5490305bc from pydantic import ConfigDict, model_serializer from pydantic import BaseModel as PydanticBaseModel diff --git a/src/ttd_data/uid2/__init__.py b/src/ttd_data/uid2/__init__.py new file mode 100644 index 0000000..2909c67 --- /dev/null +++ b/src/ttd_data/uid2/__init__.py @@ -0,0 +1,48 @@ +from .config import IdentityScope, UID2Config +from ttd_data.errors import UID2ServiceError +from .resolver import ( + UID2FailedMapping, + UID2Resolution, + UserIdType, + resolve_uid2_identifiers_in_place, +) +from .models import ( + AdvertiserDataItem, + AdvertiserDataItemTypedDict, + DataSubjectRequestAdvertiserDataResponse, + DataSubjectRequestMerchantDataResponse, + DataSubjectRequestThirdPartyDataResponse, + IngestAdvertiserDataResponse, + IngestOfflineConversionDataResponse, + IngestThirdPartyDataResponse, + OfflineConversionDataItem, + OfflineConversionDataItemTypedDict, + PartnerDsrDataItem, + PartnerDsrDataItemTypedDict, + ThirdPartyDataItem, + ThirdPartyDataItemTypedDict, +) + +__all__ = [ + "AdvertiserDataItem", + "AdvertiserDataItemTypedDict", + "DataSubjectRequestAdvertiserDataResponse", + "DataSubjectRequestMerchantDataResponse", + "DataSubjectRequestThirdPartyDataResponse", + "IdentityScope", + "IngestAdvertiserDataResponse", + "IngestOfflineConversionDataResponse", + "IngestThirdPartyDataResponse", + "OfflineConversionDataItem", + "OfflineConversionDataItemTypedDict", + "PartnerDsrDataItem", + "PartnerDsrDataItemTypedDict", + "ThirdPartyDataItem", + "ThirdPartyDataItemTypedDict", + "UID2Config", + "UID2FailedMapping", + "UID2Resolution", + "UID2ServiceError", + "UserIdType", + "resolve_uid2_identifiers_in_place", +] diff --git a/src/ttd_data/uid2/config.py b/src/ttd_data/uid2/config.py new file mode 100644 index 0000000..8b46491 --- /dev/null +++ b/src/ttd_data/uid2/config.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum + + +class IdentityScope(str, Enum): + UID2 = "UID2" + EUID = "EUID" + + +@dataclass +class UID2Config: + base_url: str + api_key: str + client_secret: str + identity_scope: IdentityScope diff --git a/src/ttd_data/uid2/models.py b/src/ttd_data/uid2/models.py new file mode 100644 index 0000000..e2e4af1 --- /dev/null +++ b/src/ttd_data/uid2/models.py @@ -0,0 +1,293 @@ +from __future__ import annotations + +import pydantic +from pydantic import model_serializer, model_validator +from typing import Any, Dict, Iterable +from typing_extensions import Annotated, NotRequired + +from ttd_data.types import Nullable, OptionalNullable, UNSET, UNSET_SENTINEL +from ttd_data.models.baseadvertiserdataitem import ( + BaseAdvertiserDataItem, + BaseAdvertiserDataItemTypedDict, +) +from ttd_data.models.basethirdpartydataitem import ( + BaseThirdPartyDataItem, + BaseThirdPartyDataItemTypedDict, +) +from ttd_data.models.basepartnerdsrdataitem import ( + BasePartnerDsrDataItem, + BasePartnerDsrDataItemTypedDict, +) +from ttd_data.models.baseofflineconversiondataitem import ( + BaseOfflineConversionDataItem, + BaseOfflineConversionDataItemTypedDict, +) +from ttd_data.models.ingestofflineconversiondataop import ( + IngestOfflineConversionDataResponse as _BaseIngestOfflineConversionDataResponse, +) +from ttd_data.models.ingestadvertiserdataop import ( + IngestAdvertiserDataResponse as _BaseIngestAdvertiserDataResponse, +) +from ttd_data.models.ingestthirdpartydataop import ( + IngestThirdPartyDataResponse as _BaseIngestThirdPartyDataResponse, +) +from ttd_data.models.datasubjectrequestadvertiserdataop import ( + DataSubjectRequestAdvertiserDataResponse as _BaseDataSubjectRequestAdvertiserDataResponse, +) +from ttd_data.models.datasubjectrequestmerchantdataop import ( + DataSubjectRequestMerchantDataResponse as _BaseDataSubjectRequestMerchantDataResponse, +) +from ttd_data.models.datasubjectrequestthirdpartydataop import ( + DataSubjectRequestThirdPartyDataResponse as _BaseDataSubjectRequestThirdPartyDataResponse, +) + +from .resolver import UID2Resolution + + +_UID2_RAW_ALIASES = ("Email", "Phone", "HashedEmail", "HashedPhone") + + +# Identifier fields that all resolve into the UID2/EUID space. An item must +# carry at most ONE of these. +_UID2_FAMILY_FIELDS: tuple[tuple[str, str], ...] = ( + ("email", "Email"), + ("hashed_email", "HashedEmail"), + ("phone", "Phone"), + ("hashed_phone", "HashedPhone"), + ("uid2", "UID2"), + ("uid2_token", "UID2Token"), + ("euid", "EUID"), + ("euid_token", "EUIDToken"), +) + + +def _validate_at_most_one_uid2_identifier(self: Any) -> Any: + """`model_validator(mode='after')` hook: ensures the item carries at most + one UID2-family identifier. Caller gets immediate feedback at object + construction. + """ + present: list[str] = [] + for py_attr, json_key in _UID2_FAMILY_FIELDS: + # Count the field as present only if it has a non-empty value; + # UNSET and empty strings both count as "not set". + if getattr(self, py_attr, None): + present.append(json_key) + if len(present) > 1: + raise ValueError( + f"At most one UID2-family identifier may be set per item; " + f"got {len(present)} ({', '.join(present)}). " + f"Use exactly one of " + f"{{Email, HashedEmail, Phone, HashedPhone, UID2, UID2Token, EUID, EUIDToken}}." + ) + return self + + +def _build_serialize_model(extra_optional_fields: Iterable[str]): + """Build a `model_serializer(mode='wrap')` that mirrors the speakeasy + UNSET/Nullable filtering and additionally surfaces the UID2 extension + fields (Email / Phone / HashedEmail / HashedPhone).""" + extras_plus_existing = set(extra_optional_fields) | set(_UID2_RAW_ALIASES) + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + serialized = handler(self) + m = {} + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + is_nullable_and_explicitly_set = ( + k in extras_plus_existing + and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member + ) + if val != UNSET_SENTINEL: + if ( + val is not None + or k not in extras_plus_existing + or is_nullable_and_explicitly_set + ): + m[k] = val + return m + + return serialize_model + + +# --------------------------------------------------------------------------- +# Item wrappers — extend each speakeasy item type with raw / hashed email + +# phone identifiers. `resolve_uid2_identifiers_in_place` resolves them to a UID2 before +# the request leaves the SDK. +# --------------------------------------------------------------------------- + + +class AdvertiserDataItemTypedDict(BaseAdvertiserDataItemTypedDict, total=False): + email: NotRequired[Nullable[str]] + phone: NotRequired[Nullable[str]] + hashed_email: NotRequired[Nullable[str]] + hashed_phone: NotRequired[Nullable[str]] + + +class AdvertiserDataItem(BaseAdvertiserDataItem): + """`BaseAdvertiserDataItem` extended with raw / hashed email + phone.""" + + email: Annotated[OptionalNullable[str], pydantic.Field(alias="Email")] = UNSET + phone: Annotated[OptionalNullable[str], pydantic.Field(alias="Phone")] = UNSET + hashed_email: Annotated[ + OptionalNullable[str], pydantic.Field(alias="HashedEmail") + ] = UNSET + hashed_phone: Annotated[ + OptionalNullable[str], pydantic.Field(alias="HashedPhone") + ] = UNSET + + _validate_uid2_family = model_validator(mode="after")(_validate_at_most_one_uid2_identifier) + + serialize_model = _build_serialize_model({ + "TDID", "DAID", "UID2", "UID2Token", "RampID", "CoreID", "EUID", + "EUIDToken", "ID5", "NetID", "FirstID", "MerkuryID", "IqviaPPID", + "CookieMappingPartnerId", + }) + + +class ThirdPartyDataItemTypedDict(BaseThirdPartyDataItemTypedDict, total=False): + email: NotRequired[Nullable[str]] + phone: NotRequired[Nullable[str]] + hashed_email: NotRequired[Nullable[str]] + hashed_phone: NotRequired[Nullable[str]] + + +class ThirdPartyDataItem(BaseThirdPartyDataItem): + """`BaseThirdPartyDataItem` extended with raw / hashed email + phone.""" + + email: Annotated[OptionalNullable[str], pydantic.Field(alias="Email")] = UNSET + phone: Annotated[OptionalNullable[str], pydantic.Field(alias="Phone")] = UNSET + hashed_email: Annotated[ + OptionalNullable[str], pydantic.Field(alias="HashedEmail") + ] = UNSET + hashed_phone: Annotated[ + OptionalNullable[str], pydantic.Field(alias="HashedPhone") + ] = UNSET + + _validate_uid2_family = model_validator(mode="after")(_validate_at_most_one_uid2_identifier) + + serialize_model = _build_serialize_model({ + "DataProviderUserId", "TDID", "DAID", "UID2", "UID2Token", "RampID", + "CoreID", "EUID", "EUIDToken", "ID5", "NetID", "FirstID", "MerkuryID", + "IqviaPPID", "CookieMappingPartnerId", + }) + + +class PartnerDsrDataItemTypedDict(BasePartnerDsrDataItemTypedDict, total=False): + email: NotRequired[Nullable[str]] + phone: NotRequired[Nullable[str]] + hashed_email: NotRequired[Nullable[str]] + hashed_phone: NotRequired[Nullable[str]] + + +class PartnerDsrDataItem(BasePartnerDsrDataItem): + """`BasePartnerDsrDataItem` extended with raw / hashed email + phone.""" + + email: Annotated[OptionalNullable[str], pydantic.Field(alias="Email")] = UNSET + phone: Annotated[OptionalNullable[str], pydantic.Field(alias="Phone")] = UNSET + hashed_email: Annotated[ + OptionalNullable[str], pydantic.Field(alias="HashedEmail") + ] = UNSET + hashed_phone: Annotated[ + OptionalNullable[str], pydantic.Field(alias="HashedPhone") + ] = UNSET + + _validate_uid2_family = model_validator(mode="after")(_validate_at_most_one_uid2_identifier) + + serialize_model = _build_serialize_model({ + "TDID", "DAID", "UID2", "UID2Token", "RampID", "CoreID", "EUID", + "EUIDToken", "ID5", "NetID", "FirstID", "MerkuryID", "IqviaPPID", + }) + + +class OfflineConversionDataItemTypedDict( + BaseOfflineConversionDataItemTypedDict, total=False +): + email: NotRequired[Nullable[str]] + phone: NotRequired[Nullable[str]] + hashed_email: NotRequired[Nullable[str]] + hashed_phone: NotRequired[Nullable[str]] + + +class OfflineConversionDataItem(BaseOfflineConversionDataItem): + """`BaseOfflineConversionDataItem` extended with raw / hashed email + phone.""" + + email: Annotated[OptionalNullable[str], pydantic.Field(alias="Email")] = UNSET + phone: Annotated[OptionalNullable[str], pydantic.Field(alias="Phone")] = UNSET + hashed_email: Annotated[ + OptionalNullable[str], pydantic.Field(alias="HashedEmail") + ] = UNSET + hashed_phone: Annotated[ + OptionalNullable[str], pydantic.Field(alias="HashedPhone") + ] = UNSET + + _validate_uid2_family = model_validator(mode="after")(_validate_at_most_one_uid2_identifier) + + serialize_model = _build_serialize_model({ + "TDID", "DAID", "UID2", "UID2Token", "RampID", "EUID", "EUIDToken", + "DataProviderUserId", "UserIdArray", "CookieMappingPartnerId", + "OrderId", "ImpressionId", "Value", "ValueCurrency", "Country", + "Region", "Metro", "City", "MerchantId", "EventName", "LineItems", + "PrivacySettings", "TD1", "TD2", "TD3", "TD4", "TD5", "TD6", "TD7", + "TD8", "TD9", "TD10", + }) + + +# --------------------------------------------------------------------------- +# Response wrappers — each adds `identity_resolutions` (a dict keyed by raw +# identifier) on top of the speakeasy-generated response. `exclude=True` +# keeps the field out of any wire serialization. +# --------------------------------------------------------------------------- + + +def _empty_resolutions() -> Dict[str, Any]: + return {} + + +_resolution_field: Any = pydantic.Field(default_factory=_empty_resolutions, exclude=True) + + +class IngestAdvertiserDataResponse(_BaseIngestAdvertiserDataResponse): + identity_resolutions: Annotated[Dict[str, UID2Resolution], _resolution_field] + + +class IngestThirdPartyDataResponse(_BaseIngestThirdPartyDataResponse): + identity_resolutions: Annotated[Dict[str, UID2Resolution], _resolution_field] + + +class IngestOfflineConversionDataResponse(_BaseIngestOfflineConversionDataResponse): + identity_resolutions: Annotated[Dict[str, UID2Resolution], _resolution_field] + + +class DataSubjectRequestAdvertiserDataResponse( + _BaseDataSubjectRequestAdvertiserDataResponse +): + identity_resolutions: Annotated[Dict[str, UID2Resolution], _resolution_field] + + +class DataSubjectRequestMerchantDataResponse( + _BaseDataSubjectRequestMerchantDataResponse +): + identity_resolutions: Annotated[Dict[str, UID2Resolution], _resolution_field] + + +class DataSubjectRequestThirdPartyDataResponse( + _BaseDataSubjectRequestThirdPartyDataResponse +): + identity_resolutions: Annotated[Dict[str, UID2Resolution], _resolution_field] + + +try: + AdvertiserDataItem.model_rebuild() + ThirdPartyDataItem.model_rebuild() + OfflineConversionDataItem.model_rebuild() + PartnerDsrDataItem.model_rebuild() + IngestAdvertiserDataResponse.model_rebuild() + IngestThirdPartyDataResponse.model_rebuild() + IngestOfflineConversionDataResponse.model_rebuild() + DataSubjectRequestAdvertiserDataResponse.model_rebuild() + DataSubjectRequestMerchantDataResponse.model_rebuild() + DataSubjectRequestThirdPartyDataResponse.model_rebuild() +except NameError: + pass diff --git a/src/ttd_data/uid2/resolver.py b/src/ttd_data/uid2/resolver.py new file mode 100644 index 0000000..23950d9 --- /dev/null +++ b/src/ttd_data/uid2/resolver.py @@ -0,0 +1,467 @@ +"""Resolve raw Email / Phone / HashedEmail / HashedPhone on ingest items to +a UID2 (or EUID) via the UID2 identity-map SDK, mutating each item in place. + +Per-item mapping failures substitute a "*" sentinel and are recorded for +the caller to merge into the response's `failed_lines`. Catastrophic SDK +errors raise `UID2ServiceError`. Runs before speakeasy serialization so +the subclass-only raw identifier fields are still readable. +""" + +from __future__ import annotations + +import time +from dataclasses import dataclass +from datetime import datetime +from enum import Enum +from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union +from urllib.error import HTTPError + +from uid2_client import IdentityMapV3Input, UnmappedIdentity # type: ignore[import-not-found,import-untyped] + +from ttd_data.types import UNSET, UNSET_SENTINEL +from ttd_data.models.baseadvertiserdataitem import BaseAdvertiserDataItem +from ttd_data.models.basethirdpartydataitem import BaseThirdPartyDataItem +from ttd_data.models.basepartnerdsrdataitem import BasePartnerDsrDataItem +from ttd_data.models.baseofflineconversiondataitem import ( + BaseOfflineConversionDataItem, +) + +from .config import IdentityScope +from ttd_data.errors import UID2ServiceError + + +# Sentinel UID2/EUID written for items whose identifier could not be mapped. +# The Trade Desk Data APIs respond with an "invalid UID2" error for "*", +# preserving the appropriate item number in the response. +_UID2_SENTINEL = "*" + +# UID2 SDK retry policy. +_RETRYABLE_HTTP_STATUS_CODES = frozenset({408, 429, 502, 503, 504}) +_MAX_RETRIES_PER_BATCH = 5 +_MAX_RETRY_DURATION_SECONDS = 15 +_RETRY_BACKOFF_BASE_SECONDS = 0.2 + +# Maximum raw ids per `IdentityMapV3Input` we send to UID2. +_UID2_BATCH_SIZE = 5000 + + + +@dataclass +class UID2Resolution: + """Per-identifier resolution result returned by the UID2 identity-map API.""" + + current_raw_uid: Optional[str] = None + previous_raw_uid: Optional[str] = None + refresh_from: Optional[datetime] = None + unmapped_reason: Optional[str] = None + + +@dataclass +class UID2FailedMapping: + """Recorded per-item UID2 mapping failure. Keyed by 0-indexed item index + in the caller's input list; the proxy converts to 1-indexed `ItemNumber` + when merging into the Trade Desk Data APIs' `failed_lines`. + """ + + item_index: int + identifier_kind: str + reason: str + array_index: Optional[int] = None + + +# Raw identifier kinds the resolver maps, as (python attr, json/alias key). +_IDENTIFIER_KINDS: Tuple[Tuple[str, str], ...] = ( + ("hashed_email", "HashedEmail"), + ("email", "Email"), + ("hashed_phone", "HashedPhone"), + ("phone", "Phone"), +) +_EXTRA_PY_ATTRS = tuple(p for p, _ in _IDENTIFIER_KINDS) +_EXTRA_JSON_KEYS = tuple(j for _, j in _IDENTIFIER_KINDS) + + +def _read_field(item: Any, py_attr: str, json_key: str) -> Any: + """Read a field off either a pydantic model or a dict, ignoring UNSET. + + Used both for source identifier fields (Email/Phone/HashedEmail/ + HashedPhone) and for the resolved target field (UID2/EUID). + """ + if isinstance(item, dict): + val = item.get(py_attr, item.get(json_key)) + else: + val = getattr(item, py_attr, None) + if val is None or val == UNSET_SENTINEL: + return None + return val + + +# (python attr name, json/alias key) for the field where we write the +# resolved id, indexed by `IdentityScope`. +_TARGET_FIELDS: Dict[IdentityScope, Tuple[str, str]] = { + IdentityScope.UID2: ("uid2", "UID2"), + IdentityScope.EUID: ("euid", "EUID"), +} + + +class UserIdType(str, Enum): + """Type codes for `UserIdArray` entries. + + Members subclass `str`, so they're wire-compatible with the + `List[List[str]]` field type — pass either the enum member or the raw + string. Unknown codes are still accepted as raw strings. + """ + + TDID = "0" + DAID = "1" + UID2 = "2" + UID2_TOKEN = "3" + EUID = "4" + EUID_TOKEN = "5" + RAMP_ID = "6" + # SDK-internal placeholders — resolved to UID2/EUID before send. + HASHED_EMAIL = "-1" + HASHED_PHONE = "-2" + EMAIL = "-3" + PHONE = "-4" + + +_USER_ID_ARRAY_RESOLVABLE_CODES: Dict[str, str] = { + UserIdType.HASHED_EMAIL.value: "HashedEmail", + UserIdType.HASHED_PHONE.value: "HashedPhone", + UserIdType.EMAIL.value: "Email", + UserIdType.PHONE.value: "Phone", +} +_USER_ID_ARRAY_TARGET_CODE: Dict[IdentityScope, str] = { + IdentityScope.UID2: UserIdType.UID2.value, + IdentityScope.EUID: UserIdType.EUID.value, +} + + +def _read_user_id_array(item: Any) -> Optional[List[List[str]]]: + if isinstance(item, dict): + val = item.get("user_id_array", item.get("UserIdArray")) + else: + val = getattr(item, "user_id_array", None) + if val is None or val == UNSET_SENTINEL: + return None + return val + + +def _set_target(item: Any, value: str, py_attr: str, json_key: str) -> None: + if isinstance(item, dict): + # Prefer json-key form when caller built a dict with aliases. + if json_key in item or py_attr not in item: + item[json_key] = value + else: + item[py_attr] = value + else: + setattr(item, py_attr, value) + + +def _clear_extras(item: Any) -> None: + if isinstance(item, dict): + for k in (*_EXTRA_PY_ATTRS, *_EXTRA_JSON_KEYS): + item.pop(k, None) + else: + for attr in _EXTRA_PY_ATTRS: + if hasattr(item, attr): + setattr(item, attr, UNSET) + + +def _record_failure( + failed_mappings: Dict[int, UID2FailedMapping], + item_idx: int, + identifier_kind: str, + reason: str, + array_index: Optional[int] = None, +) -> None: + """Record the first failure per item — callers see one error per row + even if multiple identifiers on the same item failed.""" + if item_idx in failed_mappings: + return + failed_mappings[item_idx] = UID2FailedMapping( + item_index=item_idx, + identifier_kind=identifier_kind, + reason=reason, + array_index=array_index, + ) + + +def _call_identity_map_with_retry( + identity_map_client: Any, + identity_map_input: Any, +) -> Tuple[Optional[Any], Optional[str]]: + """Call `generate_identity_map` with retries on transient HTTP errors. + + Returns `(response, None)` on success, or `(None, reason)` if retries + were exhausted on a retryable HTTP status. Raises `UID2ServiceError` for + non-retryable HTTP errors or unexpected exceptions. + """ + start = time.monotonic() + last_http_error: Optional[HTTPError] = None + for attempt in range(_MAX_RETRIES_PER_BATCH): + try: + return identity_map_client.generate_identity_map(identity_map_input), None + except HTTPError as exc: + if exc.code not in _RETRYABLE_HTTP_STATUS_CODES: + raise UID2ServiceError( + f"UID2 identity-map non-retryable HTTP {exc.code} {exc.reason}" + ) from exc + last_http_error = exc + if time.monotonic() - start >= _MAX_RETRY_DURATION_SECONDS: + break + # Exponential backoff, capped at the remaining budget. + backoff = min( + _RETRY_BACKOFF_BASE_SECONDS * (2 ** attempt), + max(0.0, _MAX_RETRY_DURATION_SECONDS - (time.monotonic() - start)), + ) + if backoff > 0: + time.sleep(backoff) + except UID2ServiceError: + raise + except Exception as exc: + raise UID2ServiceError( + f"UID2 identity-map unexpected error: {exc.__class__.__name__}: {exc}" + ) from exc + assert last_http_error is not None + return None, f"HTTP {last_http_error.code} {last_http_error.reason}" + + +def _add_raw_identifiers( + add_one: Any, raws: List[str], unmapped: Dict[str, Any] +) -> int: + """Add raw ids to the chunk's `IdentityMapV3Input` one at a time. On + `ValueError` from the UID2 SDK's normalization (`with_email` / + `with_phone` reject malformed input), skip the raw id from the input + and route it through `unmapped` so the write-back path applies the + same "*" sentinel + Uid2Error treatment used for optout / unmapped + identifiers. `with_hashed_email` / `with_hashed_phone` do not validate, + so this loop is a no-op cost on the hashed-input paths. + + Returns the number of raw ids successfully added — callers use this to + skip the network call when every raw id in a chunk was invalid. + """ + added = 0 + for raw in raws: + try: + add_one(raw) + added += 1 + except ValueError as exc: + if raw not in unmapped: + unmapped[raw] = UnmappedIdentity(reason=str(exc)) + return added + + +def _chunk_uid2_inputs( + emails: List[str], + hashed_emails: List[str], + phones: List[str], + hashed_phones: List[str], + size: int, +) -> "Iterator[Tuple[List[str], List[str], List[str], List[str]]]": + """Pack raw ids across kinds into chunks of at most `size` total + raw ids. Yields one `(emails, hashed_emails, phones, hashed_phones)` + tuple per chunk. Kinds fill in fixed order (emails → hashed_emails → + phones → hashed_phones) so chunk contents are deterministic. + + Example with size=5000 and 6000 emails + 4000 hashed_emails: + chunk 1 → (5000 emails, 0 hashed_emails, 0, 0) + chunk 2 → (1000 emails, 4000 hashed_emails, 0, 0) + """ + while emails or hashed_emails or phones or hashed_phones: + remaining = size + chunk_emails, emails = emails[:remaining], emails[remaining:] + remaining -= len(chunk_emails) + chunk_hashed_emails, hashed_emails = hashed_emails[:remaining], hashed_emails[remaining:] + remaining -= len(chunk_hashed_emails) + chunk_phones, phones = phones[:remaining], phones[remaining:] + remaining -= len(chunk_phones) + chunk_hashed_phones, hashed_phones = hashed_phones[:remaining], hashed_phones[remaining:] + yield chunk_emails, chunk_hashed_emails, chunk_phones, chunk_hashed_phones + + +ItemLike = Union[ + BaseAdvertiserDataItem, + BaseThirdPartyDataItem, + BasePartnerDsrDataItem, + BaseOfflineConversionDataItem, + Dict[str, Any], +] + + +def resolve_uid2_identifiers_in_place( + items: Sequence[ItemLike], + identity_map_client: Any, + identity_scope: IdentityScope = IdentityScope.UID2, +) -> Tuple[Dict[str, UID2Resolution], Dict[int, UID2FailedMapping]]: + """Resolve raw ids on each item to a UID2 (or EUID, depending on + `identity_scope`) via the UID2 identity-map SDK. + + Mutates `items` in place. On success the target field (UID2/EUID) is + set to the resolved `current_raw_uid`; on per-item failure it is set to + the sentinel "*" and the failure is recorded for the caller to merge + into the Trade Desk Data APIs' `failed_lines`. Raw id fields are cleared + either way. + + Returns `(resolutions, failed_mappings)`: + * `resolutions` — keyed by the raw id submitted; entries for + both mapped and unmapped ids (the latter carry `unmapped_reason`). + * `failed_mappings` — keyed by 0-indexed input item index; one entry + per item that had at least one raw id fail to map. + """ + resolutions: Dict[str, UID2Resolution] = {} + failed_mappings: Dict[int, UID2FailedMapping] = {} + if not items: + return resolutions, failed_mappings + + target_py_attr, target_json_key = _TARGET_FIELDS[identity_scope] + target_user_id_array_code = _USER_ID_ARRAY_TARGET_CODE[identity_scope] + + # Bucket top-level identifiers by kind; items already carrying the target UID2/EUID are skipped. + # Wrapper validator allows only one identifier per item, but we iterate all kinds to tolerate dict-path callers (last write wins). + top_level_identifiers_by_kind: Dict[str, List[Tuple[int, str]]] = { + json_key: [] for _, json_key in _IDENTIFIER_KINDS + } + for idx, item in enumerate(items): + if _read_field(item, target_py_attr, target_json_key): + continue + for py_attr, json_key in _IDENTIFIER_KINDS: + val = _read_field(item, py_attr, json_key) + if val: + top_level_identifiers_by_kind[json_key].append((idx, val)) + + # Bucket UserIdArray entries by kind as (item_idx, array_idx, raw) so we can rewrite in place and pinpoint failures. + # Items without a UserIdArray (advertiser / third-party / DSR) short-circuit via `_read_user_id_array` returning None. + user_id_array_by_kind: Dict[str, List[Tuple[int, int, str]]] = { + json_key: [] for _, json_key in _IDENTIFIER_KINDS + } + for idx, item in enumerate(items): + arr = _read_user_id_array(item) + if not arr: + continue + for arr_idx, entry in enumerate(arr): + if not entry or len(entry) < 2: + continue + id_type_field = entry[0] + type_code = ( + id_type_field.value + if isinstance(id_type_field, Enum) + else str(id_type_field) + ) + resolvable_key = _USER_ID_ARRAY_RESOLVABLE_CODES.get(type_code) + if resolvable_key is None: + continue + raw_id = entry[1] + if raw_id: + user_id_array_by_kind[resolvable_key].append((idx, arr_idx, raw_id)) + + # De-dupe raw ids per kind (top-level + UserIdArray) — UID2 returns one entry per unique input. + def _get_unique_raw_ids(json_key: str) -> List[str]: + return list( + {raw for _, raw in top_level_identifiers_by_kind[json_key]} + | {raw for _, _, raw in user_id_array_by_kind[json_key]} + ) + + emails = _get_unique_raw_ids("Email") + hashed_emails = _get_unique_raw_ids("HashedEmail") + phones = _get_unique_raw_ids("Phone") + hashed_phones = _get_unique_raw_ids("HashedPhone") + + if not (emails or hashed_emails or phones or hashed_phones): + # No identifiers to resolve anywhere. Still clear any UNSET raw + # fields (a no-op in practice) and return. + for item in items: + _clear_extras(item) + return resolutions, failed_mappings + + # Chunk into `_UID2_BATCH_SIZE` raw ids per SDK call, each independently retried. + # Transient-fail chunks fold into the unmapped dict (→ sentinel "*" + Uid2Error); catastrophic errors raise `UID2ServiceError` upstream. + mapped_identities_by_raw_id: Dict[str, Any] = {} + unmapped_identities_by_raw_id: Dict[str, Any] = {} + + for chunk_emails, chunk_hashed_emails, chunk_phones, chunk_hashed_phones in _chunk_uid2_inputs( + emails, hashed_emails, phones, hashed_phones, _UID2_BATCH_SIZE + ): + chunk_input = IdentityMapV3Input() + # Add raw ids per-item so a malformed email/phone (ValueError from `with_email`/`with_phone`) routes to `unmapped` instead of aborting the batch. + added = 0 + added += _add_raw_identifiers(chunk_input.with_email, chunk_emails, unmapped_identities_by_raw_id) + added += _add_raw_identifiers(chunk_input.with_hashed_email, chunk_hashed_emails, unmapped_identities_by_raw_id) + added += _add_raw_identifiers(chunk_input.with_phone, chunk_phones, unmapped_identities_by_raw_id) + added += _add_raw_identifiers(chunk_input.with_hashed_phone, chunk_hashed_phones, unmapped_identities_by_raw_id) + + if added == 0: + # Every raw id in this chunk was rejected by SDK validation — skip the empty-payload call. + continue + + response, transient_reason = _call_identity_map_with_retry( + identity_map_client, chunk_input + ) + if response is None: + reason = transient_reason or "UID2 identity-map transient failure" + transient_failure_entry = UnmappedIdentity(reason=reason) + for raw in (*chunk_emails, *chunk_hashed_emails, *chunk_phones, *chunk_hashed_phones): + if raw not in mapped_identities_by_raw_id and raw not in unmapped_identities_by_raw_id: + unmapped_identities_by_raw_id[raw] = transient_failure_entry + continue + mapped_identities_by_raw_id.update(response.mapped_identities) + # Per-identifier unmapped reasons (optout, invalid, etc.) from the SDK. + # Skip raw ids that this or a prior chunk already resolved. + for raw, entry in response.unmapped_identities.items(): + if raw not in mapped_identities_by_raw_id: + unmapped_identities_by_raw_id[raw] = entry + + for _, json_key in _IDENTIFIER_KINDS: + for item_idx, raw in top_level_identifiers_by_kind[json_key]: + resolved = mapped_identities_by_raw_id.get(raw) + if resolved is not None: + _set_target( + items[item_idx], + resolved.current_raw_uid, + target_py_attr, + target_json_key, + ) + resolutions[raw] = UID2Resolution( + current_raw_uid=resolved.current_raw_uid, + previous_raw_uid=resolved.previous_raw_uid, + refresh_from=resolved.refresh_from, + ) + else: + unmapped_entry = unmapped_identities_by_raw_id.get(raw) + reason = ( + unmapped_entry.raw_reason if unmapped_entry else "unmapped" + ) + _set_target( + items[item_idx], _UID2_SENTINEL, target_py_attr, target_json_key + ) + resolutions[raw] = UID2Resolution(unmapped_reason=reason) + _record_failure(failed_mappings, item_idx, json_key, reason) + + for item_idx, arr_idx, raw in user_id_array_by_kind[json_key]: + resolved = mapped_identities_by_raw_id.get(raw) + if resolved is not None: + arr = _read_user_id_array(items[item_idx]) + # `_read_user_id_array` returns the live reference on pydantic + # models, so this mutation is reflected on the item. + arr[arr_idx] = [target_user_id_array_code, resolved.current_raw_uid] # type: ignore[index] + resolutions[raw] = UID2Resolution( + current_raw_uid=resolved.current_raw_uid, + previous_raw_uid=resolved.previous_raw_uid, + refresh_from=resolved.refresh_from, + ) + else: + unmapped_entry = unmapped_identities_by_raw_id.get(raw) + reason = ( + unmapped_entry.raw_reason if unmapped_entry else "unmapped" + ) + arr = _read_user_id_array(items[item_idx]) + arr[arr_idx] = [target_user_id_array_code, _UID2_SENTINEL] # type: ignore[index] + resolutions[raw] = UID2Resolution(unmapped_reason=reason) + _record_failure( + failed_mappings, item_idx, json_key, reason, array_index=arr_idx + ) + + for item in items: + _clear_extras(item) + + return resolutions, failed_mappings diff --git a/src/ttd_data/utils/__init__.py b/src/ttd_data/utils/__init__.py index 0498cb8..ac6959d 100644 --- a/src/ttd_data/utils/__init__.py +++ b/src/ttd_data/utils/__init__.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 6b87bf7e78f2 from typing import Any, TYPE_CHECKING, Callable, TypeVar import asyncio diff --git a/src/ttd_data/utils/annotations.py b/src/ttd_data/utils/annotations.py index 12e0aa4..1f99145 100644 --- a/src/ttd_data/utils/annotations.py +++ b/src/ttd_data/utils/annotations.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: d1b7b6530770 from enum import Enum from typing import Any, Optional diff --git a/src/ttd_data/utils/datetimes.py b/src/ttd_data/utils/datetimes.py index a6c52cd..1c41404 100644 --- a/src/ttd_data/utils/datetimes.py +++ b/src/ttd_data/utils/datetimes.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 2d794a4852dd from datetime import datetime import sys diff --git a/src/ttd_data/utils/dynamic_imports.py b/src/ttd_data/utils/dynamic_imports.py index 673edf8..f48aa0c 100644 --- a/src/ttd_data/utils/dynamic_imports.py +++ b/src/ttd_data/utils/dynamic_imports.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: b07f05d40ca2 from importlib import import_module import builtins diff --git a/src/ttd_data/utils/enums.py b/src/ttd_data/utils/enums.py index 3324e1b..2471fa5 100644 --- a/src/ttd_data/utils/enums.py +++ b/src/ttd_data/utils/enums.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 04558d0f1a2b import enum import sys diff --git a/src/ttd_data/utils/eventstreaming.py b/src/ttd_data/utils/eventstreaming.py index 3bdcd6d..613d2ff 100644 --- a/src/ttd_data/utils/eventstreaming.py +++ b/src/ttd_data/utils/eventstreaming.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 88cef70df5ca import re import json diff --git a/src/ttd_data/utils/forms.py b/src/ttd_data/utils/forms.py index 1e550bd..7928ef2 100644 --- a/src/ttd_data/utils/forms.py +++ b/src/ttd_data/utils/forms.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 3b8d93e597bb from typing import ( Any, diff --git a/src/ttd_data/utils/headers.py b/src/ttd_data/utils/headers.py index 37864cb..e2d3e00 100644 --- a/src/ttd_data/utils/headers.py +++ b/src/ttd_data/utils/headers.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 4681366f5995 from typing import ( Any, diff --git a/src/ttd_data/utils/logger.py b/src/ttd_data/utils/logger.py index 9664bc0..6b70618 100644 --- a/src/ttd_data/utils/logger.py +++ b/src/ttd_data/utils/logger.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 4590b38a2392 import httpx import logging diff --git a/src/ttd_data/utils/metadata.py b/src/ttd_data/utils/metadata.py index 5abddd5..07e0cc0 100644 --- a/src/ttd_data/utils/metadata.py +++ b/src/ttd_data/utils/metadata.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 4fc5598faf89 from typing import Optional, Type, TypeVar, Union from dataclasses import dataclass diff --git a/src/ttd_data/utils/queryparams.py b/src/ttd_data/utils/queryparams.py index c04e0db..f18930d 100644 --- a/src/ttd_data/utils/queryparams.py +++ b/src/ttd_data/utils/queryparams.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 8325e172a666 from typing import ( Any, diff --git a/src/ttd_data/utils/requestbodies.py b/src/ttd_data/utils/requestbodies.py index 1de32b6..80c8a95 100644 --- a/src/ttd_data/utils/requestbodies.py +++ b/src/ttd_data/utils/requestbodies.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 0924f0dfaef1 import io from dataclasses import dataclass diff --git a/src/ttd_data/utils/retries.py b/src/ttd_data/utils/retries.py index af07d4e..0934a56 100644 --- a/src/ttd_data/utils/retries.py +++ b/src/ttd_data/utils/retries.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 04accebbe68a import asyncio import random diff --git a/src/ttd_data/utils/security.py b/src/ttd_data/utils/security.py index 42d8d78..e56eff2 100644 --- a/src/ttd_data/utils/security.py +++ b/src/ttd_data/utils/security.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: e38af000ccc5 import base64 from typing import ( diff --git a/src/ttd_data/utils/serializers.py b/src/ttd_data/utils/serializers.py index d2149f8..b3c3d4b 100644 --- a/src/ttd_data/utils/serializers.py +++ b/src/ttd_data/utils/serializers.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 625de2eedcad from decimal import Decimal import functools diff --git a/src/ttd_data/utils/unmarshal_json_response.py b/src/ttd_data/utils/unmarshal_json_response.py index 8b476a9..afab1d8 100644 --- a/src/ttd_data/utils/unmarshal_json_response.py +++ b/src/ttd_data/utils/unmarshal_json_response.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 2f612fb73d96 from typing import Any, Optional, Type, TypeVar, overload diff --git a/src/ttd_data/utils/url.py b/src/ttd_data/utils/url.py index c78ccba..178fae6 100644 --- a/src/ttd_data/utils/url.py +++ b/src/ttd_data/utils/url.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 31e17c41ed73 from decimal import Decimal from typing import ( diff --git a/src/ttd_data/utils/values.py b/src/ttd_data/utils/values.py index dae01a4..53c8696 100644 --- a/src/ttd_data/utils/values.py +++ b/src/ttd_data/utils/values.py @@ -1,4 +1,5 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +# @generated-id: 45979a7770a1 from datetime import datetime from enum import Enum diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_uid2.py b/tests/unit/test_uid2.py new file mode 100644 index 0000000..19a54f6 --- /dev/null +++ b/tests/unit/test_uid2.py @@ -0,0 +1,253 @@ +"""Unit tests for the UID2 wrapper layer (`ttd_data.DataClient`, the resolver, +and the at-most-one-identifier validator). No network; the UID2 SDK client +is mocked.""" + +from __future__ import annotations + +from types import SimpleNamespace +from typing import Any, Dict +from unittest.mock import MagicMock +from urllib.error import HTTPError + +import pytest + + +# --------------------------------------------------------------------------- +# Local test helpers +# --------------------------------------------------------------------------- + + +def _make_identity_map_client( + mapped: Dict[str, Any] | None = None, + unmapped: Dict[str, Any] | None = None, +) -> MagicMock: + response = SimpleNamespace( + mapped_identities=mapped or {}, + unmapped_identities=unmapped or {}, + ) + client = MagicMock() + client.generate_identity_map.return_value = response + return client + + +def _mapped(uid: str) -> SimpleNamespace: + return SimpleNamespace( + current_raw_uid=uid, + previous_raw_uid=None, + refresh_from=None, + ) + + +def _unmapped(reason: str) -> SimpleNamespace: + return SimpleNamespace(raw_reason=reason) + + +def _advertiser_item(**kwargs: Any): + """`AdvertiserDataItem` with the required `data` field defaulted.""" + from ttd_data.models import AdvertiserData + from ttd_data.uid2 import AdvertiserDataItem + + return AdvertiserDataItem(data=[AdvertiserData(name="seg")], **kwargs) + + +# --------------------------------------------------------------------------- +# Public surface +# --------------------------------------------------------------------------- + + +def test_public_imports_resolve(): + """Top-level imports a caller is expected to use.""" + from ttd_data import DataClient, IdentityScope, UID2Config # noqa: F401 + from ttd_data.uid2 import ( # noqa: F401 + AdvertiserDataItem, + OfflineConversionDataItem, + PartnerDsrDataItem, + ThirdPartyDataItem, + UID2ServiceError, + ) + + +def test_base_data_client_hidden_from_package_root(): + """Locks in the `from .sdk import *` deletion in `__init__.py`. If a + future regen re-adds the wildcard, this test fails loudly so we don't + silently re-expose the internal Speakeasy class.""" + import ttd_data + + assert not hasattr(ttd_data, "BaseDataClient"), ( + "BaseDataClient must not be exported from the package root. " + "Check that `from .sdk import *` is absent from src/ttd_data/__init__.py." + ) + + +# --------------------------------------------------------------------------- +# At-most-one UID2-family identifier validator +# --------------------------------------------------------------------------- + + +def test_item_accepts_single_raw_identifier(): + item = _advertiser_item(email="alice@example.com") + assert item.email == "alice@example.com" + + +def test_item_rejects_two_raw_identifiers(): + with pytest.raises(ValueError, match="At most one UID2-family identifier"): + _advertiser_item(email="alice@example.com", phone="+15551234567") + + +def test_item_rejects_raw_plus_resolved(): + with pytest.raises(ValueError, match="At most one UID2-family identifier"): + _advertiser_item(email="alice@example.com", uid2="some-uid2") + + +# --------------------------------------------------------------------------- +# Resolver — mapped path +# --------------------------------------------------------------------------- + + +def test_resolver_maps_top_level_email_and_clears_raw(): + from ttd_data.uid2 import IdentityScope + from ttd_data.uid2.resolver import resolve_uid2_identifiers_in_place + + items = [_advertiser_item(email="alice@example.com")] + client = _make_identity_map_client( + mapped={"alice@example.com": _mapped("UID2_ALICE")} + ) + + resolutions, failures = resolve_uid2_identifiers_in_place( + items, client, IdentityScope.UID2 + ) + + assert items[0].uid2 == "UID2_ALICE" + assert not items[0].email + assert resolutions["alice@example.com"].current_raw_uid == "UID2_ALICE" + assert failures == {} + + +def test_resolver_writes_euid_field_under_euid_scope(): + from ttd_data.uid2 import IdentityScope + from ttd_data.uid2.resolver import resolve_uid2_identifiers_in_place + + items = [_advertiser_item(email="bob@example.com")] + client = _make_identity_map_client( + mapped={"bob@example.com": _mapped("EUID_BOB")} + ) + + resolve_uid2_identifiers_in_place(items, client, IdentityScope.EUID) + + assert items[0].euid == "EUID_BOB" + assert not items[0].uid2 + + +# --------------------------------------------------------------------------- +# Resolver — unmapped path (sentinel substitution + failure record) +# --------------------------------------------------------------------------- + + +def test_resolver_substitutes_sentinel_and_records_failure_when_unmapped(): + from ttd_data.uid2 import IdentityScope + from ttd_data.uid2.resolver import resolve_uid2_identifiers_in_place + + items = [_advertiser_item(email="optout@example.com")] + client = _make_identity_map_client( + unmapped={"optout@example.com": _unmapped("optout")} + ) + + resolutions, failures = resolve_uid2_identifiers_in_place( + items, client, IdentityScope.UID2 + ) + + assert items[0].uid2 == "*" + assert resolutions["optout@example.com"].unmapped_reason == "optout" + assert 0 in failures + assert failures[0].reason == "optout" + assert failures[0].identifier_kind == "Email" + + +# --------------------------------------------------------------------------- +# Resolver — transient HTTP failure path +# --------------------------------------------------------------------------- + + +def test_resolver_marks_chunk_failed_on_retry_exhaustion(monkeypatch): + """When `generate_identity_map` keeps raising a retryable HTTP error, + the resolver marks every raw id in the chunk as failed with the HTTP + reason rather than aborting the request.""" + from ttd_data.uid2 import IdentityScope + from ttd_data.uid2.resolver import resolve_uid2_identifiers_in_place + + # Skip the real backoff sleeps so the test runs instantly. + monkeypatch.setattr("ttd_data.uid2.resolver.time.sleep", lambda _s: None) + + items = [_advertiser_item(email="alice@example.com")] + client = _make_identity_map_client() + client.generate_identity_map.side_effect = HTTPError( + url="http://uid2", code=503, msg="Service Unavailable", hdrs=None, fp=None # type: ignore[arg-type] + ) + + resolutions, failures = resolve_uid2_identifiers_in_place( + items, client, IdentityScope.UID2 + ) + + assert items[0].uid2 == "*" + reason = resolutions["alice@example.com"].unmapped_reason + assert reason is not None and "503" in reason + assert failures[0].reason.startswith("HTTP 503") + + +# --------------------------------------------------------------------------- +# Resolver — catastrophic failure path +# --------------------------------------------------------------------------- + + +def test_resolver_raises_service_error_on_non_retryable_http(): + from ttd_data.uid2 import IdentityScope, UID2ServiceError + from ttd_data.uid2.resolver import resolve_uid2_identifiers_in_place + + items = [_advertiser_item(email="alice@example.com")] + client = _make_identity_map_client() + client.generate_identity_map.side_effect = HTTPError( + url="http://uid2", code=401, msg="Unauthorized", hdrs=None, fp=None # type: ignore[arg-type] + ) + + with pytest.raises(UID2ServiceError, match="401"): + resolve_uid2_identifiers_in_place(items, client, IdentityScope.UID2) + + +def test_resolver_raises_service_error_on_unexpected_exception(): + from ttd_data.uid2 import IdentityScope, UID2ServiceError + from ttd_data.uid2.resolver import resolve_uid2_identifiers_in_place + + items = [_advertiser_item(email="alice@example.com")] + client = _make_identity_map_client() + client.generate_identity_map.side_effect = RuntimeError("kaboom") + + with pytest.raises(UID2ServiceError, match="kaboom"): + resolve_uid2_identifiers_in_place(items, client, IdentityScope.UID2) + + +# --------------------------------------------------------------------------- +# DataClient — pass-through when no `uid2_config` +# --------------------------------------------------------------------------- + + +def test_data_client_without_uid2_config_skips_resolution(monkeypatch): + """`DataClient()` with no `uid2_config` must not call the UID2 SDK. + We assert by patching the resolver and confirming it was never invoked.""" + from ttd_data import DataClient + from ttd_data.models.ingestadvertiserdataop import ( + IngestAdvertiserDataResponse as _BaseResponse, + ) + + resolver_called = MagicMock() + monkeypatch.setattr( + "ttd_data.client.resolve_uid2_identifiers_in_place", resolver_called + ) + + base = MagicMock() + base.advertiser.ingest_advertiser_data.return_value = _BaseResponse.model_construct() + client = DataClient(data_client=base) + items = [object()] # opaque — pass-through never inspects items + client.advertiser.ingest_advertiser_data(items=items) + + resolver_called.assert_not_called() + base.advertiser.ingest_advertiser_data.assert_called_once()