diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1332969..3d2ac0b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.0.1" + ".": "0.1.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 9671093..c174044 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/inf-labs/influship-api-3a7359b6f1bc29e6ba39142f71e31055407138bc92aac108c9de72dc3b0604a5.yml -openapi_spec_hash: b5814219a0ab7727c3571b1bbfc14ce3 -config_hash: 32cf7a70e5eb35a8bead35d220e1eedb +configured_endpoints: 20 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/inf-labs/influship-api-d2f171507de2ec0f4a5420e2ea170135523d8bc82c3b4baa7fd45fe261f311e4.yml +openapi_spec_hash: d812fcd5e16d62bc562e0f4618602647 +config_hash: 7ea006cb87abb30bfc76a6af984a7b88 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e9bd85d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,70 @@ +# Changelog + +## 0.1.0 (2026-06-16) + +Full Changelog: [v0.0.1...v0.1.0](https://github.com/Influship/influship-sdk-python/compare/v0.0.1...v0.1.0) + +### Features + +* **api:** add audience, brand alignment, key facts, vibe fields to creator response ([4c7bf09](https://github.com/Influship/influship-sdk-python/commit/4c7bf09985211ae6cb5c9784e8c60e186cf1f58f)) +* **api:** add creator_emails resource with lookup method ([17de04a](https://github.com/Influship/influship-sdk-python/commit/17de04a149b10c676fe01cdf338ee9af40ae1e57)) +* **api:** add creator_kinds parameter to search.create method ([712759a](https://github.com/Influship/influship-sdk-python/commit/712759ae241285079b0060ff896dc25c5cb25237)) +* **api:** add get_post/get_transcript methods to instagram, update post types ([c5d60ec](https://github.com/Influship/influship-sdk-python/commit/c5d60ec1b410ffcb49ece5ff68299d695ba9da36)) +* **api:** add pagination to creators lookalike, extract MatchInfo type, fix cursor passing ([3be1022](https://github.com/Influship/influship-sdk-python/commit/3be10226051e066b82f26fd99170612908ddda3a)) +* **api:** add raw resource with instagram/youtube endpoints ([d765413](https://github.com/Influship/influship-sdk-python/commit/d765413037084f7ad577d00b201a636a864c2f6b)) +* **api:** add search_id/total fields, remove cursor param from search.create ([bbd32c2](https://github.com/Influship/influship-sdk-python/commit/bbd32c2e4391a9f31543862afed49f9fa9ca3d8a)) +* **api:** remove source field from instagram transcript response types ([2c24026](https://github.com/Influship/influship-sdk-python/commit/2c240264f2abd9865713409ac1fb151f2479f261)) +* **api:** update instagram profile and youtube channel/transcript responses ([8b83650](https://github.com/Influship/influship-sdk-python/commit/8b83650ce59ca2f00bb2b8c0f7f8005862a1849d)) +* Fix Stainless pagination and model warnings in SDK config ([2ae3c29](https://github.com/Influship/influship-sdk-python/commit/2ae3c29275bfe74e3399f0ef0c0d950ae388531c)) +* Fix timeout enforcement and migrate Instagram scraper to curl ([c1a121d](https://github.com/Influship/influship-sdk-python/commit/c1a121d337de698699c7dac5a1fb5814648083c9)) +* honest cached /v1/posts video URLs + transcript read-through cache + discovery seeding ([72b71e3](https://github.com/Influship/influship-sdk-python/commit/72b71e36366b017314942b78436a0ea6e7fd1e24)) +* **internal/types:** support eagerly validating pydantic iterators ([252fe69](https://github.com/Influship/influship-sdk-python/commit/252fe69b3d9652bb8ff9f2bf731d63006ec5b0af)) +* **internal:** implement indices array format for query and form serialization ([b0fb922](https://github.com/Influship/influship-sdk-python/commit/b0fb922140eac09079b68be14a023d14a3f3a510)) +* Remove app-level quota system and align billing/rate-limit enforcement with Stripe + Redis ([c7d8a60](https://github.com/Influship/influship-sdk-python/commit/c7d8a6012df2e31b92554e9a8374f1b3217bbe44)) + + +### Bug Fixes + +* **api:** make include parameter optional in creators retrieve method ([7dc834c](https://github.com/Influship/influship-sdk-python/commit/7dc834c23ef1d68df710b2de3baa996510c97b0d)) +* **api:** move RawScraperError to shared types ([061b1e2](https://github.com/Influship/influship-sdk-python/commit/061b1e27385b033eb8966ebf8a02815b7dde26e4)) +* **client:** add missing f-string prefix in file type error message ([cdd7fdb](https://github.com/Influship/influship-sdk-python/commit/cdd7fdb4104a72d2be89f7c6b48517abdb4285f4)) +* **client:** preserve hardcoded query params when merging with user params ([77f87f2](https://github.com/Influship/influship-sdk-python/commit/77f87f2cca5dbacc34e894cf2c69e3f8f208842c)) +* **deps:** bump minimum typing-extensions version ([1ebf842](https://github.com/Influship/influship-sdk-python/commit/1ebf84221a6cc9da7c4bcb414f47c4455f9b2dbe)) +* ensure file data are only sent as 1 parameter ([76674d4](https://github.com/Influship/influship-sdk-python/commit/76674d4a000df97be89864108b9bf3923b4604f3)) +* **pydantic:** do not pass `by_alias` unless set ([dba41e9](https://github.com/Influship/influship-sdk-python/commit/dba41e990d67482fc211cf0674e547c310a8eb26)) +* sanitize endpoint path params ([825e363](https://github.com/Influship/influship-sdk-python/commit/825e363c5eb9b02a3e5fc5baa994fb711ea5cdab)) +* **types:** remove model and provider from instagram transcript response types ([8944d7f](https://github.com/Influship/influship-sdk-python/commit/8944d7f405ad6e3597f397bebce7f13bc0df6fe3)) + + +### Performance Improvements + +* **client:** optimize file structure copying in multipart requests ([eca9767](https://github.com/Influship/influship-sdk-python/commit/eca97676e9a6a75faf45ea3bea0b8eefeac55b8b)) + + +### Chores + +* **ci:** skip lint on metadata-only changes ([5ee4369](https://github.com/Influship/influship-sdk-python/commit/5ee43696849a917cf7a65f66a8b298ebbc4fa2e7)) +* **ci:** skip uploading artifacts on stainless-internal branches ([2c985c8](https://github.com/Influship/influship-sdk-python/commit/2c985c8141c06115d531bc9293bb1291a3d04d45)) +* **internal:** add request options to SSE classes ([fa680d0](https://github.com/Influship/influship-sdk-python/commit/fa680d005639f7836e614b03c165791f1c64c0b3)) +* **internal:** codegen related update ([6ed3406](https://github.com/Influship/influship-sdk-python/commit/6ed3406334c705ae2278a09a040b916c0432d9ae)) +* **internal:** codegen related update ([a7d0c03](https://github.com/Influship/influship-sdk-python/commit/a7d0c03f03bae935f64b076c211d283ad9069495)) +* **internal:** make `test_proxy_environment_variables` more resilient ([1775e30](https://github.com/Influship/influship-sdk-python/commit/1775e30216baabb1affe64be26b9455a35bc7a30)) +* **internal:** make `test_proxy_environment_variables` more resilient to env ([2042983](https://github.com/Influship/influship-sdk-python/commit/20429833fae8ecd9dbe6eee1d140cdb9e0763260)) +* **internal:** more robust bootstrap script ([09db9ca](https://github.com/Influship/influship-sdk-python/commit/09db9caf00714de6ccb7eccd314c0a903b6ea55f)) +* **internal:** refactor Instagram response types to shared models, fix address_json type ([1ce31a9](https://github.com/Influship/influship-sdk-python/commit/1ce31a9406b653b08e333139febfb5acb01ca641)) +* **internal:** reformat pyproject.toml ([c63d362](https://github.com/Influship/influship-sdk-python/commit/c63d362a824ea0145bb90d8be4e493463d32028c)) +* **internal:** regenerate SDK with no functional changes ([2f56132](https://github.com/Influship/influship-sdk-python/commit/2f561321662c817f965ff9d4ad61e3b13f15de38)) +* **internal:** tweak CI branches ([5eea6ab](https://github.com/Influship/influship-sdk-python/commit/5eea6ab384a360852d54e5617e92e0d8d090a12e)) +* **internal:** update gitignore ([a47bc6c](https://github.com/Influship/influship-sdk-python/commit/a47bc6c615c786a620fad78eafb31927cbecc718)) +* update Stainless production repos to influship org ([f92ef39](https://github.com/Influship/influship-sdk-python/commit/f92ef39428d669160509c279e52deb10343e2b2c)) + + +### Documentation + +* **api:** add MCP tool references to creators/posts/profiles/search methods ([fb75a8e](https://github.com/Influship/influship-sdk-python/commit/fb75a8eedb6c1e86edb35b94e82d0eb27b9f4537)) +* **api:** clarify handle parameter format in youtube resource ([ef29fd6](https://github.com/Influship/influship-sdk-python/commit/ef29fd64380a379c0589b4bec19996ae85da389a)) +* **api:** correct profile to channel terminology in youtube channel method ([a8f561f](https://github.com/Influship/influship-sdk-python/commit/a8f561fde0da698d26689d699c394cbcb3475f8d)) +* **api:** update instagram method docstrings ([9d881aa](https://github.com/Influship/influship-sdk-python/commit/9d881aadfbbc7d742d739042a96c973a9824573f)) +* **api:** update pricing documentation across creators/posts/profiles/raw/search ([b82801e](https://github.com/Influship/influship-sdk-python/commit/b82801e0c5cc55dc0509e3a8ac412518ca9745ce)) +* **internal:** update MCP server package name in installation links ([7d78559](https://github.com/Influship/influship-sdk-python/commit/7d78559d721175e453eea56d8fab60a0f4c9a229)) +* update examples ([53aa0d6](https://github.com/Influship/influship-sdk-python/commit/53aa0d622795bdc2076f69d67ac51525b814f7d6)) diff --git a/api.md b/api.md index b7922bf..9c1aa7b 100644 --- a/api.md +++ b/api.md @@ -1,7 +1,7 @@ # Shared Types ```python -from influship.types import CreatorBasic, ProfileSummary +from influship.types import CreatorBasic, ProfileSummary, RawScraperError ``` # Health @@ -69,6 +69,18 @@ Methods: - client.profiles.get(username, \*, platform) -> ProfileGetResponse - client.profiles.lookup(\*\*params) -> ProfileLookupResponse +# CreatorEmails + +Types: + +```python +from influship.types import CreatorEmailLookupResponse +``` + +Methods: + +- client.creator_emails.lookup(\*\*params) -> CreatorEmailLookupResponse + # Posts Types: @@ -91,7 +103,6 @@ Types: from influship.types.raw import ( InstagramSinglePostResponse, InstagramTranscriptResponse, - RawScraperError, InstagramGetPostResponse, InstagramGetPostsResponse, InstagramGetProfileResponse, diff --git a/pyproject.toml b/pyproject.toml index 0355ae6..b23b700 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "influship" -version = "0.0.1" +version = "0.1.0" description = "The official Python library for the Influship API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/influship/_client.py b/src/influship/_client.py index 55a6663..40c00f2 100644 --- a/src/influship/_client.py +++ b/src/influship/_client.py @@ -35,13 +35,14 @@ ) if TYPE_CHECKING: - from .resources import raw, posts, health, search, creators, profiles + from .resources import raw, posts, health, search, creators, profiles, creator_emails from .resources.posts import PostsResource, AsyncPostsResource from .resources.health import HealthResource, AsyncHealthResource from .resources.search import SearchResource, AsyncSearchResource from .resources.raw.raw import RawResource, AsyncRawResource from .resources.creators import CreatorsResource, AsyncCreatorsResource from .resources.profiles import ProfilesResource, AsyncProfilesResource + from .resources.creator_emails import CreatorEmailsResource, AsyncCreatorEmailsResource __all__ = [ "Timeout", @@ -154,6 +155,16 @@ def profiles(self) -> ProfilesResource: return ProfilesResource(self) + @cached_property + def creator_emails(self) -> CreatorEmailsResource: + """Look up known creator email addresses by creator ID or social username. + + Empty or unresolved results are not billable. + """ + from .resources.creator_emails import CreatorEmailsResource + + return CreatorEmailsResource(self) + @cached_property def posts(self) -> PostsResource: """ @@ -381,6 +392,16 @@ def profiles(self) -> AsyncProfilesResource: return AsyncProfilesResource(self) + @cached_property + def creator_emails(self) -> AsyncCreatorEmailsResource: + """Look up known creator email addresses by creator ID or social username. + + Empty or unresolved results are not billable. + """ + from .resources.creator_emails import AsyncCreatorEmailsResource + + return AsyncCreatorEmailsResource(self) + @cached_property def posts(self) -> AsyncPostsResource: """ @@ -550,6 +571,16 @@ def profiles(self) -> profiles.ProfilesResourceWithRawResponse: return ProfilesResourceWithRawResponse(self._client.profiles) + @cached_property + def creator_emails(self) -> creator_emails.CreatorEmailsResourceWithRawResponse: + """Look up known creator email addresses by creator ID or social username. + + Empty or unresolved results are not billable. + """ + from .resources.creator_emails import CreatorEmailsResourceWithRawResponse + + return CreatorEmailsResourceWithRawResponse(self._client.creator_emails) + @cached_property def posts(self) -> posts.PostsResourceWithRawResponse: """ @@ -607,6 +638,16 @@ def profiles(self) -> profiles.AsyncProfilesResourceWithRawResponse: return AsyncProfilesResourceWithRawResponse(self._client.profiles) + @cached_property + def creator_emails(self) -> creator_emails.AsyncCreatorEmailsResourceWithRawResponse: + """Look up known creator email addresses by creator ID or social username. + + Empty or unresolved results are not billable. + """ + from .resources.creator_emails import AsyncCreatorEmailsResourceWithRawResponse + + return AsyncCreatorEmailsResourceWithRawResponse(self._client.creator_emails) + @cached_property def posts(self) -> posts.AsyncPostsResourceWithRawResponse: """ @@ -664,6 +705,16 @@ def profiles(self) -> profiles.ProfilesResourceWithStreamingResponse: return ProfilesResourceWithStreamingResponse(self._client.profiles) + @cached_property + def creator_emails(self) -> creator_emails.CreatorEmailsResourceWithStreamingResponse: + """Look up known creator email addresses by creator ID or social username. + + Empty or unresolved results are not billable. + """ + from .resources.creator_emails import CreatorEmailsResourceWithStreamingResponse + + return CreatorEmailsResourceWithStreamingResponse(self._client.creator_emails) + @cached_property def posts(self) -> posts.PostsResourceWithStreamingResponse: """ @@ -721,6 +772,16 @@ def profiles(self) -> profiles.AsyncProfilesResourceWithStreamingResponse: return AsyncProfilesResourceWithStreamingResponse(self._client.profiles) + @cached_property + def creator_emails(self) -> creator_emails.AsyncCreatorEmailsResourceWithStreamingResponse: + """Look up known creator email addresses by creator ID or social username. + + Empty or unresolved results are not billable. + """ + from .resources.creator_emails import AsyncCreatorEmailsResourceWithStreamingResponse + + return AsyncCreatorEmailsResourceWithStreamingResponse(self._client.creator_emails) + @cached_property def posts(self) -> posts.AsyncPostsResourceWithStreamingResponse: """ diff --git a/src/influship/_version.py b/src/influship/_version.py index de0c280..388cde9 100644 --- a/src/influship/_version.py +++ b/src/influship/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "influship" -__version__ = "0.0.1" # x-release-please-version +__version__ = "0.1.0" # x-release-please-version diff --git a/src/influship/resources/__init__.py b/src/influship/resources/__init__.py index c54bbd6..abeb1ca 100644 --- a/src/influship/resources/__init__.py +++ b/src/influship/resources/__init__.py @@ -48,6 +48,14 @@ ProfilesResourceWithStreamingResponse, AsyncProfilesResourceWithStreamingResponse, ) +from .creator_emails import ( + CreatorEmailsResource, + AsyncCreatorEmailsResource, + CreatorEmailsResourceWithRawResponse, + AsyncCreatorEmailsResourceWithRawResponse, + CreatorEmailsResourceWithStreamingResponse, + AsyncCreatorEmailsResourceWithStreamingResponse, +) __all__ = [ "HealthResource", @@ -74,6 +82,12 @@ "AsyncProfilesResourceWithRawResponse", "ProfilesResourceWithStreamingResponse", "AsyncProfilesResourceWithStreamingResponse", + "CreatorEmailsResource", + "AsyncCreatorEmailsResource", + "CreatorEmailsResourceWithRawResponse", + "AsyncCreatorEmailsResourceWithRawResponse", + "CreatorEmailsResourceWithStreamingResponse", + "AsyncCreatorEmailsResourceWithStreamingResponse", "PostsResource", "AsyncPostsResource", "PostsResourceWithRawResponse", diff --git a/src/influship/resources/creator_emails.py b/src/influship/resources/creator_emails.py new file mode 100644 index 0000000..061bb9f --- /dev/null +++ b/src/influship/resources/creator_emails.py @@ -0,0 +1,197 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable + +import httpx + +from ..types import creator_email_lookup_params +from .._types import Body, Query, Headers, NotGiven, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.creator_email_lookup_response import CreatorEmailLookupResponse + +__all__ = ["CreatorEmailsResource", "AsyncCreatorEmailsResource"] + + +class CreatorEmailsResource(SyncAPIResource): + """Look up known creator email addresses by creator ID or social username. + + Empty or unresolved results are not billable. + """ + + @cached_property + def with_raw_response(self) -> CreatorEmailsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Influship/influship-sdk-python#accessing-raw-response-data-eg-headers + """ + return CreatorEmailsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CreatorEmailsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Influship/influship-sdk-python#with_streaming_response + """ + return CreatorEmailsResourceWithStreamingResponse(self) + + def lookup( + self, + *, + creators: Iterable[creator_email_lookup_params.Creator], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CreatorEmailLookupResponse: + """ + Look up known email addresses for creators by creator ID or social username. + + **Billing behavior:** + + - Charged only for unique resolved creators with at least one returned email + - Empty and unresolved results are not billable + - Returns validation status so unvalidated emails are explicit + + **Pricing**: 5 credits per creator with at least one returned email ($0.05) + + Args: + creators: Creator lookups to resolve. Response rows preserve this input order. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/creator-emails/lookup", + body=maybe_transform({"creators": creators}, creator_email_lookup_params.CreatorEmailLookupParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CreatorEmailLookupResponse, + ) + + +class AsyncCreatorEmailsResource(AsyncAPIResource): + """Look up known creator email addresses by creator ID or social username. + + Empty or unresolved results are not billable. + """ + + @cached_property + def with_raw_response(self) -> AsyncCreatorEmailsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Influship/influship-sdk-python#accessing-raw-response-data-eg-headers + """ + return AsyncCreatorEmailsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCreatorEmailsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Influship/influship-sdk-python#with_streaming_response + """ + return AsyncCreatorEmailsResourceWithStreamingResponse(self) + + async def lookup( + self, + *, + creators: Iterable[creator_email_lookup_params.Creator], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CreatorEmailLookupResponse: + """ + Look up known email addresses for creators by creator ID or social username. + + **Billing behavior:** + + - Charged only for unique resolved creators with at least one returned email + - Empty and unresolved results are not billable + - Returns validation status so unvalidated emails are explicit + + **Pricing**: 5 credits per creator with at least one returned email ($0.05) + + Args: + creators: Creator lookups to resolve. Response rows preserve this input order. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/creator-emails/lookup", + body=await async_maybe_transform( + {"creators": creators}, creator_email_lookup_params.CreatorEmailLookupParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CreatorEmailLookupResponse, + ) + + +class CreatorEmailsResourceWithRawResponse: + def __init__(self, creator_emails: CreatorEmailsResource) -> None: + self._creator_emails = creator_emails + + self.lookup = to_raw_response_wrapper( + creator_emails.lookup, + ) + + +class AsyncCreatorEmailsResourceWithRawResponse: + def __init__(self, creator_emails: AsyncCreatorEmailsResource) -> None: + self._creator_emails = creator_emails + + self.lookup = async_to_raw_response_wrapper( + creator_emails.lookup, + ) + + +class CreatorEmailsResourceWithStreamingResponse: + def __init__(self, creator_emails: CreatorEmailsResource) -> None: + self._creator_emails = creator_emails + + self.lookup = to_streamed_response_wrapper( + creator_emails.lookup, + ) + + +class AsyncCreatorEmailsResourceWithStreamingResponse: + def __init__(self, creator_emails: AsyncCreatorEmailsResource) -> None: + self._creator_emails = creator_emails + + self.lookup = async_to_streamed_response_wrapper( + creator_emails.lookup, + ) diff --git a/src/influship/resources/raw/instagram.py b/src/influship/resources/raw/instagram.py index decb1e2..29e1e65 100644 --- a/src/influship/resources/raw/instagram.py +++ b/src/influship/resources/raw/instagram.py @@ -73,8 +73,12 @@ def get_post( product mentions, music attribution, location, display resources, and video versions. - **Note:** These fields are only guaranteed on this raw single-post lookup for - now. Regular cached post-list endpoints may not include them yet. + **Note:** These fields are only guaranteed on this raw single-post lookup. + Cached post-list endpoints may not include them. + + Returns fresh `video_url` (single best stream) and `video_versions[]` + (multi-bitrate). These are signed Instagram CDN URLs valid for ~24h — download + promptly. For carousels with embedded videos, see `carousel_items[].video_url`. **Pricing**: 1 credit per post scraped ($0.01) @@ -118,6 +122,9 @@ def get_posts( **Note:** Batch post lookup is capped at 20 shortcodes per request and is charged for every requested shortcode. + Returns fresh `video_url` and `video_versions[]` per post (signed IG CDN URLs, + ~24h validity). Batch up to 20 posts at 1 credit ($0.01) each. + **Pricing**: 1 credit per post scraped ($0.01) Args: @@ -167,6 +174,10 @@ def get_profile( **Note:** Live scraping is slower than cached data (2-5 seconds) and costs more. Use cached endpoints when freshness isn't critical. + The `posts[]` array returns up to 12 recent posts with fresh `video_url` for + each video post. This is the cheapest bulk-download path: 0.5 credits ($0.005) + per profile call vs 1 credit per individual raw-post call. + **Pricing**: 0.5 credits per profile scraped ($0.005) Args: @@ -218,12 +229,7 @@ def get_transcript( ) -> InstagramGetTranscriptResponse: """ Transcribe an Instagram video post by shortcode and return the raw post-page - data used for transcription. For now this raw endpoint retranscribes every - request and piggybacks the post plus transcript into our database when the owner - account exists. - - **Note:** Cached transcript reads are a planned follow-up; public pricing stays - the same for live and cached transcript delivery. + data used for transcription. **Pricing**: 5 credits per transcript ($0.05) @@ -273,10 +279,8 @@ def get_transcripts( item per requested shortcode with per-item success or error details. Successful items include the raw post-page data used for transcription. - **Note:** Batch transcription is capped at 10 shortcodes per request, - retranscribes every request for now, and is charged for every requested - shortcode. Cached transcript reads are a planned follow-up; public pricing stays - the same for live and cached transcript delivery. + **Note:** Batch transcription is capped at 10 shortcodes per request and is + charged for every requested shortcode. **Pricing**: 5 credits per transcript ($0.05) @@ -352,8 +356,12 @@ async def get_post( product mentions, music attribution, location, display resources, and video versions. - **Note:** These fields are only guaranteed on this raw single-post lookup for - now. Regular cached post-list endpoints may not include them yet. + **Note:** These fields are only guaranteed on this raw single-post lookup. + Cached post-list endpoints may not include them. + + Returns fresh `video_url` (single best stream) and `video_versions[]` + (multi-bitrate). These are signed Instagram CDN URLs valid for ~24h — download + promptly. For carousels with embedded videos, see `carousel_items[].video_url`. **Pricing**: 1 credit per post scraped ($0.01) @@ -397,6 +405,9 @@ async def get_posts( **Note:** Batch post lookup is capped at 20 shortcodes per request and is charged for every requested shortcode. + Returns fresh `video_url` and `video_versions[]` per post (signed IG CDN URLs, + ~24h validity). Batch up to 20 posts at 1 credit ($0.01) each. + **Pricing**: 1 credit per post scraped ($0.01) Args: @@ -448,6 +459,10 @@ async def get_profile( **Note:** Live scraping is slower than cached data (2-5 seconds) and costs more. Use cached endpoints when freshness isn't critical. + The `posts[]` array returns up to 12 recent posts with fresh `video_url` for + each video post. This is the cheapest bulk-download path: 0.5 credits ($0.005) + per profile call vs 1 credit per individual raw-post call. + **Pricing**: 0.5 credits per profile scraped ($0.005) Args: @@ -499,12 +514,7 @@ async def get_transcript( ) -> InstagramGetTranscriptResponse: """ Transcribe an Instagram video post by shortcode and return the raw post-page - data used for transcription. For now this raw endpoint retranscribes every - request and piggybacks the post plus transcript into our database when the owner - account exists. - - **Note:** Cached transcript reads are a planned follow-up; public pricing stays - the same for live and cached transcript delivery. + data used for transcription. **Pricing**: 5 credits per transcript ($0.05) @@ -554,10 +564,8 @@ async def get_transcripts( item per requested shortcode with per-item success or error details. Successful items include the raw post-page data used for transcription. - **Note:** Batch transcription is capped at 10 shortcodes per request, - retranscribes every request for now, and is charged for every requested - shortcode. Cached transcript reads are a planned follow-up; public pricing stays - the same for live and cached transcript delivery. + **Note:** Batch transcription is capped at 10 shortcodes per request and is + charged for every requested shortcode. **Pricing**: 5 credits per transcript ($0.05) diff --git a/src/influship/resources/raw/youtube.py b/src/influship/resources/raw/youtube.py index e98c1fb..c4db0c7 100644 --- a/src/influship/resources/raw/youtube.py +++ b/src/influship/resources/raw/youtube.py @@ -76,7 +76,10 @@ def get_channel( **Pricing**: 0.5 credits per channel scraped ($0.005) Args: - handle: YouTube channel handle + handle: YouTube channel handle. Accepts a bare handle (`techreviews`), an `@handle` + (`@techreviews`), or a full channel URL (`https://youtube.com/@techreviews`); + surrounding share tokens and trailing paths are ignored. A value that is not a + channel handle returns a 400 validation error. include_videos: Include recent videos in response @@ -140,7 +143,10 @@ def get_channel_transcripts( **Pricing**: 0.5 credits per transcript fetched ($0.005) Args: - handle: YouTube channel handle + handle: YouTube channel handle. Accepts a bare handle (`techreviews`), an `@handle` + (`@techreviews`), or a full channel URL (`https://youtube.com/@techreviews`); + surrounding share tokens and trailing paths are ignored. A value that is not a + channel handle returns a 400 validation error. include_segments: Include timestamped transcript segments in response @@ -334,7 +340,10 @@ async def get_channel( **Pricing**: 0.5 credits per channel scraped ($0.005) Args: - handle: YouTube channel handle + handle: YouTube channel handle. Accepts a bare handle (`techreviews`), an `@handle` + (`@techreviews`), or a full channel URL (`https://youtube.com/@techreviews`); + surrounding share tokens and trailing paths are ignored. A value that is not a + channel handle returns a 400 validation error. include_videos: Include recent videos in response @@ -398,7 +407,10 @@ async def get_channel_transcripts( **Pricing**: 0.5 credits per transcript fetched ($0.005) Args: - handle: YouTube channel handle + handle: YouTube channel handle. Accepts a bare handle (`techreviews`), an `@handle` + (`@techreviews`), or a full channel URL (`https://youtube.com/@techreviews`); + surrounding share tokens and trailing paths are ignored. A value that is not a + channel handle returns a 400 validation error. include_segments: Include timestamped transcript segments in response diff --git a/src/influship/types/__init__.py b/src/influship/types/__init__.py index 88a1500..de3a20f 100644 --- a/src/influship/types/__init__.py +++ b/src/influship/types/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from .shared import CreatorBasic as CreatorBasic, ProfileSummary as ProfileSummary +from .shared import CreatorBasic as CreatorBasic, ProfileSummary as ProfileSummary, RawScraperError as RawScraperError from .match_info import MatchInfo as MatchInfo from .profile_growth import ProfileGrowth as ProfileGrowth from .profile_metrics import ProfileMetrics as ProfileMetrics @@ -25,4 +25,6 @@ from .creator_retrieve_response import CreatorRetrieveResponse as CreatorRetrieveResponse from .creator_lookalike_response import CreatorLookalikeResponse as CreatorLookalikeResponse from .creator_autocomplete_params import CreatorAutocompleteParams as CreatorAutocompleteParams +from .creator_email_lookup_params import CreatorEmailLookupParams as CreatorEmailLookupParams from .creator_autocomplete_response import CreatorAutocompleteResponse as CreatorAutocompleteResponse +from .creator_email_lookup_response import CreatorEmailLookupResponse as CreatorEmailLookupResponse diff --git a/src/influship/types/creator_email_lookup_params.py b/src/influship/types/creator_email_lookup_params.py new file mode 100644 index 0000000..2e77b44 --- /dev/null +++ b/src/influship/types/creator_email_lookup_params.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Iterable +from typing_extensions import Literal, Required, TypeAlias, TypedDict + +__all__ = ["CreatorEmailLookupParams", "Creator", "CreatorCreatorEmailLookupByID", "CreatorCreatorEmailLookupByHandle"] + + +class CreatorEmailLookupParams(TypedDict, total=False): + creators: Required[Iterable[Creator]] + """Creator lookups to resolve. Response rows preserve this input order.""" + + +class CreatorCreatorEmailLookupByID(TypedDict, total=False): + """Creator email lookup input by creator ID""" + + creator_id: Required[str] + """Creator profile ID to look up directly""" + + +class CreatorCreatorEmailLookupByHandle(TypedDict, total=False): + """Creator email lookup input by social handle""" + + platform: Required[Literal["instagram"]] + """Social platform for handle-based lookup""" + + username: Required[str] + """Social username for handle-based lookup""" + + +Creator: TypeAlias = Union[CreatorCreatorEmailLookupByID, CreatorCreatorEmailLookupByHandle] diff --git a/src/influship/types/creator_email_lookup_response.py b/src/influship/types/creator_email_lookup_response.py new file mode 100644 index 0000000..823f219 --- /dev/null +++ b/src/influship/types/creator_email_lookup_response.py @@ -0,0 +1,104 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Union, Optional +from datetime import datetime +from typing_extensions import Literal, TypeAlias + +from .._models import BaseModel + +__all__ = [ + "CreatorEmailLookupResponse", + "Data", + "DataBilling", + "DataResult", + "DataResultEmail", + "DataResultInput", + "DataResultInputCreatorEmailLookupByIDOutput", + "DataResultInputCreatorEmailLookupByHandleOutput", +] + + +class DataBilling(BaseModel): + """Creator email lookup billing summary""" + + billable_results: int + """Unique resolved creators with at least one returned email""" + + credits_charged: float + """Preview of credits charged for this lookup""" + + +class DataResultEmail(BaseModel): + """API-visible creator email""" + + confidence: Optional[float] = None + """Nullable confidence score for the email""" + + email: str + """Email address as stored, preserving original casing""" + + first_seen_at: datetime + """When Influship first observed this email""" + + is_primary: bool + """Whether this is the primary email for the creator""" + + last_seen_at: datetime + """When Influship most recently observed this email""" + + status: Literal["unvalidated", "valid", "risky", "creator_verified"] + """API-visible email validation status""" + + validated_at: Optional[datetime] = None + """When the email was last validated, if known""" + + +class DataResultInputCreatorEmailLookupByIDOutput(BaseModel): + """Creator email lookup input by creator ID""" + + creator_id: str + """Creator profile ID to look up directly""" + + +class DataResultInputCreatorEmailLookupByHandleOutput(BaseModel): + """Creator email lookup input by social handle""" + + platform: Literal["instagram"] + """Social platform for handle-based lookup""" + + username: str + """Social username for handle-based lookup""" + + +DataResultInput: TypeAlias = Union[ + DataResultInputCreatorEmailLookupByIDOutput, DataResultInputCreatorEmailLookupByHandleOutput +] + + +class DataResult(BaseModel): + """Creator email lookup result""" + + creator_id: Optional[str] = None + """Resolved creator ID, or null when the input could not be resolved""" + + emails: List[DataResultEmail] + """API-visible emails for the resolved creator. Empty results are not billable.""" + + input: DataResultInput + """Creator email lookup input by creator ID or social handle""" + + resolved: bool + """Whether the lookup resolved to a creator profile""" + + +class Data(BaseModel): + billing: DataBilling + """Creator email lookup billing summary""" + + results: List[DataResult] + + +class CreatorEmailLookupResponse(BaseModel): + """Creator email lookup response""" + + data: Data diff --git a/src/influship/types/post_list_response.py b/src/influship/types/post_list_response.py index 48cd5e8..130bae5 100644 --- a/src/influship/types/post_list_response.py +++ b/src/influship/types/post_list_response.py @@ -6,7 +6,7 @@ from .._models import BaseModel -__all__ = ["PostListResponse", "Location", "Media", "Metrics"] +__all__ = ["PostListResponse", "Location", "Media", "MediaCarouselItem", "Metrics"] class Location(BaseModel): @@ -16,20 +16,40 @@ class Location(BaseModel): """Location name""" +class MediaCarouselItem(BaseModel): + index: int + """Zero-based position in the carousel.""" + + is_video: bool + """True if this item is a video.""" + + thumbnail_url: Optional[str] = None + """Thumbnail URL for this item. Cover frame for videos.""" + + class Media(BaseModel): """Post media information""" + carousel_items: Optional[List[MediaCarouselItem]] = None + """Per-item structure for carousel posts. + + Null for non-carousel posts. Per-item video_url is intentionally omitted (would + be stale). For fresh video URLs, call GET /v1/raw/instagram/post/{shortcode}. + """ + duration_seconds: Optional[float] = None - """Video duration in seconds""" + """Video duration in seconds. Null for non-video posts.""" thumbnail_url: Optional[str] = None - """Thumbnail URL""" + """Thumbnail URL. For videos, this is the cover frame.""" url: Optional[str] = None - """Media URL""" + """Cover/primary image URL for image and carousel posts. - video_url: Optional[str] = None - """Video URL (for video content)""" + Null for video posts — call GET /v1/raw/instagram/post/{shortcode} for a fresh, + downloadable video URL. Note: returned image URLs are Instagram CDN URLs and may + expire; a future change will migrate to persistent R2-hosted URLs. + """ class Metrics(BaseModel): diff --git a/src/influship/types/raw/__init__.py b/src/influship/types/raw/__init__.py index 2afb1d6..aa51c72 100644 --- a/src/influship/types/raw/__init__.py +++ b/src/influship/types/raw/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations -from .raw_scraper_error import RawScraperError as RawScraperError from .transcript_segment import TranscriptSegment as TranscriptSegment from .youtube_search_params import YoutubeSearchParams as YoutubeSearchParams from .youtube_search_response import YoutubeSearchResponse as YoutubeSearchResponse diff --git a/src/influship/types/raw/instagram_get_posts_response.py b/src/influship/types/raw/instagram_get_posts_response.py index 18e9d0d..31dc28c 100644 --- a/src/influship/types/raw/instagram_get_posts_response.py +++ b/src/influship/types/raw/instagram_get_posts_response.py @@ -5,7 +5,7 @@ from typing_extensions import Literal, TypeAlias from ..._models import BaseModel -from .raw_scraper_error import RawScraperError +from ..shared.raw_scraper_error import RawScraperError from .instagram_single_post_response import InstagramSinglePostResponse __all__ = [ diff --git a/src/influship/types/raw/instagram_get_transcripts_response.py b/src/influship/types/raw/instagram_get_transcripts_response.py index d82bb35..bfc7e9d 100644 --- a/src/influship/types/raw/instagram_get_transcripts_response.py +++ b/src/influship/types/raw/instagram_get_transcripts_response.py @@ -5,7 +5,7 @@ from typing_extensions import Literal, TypeAlias from ..._models import BaseModel -from .raw_scraper_error import RawScraperError +from ..shared.raw_scraper_error import RawScraperError from .instagram_transcript_response import InstagramTranscriptResponse __all__ = [ diff --git a/src/influship/types/raw/instagram_transcript_response.py b/src/influship/types/raw/instagram_transcript_response.py index 84646ac..1394fe7 100644 --- a/src/influship/types/raw/instagram_transcript_response.py +++ b/src/influship/types/raw/instagram_transcript_response.py @@ -154,8 +154,6 @@ class InstagramTranscriptResponse(BaseModel): language: str - post: Post - scraped_at: datetime shortcode: str @@ -165,3 +163,5 @@ class InstagramTranscriptResponse(BaseModel): word_count: float duration_seconds: Optional[float] = None + + post: Optional[Post] = None diff --git a/src/influship/types/shared/__init__.py b/src/influship/types/shared/__init__.py index 7e07f42..4d440b3 100644 --- a/src/influship/types/shared/__init__.py +++ b/src/influship/types/shared/__init__.py @@ -2,3 +2,4 @@ from .creator_basic import CreatorBasic as CreatorBasic from .profile_summary import ProfileSummary as ProfileSummary +from .raw_scraper_error import RawScraperError as RawScraperError diff --git a/src/influship/types/raw/raw_scraper_error.py b/src/influship/types/shared/raw_scraper_error.py similarity index 100% rename from src/influship/types/raw/raw_scraper_error.py rename to src/influship/types/shared/raw_scraper_error.py diff --git a/tests/api_resources/test_creator_emails.py b/tests/api_resources/test_creator_emails.py new file mode 100644 index 0000000..f1e6772 --- /dev/null +++ b/tests/api_resources/test_creator_emails.py @@ -0,0 +1,92 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from influship import Influship, AsyncInfluship +from tests.utils import assert_matches_type +from influship.types import CreatorEmailLookupResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestCreatorEmails: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_lookup(self, client: Influship) -> None: + creator_email = client.creator_emails.lookup( + creators=[{"creator_id": "123e4567-e89b-12d3-a456-426614174000"}], + ) + assert_matches_type(CreatorEmailLookupResponse, creator_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_lookup(self, client: Influship) -> None: + response = client.creator_emails.with_raw_response.lookup( + creators=[{"creator_id": "123e4567-e89b-12d3-a456-426614174000"}], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + creator_email = response.parse() + assert_matches_type(CreatorEmailLookupResponse, creator_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_lookup(self, client: Influship) -> None: + with client.creator_emails.with_streaming_response.lookup( + creators=[{"creator_id": "123e4567-e89b-12d3-a456-426614174000"}], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + creator_email = response.parse() + assert_matches_type(CreatorEmailLookupResponse, creator_email, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncCreatorEmails: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_lookup(self, async_client: AsyncInfluship) -> None: + creator_email = await async_client.creator_emails.lookup( + creators=[{"creator_id": "123e4567-e89b-12d3-a456-426614174000"}], + ) + assert_matches_type(CreatorEmailLookupResponse, creator_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_lookup(self, async_client: AsyncInfluship) -> None: + response = await async_client.creator_emails.with_raw_response.lookup( + creators=[{"creator_id": "123e4567-e89b-12d3-a456-426614174000"}], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + creator_email = await response.parse() + assert_matches_type(CreatorEmailLookupResponse, creator_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_lookup(self, async_client: AsyncInfluship) -> None: + async with async_client.creator_emails.with_streaming_response.lookup( + creators=[{"creator_id": "123e4567-e89b-12d3-a456-426614174000"}], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + creator_email = await response.parse() + assert_matches_type(CreatorEmailLookupResponse, creator_email, path=["response"]) + + assert cast(Any, response.is_closed) is True