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