feat(native-connector): add reddit scraper schemas

This commit is contained in:
Anish Sarkar 2026-07-04 17:27:33 +05:30
parent 0745f433ab
commit dd43ab8505

View file

@ -0,0 +1,127 @@
# ruff: noqa: N815 - field names intentionally mirror the Apify camelCase spec
"""Apify-compatible input/output models for the Reddit scraper.
The models mirror the public Apify "Reddit Scraper" actor spec so the endpoint
can be a drop-in. The MVP populates a reliably-sourced subset; every other field
is accepted (input) or emitted as ``None``/``[]`` (output) so parity is additive.
**Anonymous only.** There is deliberately **no** authentication field on the
input (no username/password/token/``login*``) the scraper holds only Reddit's
anonymous ``loid`` session cookie 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
RedditSort = Literal["relevance", "hot", "top", "new", "rising", "comments"]
RedditTime = Literal["all", "hour", "day", "week", "month", "year"]
RedditDataType = Literal["post", "comment", "community", "user"]
class StartUrl(BaseModel):
"""A single direct URL entry (Apify passes ``{"url": ...}`` objects)."""
model_config = ConfigDict(extra="allow")
url: str
class RedditScrapeInput(BaseModel):
"""Full Apify "Reddit Scraper" input surface (anonymous, no auth fields).
Caps (``maxItems``/``maxPostCount``/...) are collector policy applied by
:func:`scrape_reddit`, NOT ceilings baked into the streaming flows. Fields
the MVP doesn't act on are still accepted via ``extra="allow"`` for parity.
"""
model_config = ConfigDict(extra="allow")
# Discovery
startUrls: list[StartUrl] = Field(default_factory=list)
searches: list[str] = Field(default_factory=list)
searchCommunityName: str | None = None
# Sort / filter
sort: RedditSort = "new"
time: RedditTime | None = None
includeNSFW: bool = True
# Skips
skipComments: bool = False
skipUserPosts: bool = False
skipCommunity: bool = False
# Caps (collector policy; enforced by scrape_reddit, not the flows)
maxItems: int = Field(default=10, ge=0)
maxPostCount: int = Field(default=10, ge=0)
maxComments: int = Field(default=10, ge=0)
maxCommunitiesCount: int = Field(default=2, ge=0)
maxUserCount: int = Field(default=2, ge=0)
# Incremental scraping (ISO dates)
postDateLimit: str | None = None
commentDateLimit: str | None = None
class RedditItem(BaseModel):
"""Single flat output item, keyed by ``dataType``.
One model for post/comment/community/user (union of fields, unsourced
default ``None``/``[]``), matching the Apify actor's single-dataset shape.
``extra="allow"`` keeps the contract open so added fields never break
consumers.
"""
model_config = ConfigDict(extra="allow")
dataType: RedditDataType | None = None
# Identity / provenance
id: str | None = None
parsedId: str | None = None
url: str | None = None
username: str | None = None
userId: str | None = None
# Content
title: str | None = None
body: str | None = None
html: str | None = None
link: str | None = None
externalUrl: str | None = None
# Community
communityName: str | None = None
parsedCommunityName: str | None = None
numberOfMembers: int | None = None
# Engagement
numberOfComments: int | None = None
numberOfReplies: int | None = None
upVotes: int | None = None
upVoteRatio: float | None = None
# Flags / media
over18: bool | None = None
isVideo: bool | None = None
flair: str | None = None
authorFlair: str | None = None
thumbnailUrl: str | None = None
imageUrls: list[str] = Field(default_factory=list)
videoUrls: list[str] = Field(default_factory=list)
# Threading
postId: str | None = None
parentId: str | None = None
# Timestamps
createdAt: str | None = None
scrapedAt: str | None = None
def to_output(self) -> dict[str, Any]:
"""Serialize to the flat dict shape Apify emits (keeps extras)."""
return self.model_dump(exclude_none=False)