mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-08 22:22:17 +02:00
feat(native-connector): add reddit scraper schemas
This commit is contained in:
parent
0745f433ab
commit
dd43ab8505
1 changed files with 127 additions and 0 deletions
127
surfsense_backend/app/proprietary/scrapers/reddit/schemas.py
Normal file
127
surfsense_backend/app/proprietary/scrapers/reddit/schemas.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue