mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-18 23:11:12 +02:00
feat(instagram): add platform input/output schemas
This commit is contained in:
parent
51c2dfdfe6
commit
842a0e666d
1 changed files with 187 additions and 0 deletions
187
surfsense_backend/app/proprietary/platforms/instagram/schemas.py
Normal file
187
surfsense_backend/app/proprietary/platforms/instagram/schemas.py
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
# ruff: noqa: N815
|
||||||
|
"""Input/output models for the Instagram scraper.
|
||||||
|
|
||||||
|
The models mirror the public Instagram scraper actor spec so the endpoint can be
|
||||||
|
a drop-in: the input accepts the full documented surface, and every output field
|
||||||
|
is emitted (``None``/``[]`` when the anonymous web endpoints cannot source it
|
||||||
|
yet) so the contract expands additively — the same rule the Google Search and
|
||||||
|
YouTube models follow.
|
||||||
|
|
||||||
|
**Anonymous only.** There is deliberately **no** authentication field on the
|
||||||
|
input (no username/password/token/cookie/``login*``) — the scraper holds only
|
||||||
|
Instagram's anonymous web-session cookies (``csrftoken``/``mid``) and can never
|
||||||
|
log in. Anything auth-shaped a caller sends lands in ``extra`` and is ignored.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
InstagramResultsType = Literal[
|
||||||
|
"posts", "details", "comments", "reels", "mentions", "stories"
|
||||||
|
]
|
||||||
|
InstagramSearchType = Literal["hashtag", "profile", "place", "user"]
|
||||||
|
InstagramDetailKind = Literal["profile", "hashtag", "place"]
|
||||||
|
|
||||||
|
|
||||||
|
class InstagramScrapeInput(BaseModel):
|
||||||
|
"""Instagram scraper input surface (anonymous, no auth fields).
|
||||||
|
|
||||||
|
Field names mirror the public actor spec verbatim. ``resultsLimit`` /
|
||||||
|
``searchLimit`` are collector policy applied by :func:`scrape_instagram`,
|
||||||
|
NOT ceilings baked into the streaming flows. Fields the acquisition layer
|
||||||
|
doesn't source yet are still accepted via ``extra="allow"`` for parity.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="allow")
|
||||||
|
resultsType: InstagramResultsType = "posts"
|
||||||
|
directUrls: list[str] = Field(default_factory=list)
|
||||||
|
resultsLimit: int | None = Field(default=None, ge=1)
|
||||||
|
onlyPostsNewerThan: str | None = None
|
||||||
|
search: str | None = None
|
||||||
|
searchType: InstagramSearchType = "hashtag"
|
||||||
|
searchLimit: int | None = Field(default=None, ge=1, le=250)
|
||||||
|
addParentData: bool = False
|
||||||
|
skipPinnedPosts: bool = False
|
||||||
|
isNewestComments: bool = False
|
||||||
|
includeNestedComments: bool = False
|
||||||
|
addProfileStatistics: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class _ItemBase(BaseModel):
|
||||||
|
"""Common error / provenance fields carried on every output item.
|
||||||
|
|
||||||
|
Errors surface as item-level fields (never exceptions) so a partial run
|
||||||
|
still returns the items it could source, mirroring the actor's shape.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="allow")
|
||||||
|
inputUrl: str | None = None
|
||||||
|
error: str | None = None
|
||||||
|
errorDescription: str | None = None
|
||||||
|
requestErrorMessages: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
def to_output(self) -> dict[str, Any]:
|
||||||
|
"""Serialize to the flat output dict shape (keeps extras)."""
|
||||||
|
return self.model_dump(exclude_none=False)
|
||||||
|
|
||||||
|
|
||||||
|
class InstagramMediaItem(_ItemBase):
|
||||||
|
"""A post / reel / mention. One flat schema per the actor FAQ."""
|
||||||
|
|
||||||
|
id: str | None = None
|
||||||
|
type: Literal["Image", "Video", "Sidecar"] | None = None
|
||||||
|
shortCode: str | None = None
|
||||||
|
caption: str | None = None
|
||||||
|
hashtags: list[str] = Field(default_factory=list)
|
||||||
|
mentions: list[str] = Field(default_factory=list)
|
||||||
|
url: str | None = None
|
||||||
|
commentsCount: int | None = None
|
||||||
|
firstComment: str | None = None
|
||||||
|
latestComments: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
dimensionsHeight: int | None = None
|
||||||
|
dimensionsWidth: int | None = None
|
||||||
|
displayUrl: str | None = None
|
||||||
|
images: list[str] = Field(default_factory=list)
|
||||||
|
videoUrl: str | None = None
|
||||||
|
alt: str | None = None
|
||||||
|
likesCount: int | None = None
|
||||||
|
videoViewCount: int | None = None
|
||||||
|
videoPlayCount: int | None = None
|
||||||
|
reshareCount: int | None = None
|
||||||
|
timestamp: str | None = None
|
||||||
|
childPosts: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
ownerUsername: str | None = None
|
||||||
|
ownerId: str | None = None
|
||||||
|
ownerFullName: str | None = None
|
||||||
|
isPinned: bool | None = None
|
||||||
|
productType: str | None = None
|
||||||
|
videoDuration: float | None = None
|
||||||
|
paidPartnership: bool | None = None
|
||||||
|
taggedUsers: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
musicInfo: dict[str, Any] | None = None
|
||||||
|
coauthorProducers: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
locationName: str | None = None
|
||||||
|
locationId: str | None = None
|
||||||
|
isCommentsDisabled: bool | None = None
|
||||||
|
dataSource: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class InstagramComment(_ItemBase):
|
||||||
|
"""A comment on a post / reel."""
|
||||||
|
|
||||||
|
id: str | None = None
|
||||||
|
postUrl: str | None = None
|
||||||
|
commentUrl: str | None = None
|
||||||
|
text: str | None = None
|
||||||
|
ownerUsername: str | None = None
|
||||||
|
ownerProfilePicUrl: str | None = None
|
||||||
|
timestamp: str | None = None
|
||||||
|
repliesCount: int | None = None
|
||||||
|
replies: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
likesCount: int | None = None
|
||||||
|
owner: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class InstagramProfile(_ItemBase):
|
||||||
|
"""A profile detail item (``detailKind = "profile"``)."""
|
||||||
|
|
||||||
|
detailKind: Literal["profile"] = "profile"
|
||||||
|
id: str | None = None
|
||||||
|
username: str | None = None
|
||||||
|
url: str | None = None
|
||||||
|
fullName: str | None = None
|
||||||
|
biography: str | None = None
|
||||||
|
externalUrl: str | None = None
|
||||||
|
followersCount: int | None = None
|
||||||
|
followsCount: int | None = None
|
||||||
|
postsCount: int | None = None
|
||||||
|
highlightReelCount: int | None = None
|
||||||
|
igtvVideoCount: int | None = None
|
||||||
|
isBusinessAccount: bool | None = None
|
||||||
|
businessCategoryName: str | None = None
|
||||||
|
private: bool | None = None
|
||||||
|
verified: bool | None = None
|
||||||
|
profilePicUrl: str | None = None
|
||||||
|
profilePicUrlHD: str | None = None
|
||||||
|
relatedProfiles: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
latestPosts: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
statistics: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class InstagramHashtag(_ItemBase):
|
||||||
|
"""A hashtag detail item (``detailKind = "hashtag"``)."""
|
||||||
|
|
||||||
|
detailKind: Literal["hashtag"] = "hashtag"
|
||||||
|
id: str | None = None
|
||||||
|
name: str | None = None
|
||||||
|
url: str | None = None
|
||||||
|
postsCount: int | None = None
|
||||||
|
topPosts: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
posts: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
related: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
searchTerm: str | None = None
|
||||||
|
searchSource: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class InstagramPlace(_ItemBase):
|
||||||
|
"""A place detail item (``detailKind = "place"``)."""
|
||||||
|
|
||||||
|
detailKind: Literal["place"] = "place"
|
||||||
|
name: str | None = None
|
||||||
|
location_id: str | None = None
|
||||||
|
slug: str | None = None
|
||||||
|
lat: float | None = None
|
||||||
|
lng: float | None = None
|
||||||
|
location_address: str | None = None
|
||||||
|
location_city: str | None = None
|
||||||
|
location_zip: str | None = None
|
||||||
|
phone: str | None = None
|
||||||
|
website: str | None = None
|
||||||
|
category: str | None = None
|
||||||
|
media_count: int | None = None
|
||||||
|
posts: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
searchTerm: str | None = None
|
||||||
|
searchSource: str | None = None
|
||||||
Loading…
Add table
Add a link
Reference in a new issue