mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-16 23:01:06 +02:00
feat(instagram): add details capability verb
This commit is contained in:
parent
38a26b1ccf
commit
7ea474be93
4 changed files with 162 additions and 0 deletions
|
|
@ -0,0 +1,3 @@
|
|||
"""``instagram.details`` verb: profile/hashtag/place URLs or search → metadata."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
"""``instagram.details`` capability registration (billed per item; see config
|
||||
``INSTAGRAM_SCRAPE_MICROS_PER_ITEM``)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.capabilities.core import BillingUnit, Capability, register_capability
|
||||
from app.capabilities.instagram.details.executor import build_details_executor
|
||||
from app.capabilities.instagram.details.schemas import DetailsInput, DetailsOutput
|
||||
|
||||
INSTAGRAM_DETAILS = Capability(
|
||||
name="instagram.details",
|
||||
description=(
|
||||
"Fetch Instagram profile, hashtag, or place metadata by URL or discovery "
|
||||
"search. Each item carries a detailKind discriminator."
|
||||
),
|
||||
input_schema=DetailsInput,
|
||||
output_schema=DetailsOutput,
|
||||
executor=build_details_executor(),
|
||||
billing_unit=BillingUnit.INSTAGRAM_ITEM,
|
||||
docs_url="/docs/connectors/native/instagram",
|
||||
)
|
||||
|
||||
register_capability(INSTAGRAM_DETAILS)
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
"""``instagram.details`` executor: verb input → scraper → detail items."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from app.capabilities.core import Executor
|
||||
from app.capabilities.core.progress import emit_progress
|
||||
from app.capabilities.instagram.details.schemas import DetailsInput, DetailsOutput
|
||||
from app.exceptions import ForbiddenError
|
||||
from app.proprietary.platforms.instagram import (
|
||||
InstagramAccessBlockedError,
|
||||
InstagramScrapeInput,
|
||||
scrape_instagram,
|
||||
)
|
||||
|
||||
ScrapeFn = Callable[..., Awaitable[list[dict]]]
|
||||
|
||||
|
||||
def build_details_executor(scrape_fn: ScrapeFn | None = None) -> Executor:
|
||||
"""Bind the executor to a scraper fn (defaults to the proprietary actor)."""
|
||||
scrape_fn = scrape_fn or scrape_instagram
|
||||
|
||||
async def execute(payload: DetailsInput) -> DetailsOutput:
|
||||
actor_input = InstagramScrapeInput(
|
||||
resultsType="details",
|
||||
directUrls=payload.urls,
|
||||
search=",".join(payload.search_queries),
|
||||
searchType=payload.search_type,
|
||||
searchLimit=payload.search_limit,
|
||||
)
|
||||
emit_progress(
|
||||
"starting",
|
||||
"Resolving Instagram detail targets",
|
||||
total=payload.max_items,
|
||||
unit="item",
|
||||
)
|
||||
try:
|
||||
items = await scrape_fn(actor_input, limit=payload.max_items)
|
||||
except InstagramAccessBlockedError as exc:
|
||||
raise ForbiddenError(
|
||||
f"Instagram refused anonymous access: {exc}",
|
||||
code="INSTAGRAM_ACCESS_BLOCKED",
|
||||
) from exc
|
||||
emit_progress(
|
||||
"done", f"Scraped {len(items)} item(s)", current=len(items), unit="item"
|
||||
)
|
||||
return DetailsOutput(items=items)
|
||||
|
||||
return execute
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
"""``instagram.details`` I/O contracts.
|
||||
|
||||
A lean surface over ``InstagramScrapeInput`` (``resultsType="details"``). Each
|
||||
output item is a profile / hashtag / place, discriminated by the synthesized
|
||||
``detailKind`` field (a SurfSense addition; every other field mirrors the actor).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
from app.capabilities.instagram.scrape.schemas import (
|
||||
MAX_INSTAGRAM_ITEMS,
|
||||
MAX_INSTAGRAM_SOURCES,
|
||||
)
|
||||
from app.proprietary.platforms.instagram import (
|
||||
InstagramHashtag,
|
||||
InstagramPlace,
|
||||
InstagramProfile,
|
||||
)
|
||||
|
||||
InstagramDetailItem = Annotated[
|
||||
InstagramProfile | InstagramHashtag | InstagramPlace,
|
||||
Field(discriminator="detailKind"),
|
||||
]
|
||||
|
||||
|
||||
class DetailsInput(BaseModel):
|
||||
urls: list[str] = Field(
|
||||
default_factory=list,
|
||||
max_length=MAX_INSTAGRAM_SOURCES,
|
||||
description=(
|
||||
"Profile / hashtag / place URLs (or bare profile IDs). The URL type "
|
||||
"determines the detail kind. Provide these OR search_queries."
|
||||
),
|
||||
)
|
||||
search_queries: list[str] = Field(
|
||||
default_factory=list,
|
||||
max_length=MAX_INSTAGRAM_SOURCES,
|
||||
description="Discovery keywords. Provide these OR urls (never both).",
|
||||
)
|
||||
search_type: Literal["hashtag", "profile", "place"] = Field(
|
||||
default="hashtag",
|
||||
description="What to discover from search_queries (no 'user' — use instagram.scrape).",
|
||||
)
|
||||
search_limit: int = Field(
|
||||
default=10,
|
||||
ge=1,
|
||||
le=MAX_INSTAGRAM_ITEMS,
|
||||
description="Max discovered entities per query.",
|
||||
)
|
||||
max_items: int = Field(
|
||||
default=10,
|
||||
ge=1,
|
||||
le=MAX_INSTAGRAM_ITEMS,
|
||||
description="Max total detail items to return.",
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _exactly_one_source(self) -> DetailsInput:
|
||||
if not self.urls and not self.search_queries:
|
||||
raise ValueError(
|
||||
"Provide at least one of 'urls' or 'search_queries'."
|
||||
)
|
||||
if self.urls and self.search_queries:
|
||||
raise ValueError(
|
||||
"Provide 'urls' OR 'search_queries', not both (they cannot be combined)."
|
||||
)
|
||||
return self
|
||||
|
||||
@property
|
||||
def estimated_units(self) -> int:
|
||||
return self.max_items
|
||||
|
||||
|
||||
class DetailsOutput(BaseModel):
|
||||
items: list[InstagramDetailItem] = Field(
|
||||
default_factory=list,
|
||||
description="One item per profile/hashtag/place, keyed by detailKind.",
|
||||
)
|
||||
|
||||
@property
|
||||
def billable_units(self) -> int:
|
||||
return len(self.items)
|
||||
Loading…
Add table
Add a link
Reference in a new issue