Compare commits

..

9 Commits

Author SHA1 Message Date
Zamil Majdy
47a996e75d lint 2024-12-09 15:17:35 -06:00
Zamil Majdy
2ff033aeb2 Address comments 2024-12-09 15:13:34 -06:00
abhi
95b1beecaf allowing default empty string to pass the test 2024-12-08 11:01:12 +05:30
abhi1992002
cb5452f7ae fix(graph): formatting 2024-12-03 11:32:14 +05:30
abhi1992002
02a60966d4 feat(validation): Enhance input validation for dependencies in GraphModel
- Updated GraphModel to validate field dependencies and required fields in input data.
2024-12-03 11:25:28 +05:30
abhi1992002
f28112f5e2 refactor(validation): Remove InputValidationBlock class
- Deleted the InputValidationBlock class from checking_input_validation.py, which was previously used for input validation with required and dependent fields.
2024-12-02 22:39:47 +05:30
abhi1992002
20f018b2e5 feat(validation): Enhance input validation with 'depends_on' support
- Updated InputValidationBlock to include default values for required fields.
- Implemented validation logic in manager.py to ensure dependent fields are validated based on their dependencies.
2024-12-02 22:36:11 +05:30
Abhimanyu Yadav
6f144ef77a Update useAgentGraph.ts 2024-12-02 11:15:28 +05:30
abhi1992002
bd1fa8f6c2 feat(validation): Enhance schema validation with 'depends_on' support
- Added 'depends_on' parameter to SchemaField in model.py to specify field dependencies.
- Updated useAgentGraph hook to validate input fields based on their dependencies, ensuring required fields are set when dependent fields are filled.
- Modified BlockIOSubSchemaMeta to include 'depends_on' as an optional property.
2024-12-02 11:02:00 +05:30
438 changed files with 14539 additions and 44927 deletions

View File

@@ -1,61 +1,40 @@
# Ignore everything by default, selectively add things to context
*
classic/run
# Platform - Libs
!autogpt_platform/autogpt_libs/autogpt_libs/
!autogpt_platform/autogpt_libs/pyproject.toml
!autogpt_platform/autogpt_libs/poetry.lock
!autogpt_platform/autogpt_libs/README.md
# Platform - Backend
!autogpt_platform/backend/backend/
!autogpt_platform/backend/migrations/
!autogpt_platform/backend/schema.prisma
!autogpt_platform/backend/pyproject.toml
!autogpt_platform/backend/poetry.lock
!autogpt_platform/backend/README.md
# Platform - Market
!autogpt_platform/market/market/
!autogpt_platform/market/scripts.py
!autogpt_platform/market/schema.prisma
!autogpt_platform/market/pyproject.toml
!autogpt_platform/market/poetry.lock
!autogpt_platform/market/README.md
# Platform - Frontend
!autogpt_platform/frontend/src/
!autogpt_platform/frontend/public/
!autogpt_platform/frontend/package.json
!autogpt_platform/frontend/yarn.lock
!autogpt_platform/frontend/tsconfig.json
!autogpt_platform/frontend/README.md
## config
!autogpt_platform/frontend/*.config.*
!autogpt_platform/frontend/.env.*
# Classic - AutoGPT
# AutoGPT
!classic/original_autogpt/autogpt/
!classic/original_autogpt/pyproject.toml
!classic/original_autogpt/poetry.lock
!classic/original_autogpt/README.md
!classic/original_autogpt/tests/
# Classic - Benchmark
# Benchmark
!classic/benchmark/agbenchmark/
!classic/benchmark/pyproject.toml
!classic/benchmark/poetry.lock
!classic/benchmark/README.md
# Classic - Forge
# Forge
!classic/forge/
!classic/forge/pyproject.toml
!classic/forge/poetry.lock
!classic/forge/README.md
# Classic - Frontend
# Frontend
!classic/frontend/build/web/
# Platform
!autogpt_platform/
# Explicitly re-ignore some folders
.*
**/__pycache__
autogpt_platform/frontend/.next/
autogpt_platform/frontend/node_modules
autogpt_platform/frontend/.env.example
autogpt_platform/frontend/.env.local
autogpt_platform/backend/.env
autogpt_platform/backend/.venv/
autogpt_platform/market/.env

View File

@@ -7,9 +7,6 @@ updates:
interval: "weekly"
open-pull-requests-limit: 10
target-branch: "dev"
commit-message:
prefix: "chore(libs/deps)"
prefix-development: "chore(libs/deps-dev)"
groups:
production-dependencies:
dependency-type: "production"
@@ -29,9 +26,6 @@ updates:
interval: "weekly"
open-pull-requests-limit: 10
target-branch: "dev"
commit-message:
prefix: "chore(backend/deps)"
prefix-development: "chore(backend/deps-dev)"
groups:
production-dependencies:
dependency-type: "production"
@@ -44,6 +38,7 @@ updates:
- "minor"
- "patch"
# frontend (Next.js project)
- package-ecosystem: "npm"
directory: "autogpt_platform/frontend"
@@ -51,9 +46,6 @@ updates:
interval: "weekly"
open-pull-requests-limit: 10
target-branch: "dev"
commit-message:
prefix: "chore(frontend/deps)"
prefix-development: "chore(frontend/deps-dev)"
groups:
production-dependencies:
dependency-type: "production"
@@ -66,6 +58,7 @@ updates:
- "minor"
- "patch"
# infra (Terraform)
- package-ecosystem: "terraform"
directory: "autogpt_platform/infra"
@@ -73,10 +66,26 @@ updates:
interval: "weekly"
open-pull-requests-limit: 5
target-branch: "dev"
commit-message:
prefix: "chore(infra/deps)"
prefix-development: "chore(infra/deps-dev)"
groups:
production-dependencies:
dependency-type: "production"
update-types:
- "minor"
- "patch"
development-dependencies:
dependency-type: "development"
update-types:
- "minor"
- "patch"
# market (Poetry project)
- package-ecosystem: "pip"
directory: "autogpt_platform/market"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
target-branch: "dev"
groups:
production-dependencies:
dependency-type: "production"
@@ -137,9 +146,6 @@ updates:
interval: "weekly"
open-pull-requests-limit: 1
target-branch: "dev"
commit-message:
prefix: "chore(platform/deps)"
prefix-development: "chore(platform/deps-dev)"
groups:
production-dependencies:
dependency-type: "production"
@@ -160,8 +166,6 @@ updates:
interval: "weekly"
open-pull-requests-limit: 1
target-branch: "dev"
commit-message:
prefix: "chore(docs/deps)"
groups:
production-dependencies:
dependency-type: "production"

View File

@@ -35,6 +35,12 @@ jobs:
env:
DATABASE_URL: ${{ secrets.BACKEND_DATABASE_URL }}
- name: Run Market Migrations
working-directory: ./autogpt_platform/market
run: |
python -m prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.MARKET_DATABASE_URL }}
trigger:
needs: migrate

View File

@@ -37,6 +37,13 @@ jobs:
env:
DATABASE_URL: ${{ secrets.BACKEND_DATABASE_URL }}
- name: Run Market Migrations
working-directory: ./autogpt_platform/market
run: |
python -m prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.MARKET_DATABASE_URL }}
trigger:
needs: migrate
runs-on: ubuntu-latest

View File

@@ -6,13 +6,11 @@ on:
paths:
- ".github/workflows/platform-backend-ci.yml"
- "autogpt_platform/backend/**"
- "autogpt_platform/autogpt_libs/**"
pull_request:
branches: [master, dev, release-*]
paths:
- ".github/workflows/platform-backend-ci.yml"
- "autogpt_platform/backend/**"
- "autogpt_platform/autogpt_libs/**"
merge_group:
concurrency:
@@ -79,17 +77,6 @@ jobs:
echo "$HOME/.local/bin" >> $GITHUB_PATH
fi
- name: Check poetry.lock
run: |
poetry lock
if ! git diff --quiet poetry.lock; then
echo "Error: poetry.lock not up to date."
echo
git diff poetry.lock
exit 1
fi
- name: Install Python dependencies
run: poetry install

View File

@@ -23,7 +23,6 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
@@ -39,12 +38,24 @@ jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
browser: [chromium, webkit]
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: false
dotnet: false
haskell: false
large-packages: true
docker-images: true
swap-storage: true
- name: Checkout repository
uses: actions/checkout@v4
with:
@@ -55,12 +66,6 @@ jobs:
with:
node-version: "21"
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
large-packages: false # slow
docker-images: false # limited benefit
- name: Copy default supabase .env
run: |
cp ../supabase/docker/.env.example ../.env
@@ -81,21 +86,16 @@ jobs:
run: |
cp .env.example .env
- name: Install Browser '${{ matrix.browser }}'
run: yarn playwright install --with-deps ${{ matrix.browser }}
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Run tests
run: |
yarn test --project=${{ matrix.browser }}
- name: Print Docker Compose logs in debug mode
if: runner.debug
run: |
docker compose -f ../docker-compose.yml logs
yarn test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report-${{ matrix.browser }}
name: playwright-report
path: playwright-report/
retention-days: 30

126
.github/workflows/platform-market-ci.yml vendored Normal file
View File

@@ -0,0 +1,126 @@
name: AutoGPT Platform - Backend CI
on:
push:
branches: [master, dev, ci-test*]
paths:
- ".github/workflows/platform-market-ci.yml"
- "autogpt_platform/market/**"
pull_request:
branches: [master, dev, release-*]
paths:
- ".github/workflows/platform-market-ci.yml"
- "autogpt_platform/market/**"
merge_group:
concurrency:
group: ${{ format('backend-ci-{0}', github.head_ref && format('{0}-{1}', github.event_name, github.event.pull_request.number) || github.sha) }}
cancel-in-progress: ${{ startsWith(github.event_name, 'pull_request') }}
defaults:
run:
shell: bash
working-directory: autogpt_platform/market
jobs:
test:
permissions:
contents: read
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
python-version: ["3.10"]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Setup Supabase
uses: supabase/setup-cli@v1
with:
version: latest
- id: get_date
name: Get date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Set up Python dependency cache
uses: actions/cache@v4
with:
path: ~/.cache/pypoetry
key: poetry-${{ runner.os }}-${{ hashFiles('autogpt_platform/market/poetry.lock') }}
- name: Install Poetry (Unix)
run: |
curl -sSL https://install.python-poetry.org | python3 -
if [ "${{ runner.os }}" = "macOS" ]; then
PATH="$HOME/.local/bin:$PATH"
echo "$HOME/.local/bin" >> $GITHUB_PATH
fi
- name: Install Python dependencies
run: poetry install
- name: Generate Prisma Client
run: poetry run prisma generate
- id: supabase
name: Start Supabase
working-directory: .
run: |
supabase init
supabase start --exclude postgres-meta,realtime,storage-api,imgproxy,inbucket,studio,edge-runtime,logflare,vector,supavisor
supabase status -o env | sed 's/="/=/; s/"$//' >> $GITHUB_OUTPUT
# outputs:
# DB_URL, API_URL, GRAPHQL_URL, ANON_KEY, SERVICE_ROLE_KEY, JWT_SECRET
- name: Run Database Migrations
run: poetry run prisma migrate dev --name updates
env:
DATABASE_URL: ${{ steps.supabase.outputs.DB_URL }}
- id: lint
name: Run Linter
run: poetry run lint
# Tests comment out because they do not work with prisma mock, nor have they been updated since they were created
# - name: Run pytest with coverage
# run: |
# if [[ "${{ runner.debug }}" == "1" ]]; then
# poetry run pytest -s -vv -o log_cli=true -o log_cli_level=DEBUG test
# else
# poetry run pytest -s -vv test
# fi
# if: success() || (failure() && steps.lint.outcome == 'failure')
# env:
# LOG_LEVEL: ${{ runner.debug && 'DEBUG' || 'INFO' }}
# DATABASE_URL: ${{ steps.supabase.outputs.DB_URL }}
# SUPABASE_URL: ${{ steps.supabase.outputs.API_URL }}
# SUPABASE_SERVICE_ROLE_KEY: ${{ steps.supabase.outputs.SERVICE_ROLE_KEY }}
# SUPABASE_JWT_SECRET: ${{ steps.supabase.outputs.JWT_SECRET }}
# REDIS_HOST: 'localhost'
# REDIS_PORT: '6379'
# REDIS_PASSWORD: 'testpassword'
env:
CI: true
PLAIN_OUTPUT: True
RUN_ENV: local
PORT: 8080
# - name: Upload coverage reports to Codecov
# uses: codecov/codecov-action@v4
# with:
# token: ${{ secrets.CODECOV_TOKEN }}
# flags: backend,${{ runner.os }}

3
.gitignore vendored
View File

@@ -173,6 +173,3 @@ LICENSE.rtf
autogpt_platform/backend/settings.py
/.auth
/autogpt_platform/frontend/.auth
*.ign.*
.test-contents

View File

@@ -98,11 +98,6 @@ repos:
files: ^autogpt_platform/autogpt_libs/
args: [--fix]
- id: ruff-format
name: Format (Ruff) - AutoGPT Platform - Libs
alias: ruff-lint-platform-libs
files: ^autogpt_platform/autogpt_libs/
- repo: local
# isort needs the context of which packages are installed to function, so we
# can't use a vendored isort pre-commit hook (which runs in its own isolated venv).
@@ -110,7 +105,7 @@ repos:
- id: isort
name: Lint (isort) - AutoGPT Platform - Backend
alias: isort-platform-backend
entry: poetry -P autogpt_platform/backend run isort -p backend
entry: poetry -C autogpt_platform/backend run isort -p backend
files: ^autogpt_platform/backend/
types: [file, python]
language: system
@@ -118,7 +113,7 @@ repos:
- id: isort
name: Lint (isort) - Classic - AutoGPT
alias: isort-classic-autogpt
entry: poetry -P classic/original_autogpt run isort -p autogpt
entry: poetry -C classic/original_autogpt run isort -p autogpt
files: ^classic/original_autogpt/
types: [file, python]
language: system
@@ -126,7 +121,7 @@ repos:
- id: isort
name: Lint (isort) - Classic - Forge
alias: isort-classic-forge
entry: poetry -P classic/forge run isort -p forge
entry: poetry -C classic/forge run isort -p forge
files: ^classic/forge/
types: [file, python]
language: system
@@ -134,7 +129,7 @@ repos:
- id: isort
name: Lint (isort) - Classic - Benchmark
alias: isort-classic-benchmark
entry: poetry -P classic/benchmark run isort -p agbenchmark
entry: poetry -C classic/benchmark run isort -p agbenchmark
files: ^classic/benchmark/
types: [file, python]
language: system
@@ -145,7 +140,7 @@ repos:
# everything in .gitignore, so it works fine without any config or arguments.
hooks:
- id: black
name: Format (Black)
name: Lint (Black)
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
@@ -178,6 +173,7 @@ repos:
name: Typecheck - AutoGPT Platform - Backend
alias: pyright-platform-backend
entry: poetry -C autogpt_platform/backend run pyright
args: [-p, autogpt_platform/backend, autogpt_platform/backend]
# include forge source (since it's a path dependency) but exclude *_test.py files:
files: ^autogpt_platform/(backend/((backend|test)/|(\w+\.py|poetry\.lock)$)|autogpt_libs/(autogpt_libs/.*(?<!_test)\.py|poetry\.lock)$)
types: [file]
@@ -188,6 +184,7 @@ repos:
name: Typecheck - AutoGPT Platform - Libs
alias: pyright-platform-libs
entry: poetry -C autogpt_platform/autogpt_libs run pyright
args: [-p, autogpt_platform/autogpt_libs, autogpt_platform/autogpt_libs]
files: ^autogpt_platform/autogpt_libs/(autogpt_libs/|poetry\.lock$)
types: [file]
language: system
@@ -197,6 +194,7 @@ repos:
name: Typecheck - Classic - AutoGPT
alias: pyright-classic-autogpt
entry: poetry -C classic/original_autogpt run pyright
args: [-p, classic/original_autogpt, classic/original_autogpt]
# include forge source (since it's a path dependency) but exclude *_test.py files:
files: ^(classic/original_autogpt/((autogpt|scripts|tests)/|poetry\.lock$)|classic/forge/(forge/.*(?<!_test)\.py|poetry\.lock)$)
types: [file]
@@ -207,6 +205,7 @@ repos:
name: Typecheck - Classic - Forge
alias: pyright-classic-forge
entry: poetry -C classic/forge run pyright
args: [-p, classic/forge, classic/forge]
files: ^classic/forge/(forge/|poetry\.lock$)
types: [file]
language: system
@@ -216,6 +215,7 @@ repos:
name: Typecheck - Classic - Benchmark
alias: pyright-classic-benchmark
entry: poetry -C classic/benchmark run pyright
args: [-p, classic/benchmark, classic/benchmark]
files: ^classic/benchmark/(agbenchmark/|tests/|poetry\.lock$)
types: [file]
language: system

View File

@@ -8,7 +8,7 @@ We take the security of our project seriously. If you believe you have found a s
Instead, please report them via:
- [GitHub Security Advisory](https://github.com/Significant-Gravitas/AutoGPT/security/advisories/new)
<!--- [Huntr.dev](https://huntr.com/repos/significant-gravitas/autogpt) - where you may be eligible for a bounty-->
- [Huntr.dev](https://huntr.com/repos/significant-gravitas/autogpt) - where you may be eligible for a bounty
### Reporting Process
1. **Submit Report**: Use one of the above channels to submit your report

View File

@@ -35,12 +35,3 @@ def verify_user(payload: dict | None, admin_only: bool) -> User:
raise fastapi.HTTPException(status_code=403, detail="Admin access required")
return User.from_payload(payload)
def get_user_id(payload: dict = fastapi.Depends(auth_middleware)) -> str:
user_id = payload.get("sub")
if not user_id:
raise fastapi.HTTPException(
status_code=401, detail="User ID not found in token"
)
return user_id

View File

@@ -72,7 +72,7 @@ def feature_flag(
"""
def decorator(
func: Callable[P, Union[T, Awaitable[T]]],
func: Callable[P, Union[T, Awaitable[T]]]
) -> Callable[P, Union[T, Awaitable[T]]]:
@wraps(func)
async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:

View File

@@ -1,7 +1,6 @@
import pytest
from ldclient import LDClient
from autogpt_libs.feature_flag.client import feature_flag, mock_flag_variation
from ldclient import LDClient
@pytest.fixture

View File

@@ -6,7 +6,7 @@ class Settings(BaseSettings):
launch_darkly_sdk_key: str = Field(
default="",
description="The Launch Darkly SDK key",
validation_alias="LAUNCH_DARKLY_SDK_KEY",
validation_alias="LAUNCH_DARKLY_SDK_KEY"
)
model_config = SettingsConfigDict(case_sensitive=True, extra="ignore")

View File

@@ -23,6 +23,7 @@ DEBUG_LOG_FORMAT = (
class LoggingConfig(BaseSettings):
level: str = Field(
default="INFO",
description="Logging level",

View File

@@ -24,10 +24,10 @@ from .utils import remove_color_codes
),
("", ""),
("hello", "hello"),
("hello\x1b[31m world", "hello world"),
("\x1b[36mHello,\x1b[32m World!", "Hello, World!"),
("hello\x1B[31m world", "hello world"),
("\x1B[36mHello,\x1B[32m World!", "Hello, World!"),
(
"\x1b[1m\x1b[31mError:\x1b[0m\x1b[31m file not found",
"\x1B[1m\x1B[31mError:\x1B[0m\x1B[31m file not found",
"Error: file not found",
),
],

View File

@@ -1,31 +0,0 @@
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class RateLimitSettings(BaseSettings):
redis_host: str = Field(
default="redis://localhost:6379",
description="Redis host",
validation_alias="REDIS_HOST",
)
redis_port: str = Field(
default="6379", description="Redis port", validation_alias="REDIS_PORT"
)
redis_password: str = Field(
default="password",
description="Redis password",
validation_alias="REDIS_PASSWORD",
)
requests_per_minute: int = Field(
default=60,
description="Maximum number of requests allowed per minute per API key",
validation_alias="RATE_LIMIT_REQUESTS_PER_MINUTE",
)
model_config = SettingsConfigDict(case_sensitive=True, extra="ignore")
RATE_LIMIT_SETTINGS = RateLimitSettings()

View File

@@ -1,51 +0,0 @@
import time
from typing import Tuple
from redis import Redis
from .config import RATE_LIMIT_SETTINGS
class RateLimiter:
def __init__(
self,
redis_host: str = RATE_LIMIT_SETTINGS.redis_host,
redis_port: str = RATE_LIMIT_SETTINGS.redis_port,
redis_password: str = RATE_LIMIT_SETTINGS.redis_password,
requests_per_minute: int = RATE_LIMIT_SETTINGS.requests_per_minute,
):
self.redis = Redis(
host=redis_host,
port=int(redis_port),
password=redis_password,
decode_responses=True,
)
self.window = 60
self.max_requests = requests_per_minute
async def check_rate_limit(self, api_key_id: str) -> Tuple[bool, int, int]:
"""
Check if request is within rate limits.
Args:
api_key_id: The API key identifier to check
Returns:
Tuple of (is_allowed, remaining_requests, reset_time)
"""
now = time.time()
window_start = now - self.window
key = f"ratelimit:{api_key_id}:1min"
pipe = self.redis.pipeline()
pipe.zremrangebyscore(key, 0, window_start)
pipe.zadd(key, {str(now): now})
pipe.zcount(key, window_start, now)
pipe.expire(key, self.window)
_, _, request_count, _ = pipe.execute()
remaining = max(0, self.max_requests - request_count)
reset_time = int(now + self.window)
return request_count <= self.max_requests, remaining, reset_time

View File

@@ -1,32 +0,0 @@
from fastapi import HTTPException, Request
from starlette.middleware.base import RequestResponseEndpoint
from .limiter import RateLimiter
async def rate_limit_middleware(request: Request, call_next: RequestResponseEndpoint):
"""FastAPI middleware for rate limiting API requests."""
limiter = RateLimiter()
if not request.url.path.startswith("/api"):
return await call_next(request)
api_key = request.headers.get("Authorization")
if not api_key:
return await call_next(request)
api_key = api_key.replace("Bearer ", "")
is_allowed, remaining, reset_time = await limiter.check_rate_limit(api_key)
if not is_allowed:
raise HTTPException(
status_code=429, detail="Rate limit exceeded. Please try again later."
)
response = await call_next(request)
response.headers["X-RateLimit-Limit"] = str(limiter.max_requests)
response.headers["X-RateLimit-Remaining"] = str(remaining)
response.headers["X-RateLimit-Reset"] = str(reset_time)
return response

View File

@@ -1,76 +0,0 @@
from typing import Annotated, Any, Literal, Optional, TypedDict
from uuid import uuid4
from pydantic import BaseModel, Field, SecretStr, field_serializer
class _BaseCredentials(BaseModel):
id: str = Field(default_factory=lambda: str(uuid4()))
provider: str
title: Optional[str]
@field_serializer("*")
def dump_secret_strings(value: Any, _info):
if isinstance(value, SecretStr):
return value.get_secret_value()
return value
class OAuth2Credentials(_BaseCredentials):
type: Literal["oauth2"] = "oauth2"
username: Optional[str]
"""Username of the third-party service user that these credentials belong to"""
access_token: SecretStr
access_token_expires_at: Optional[int]
"""Unix timestamp (seconds) indicating when the access token expires (if at all)"""
refresh_token: Optional[SecretStr]
refresh_token_expires_at: Optional[int]
"""Unix timestamp (seconds) indicating when the refresh token expires (if at all)"""
scopes: list[str]
metadata: dict[str, Any] = Field(default_factory=dict)
def bearer(self) -> str:
return f"Bearer {self.access_token.get_secret_value()}"
class APIKeyCredentials(_BaseCredentials):
type: Literal["api_key"] = "api_key"
api_key: SecretStr
expires_at: Optional[int]
"""Unix timestamp (seconds) indicating when the API key expires (if at all)"""
def bearer(self) -> str:
return f"Bearer {self.api_key.get_secret_value()}"
Credentials = Annotated[
OAuth2Credentials | APIKeyCredentials,
Field(discriminator="type"),
]
CredentialsType = Literal["api_key", "oauth2"]
class OAuthState(BaseModel):
token: str
provider: str
expires_at: int
code_verifier: Optional[str] = None
scopes: list[str]
"""Unix timestamp (seconds) indicating when this OAuth state expires"""
class UserMetadata(BaseModel):
integration_credentials: list[Credentials] = Field(default_factory=list)
integration_oauth_states: list[OAuthState] = Field(default_factory=list)
class UserMetadataRaw(TypedDict, total=False):
integration_credentials: list[dict]
integration_oauth_states: list[dict]
class UserIntegrations(BaseModel):
credentials: list[Credentials] = Field(default_factory=list)
oauth_states: list[OAuthState] = Field(default_factory=list)

View File

@@ -31,8 +31,7 @@ class RedisKeyedMutex:
try:
yield
finally:
if lock.locked():
lock.release()
lock.release()
def acquire(self, key: Any) -> "RedisLock":
"""Acquires and returns a lock with the given key"""
@@ -46,7 +45,7 @@ class RedisKeyedMutex:
return lock
def release(self, key: Any):
if (lock := self.locks.get(key)) and lock.locked() and lock.owned():
if lock := self.locks.get(key):
lock.release()
def release_all_locks(self):

View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "aiohappyeyeballs"
@@ -1091,13 +1091,13 @@ pyasn1 = ">=0.4.6,<0.7.0"
[[package]]
name = "pydantic"
version = "2.10.3"
version = "2.10.2"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"},
{file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"},
{file = "pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e"},
{file = "pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa"},
]
[package.dependencies]
@@ -1223,13 +1223,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "pydantic-settings"
version = "2.7.0"
version = "2.6.1"
description = "Settings management using Pydantic"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_settings-2.7.0-py3-none-any.whl", hash = "sha256:e00c05d5fa6cbbb227c84bd7487c5c1065084119b750df7c8c1a554aed236eb5"},
{file = "pydantic_settings-2.7.0.tar.gz", hash = "sha256:ac4bfd4a36831a48dbf8b2d9325425b549a0a6f18cea118436d728eb4f1c4d66"},
{file = "pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87"},
{file = "pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0"},
]
[package.dependencies]
@@ -1243,13 +1243,13 @@ yaml = ["pyyaml (>=6.0.1)"]
[[package]]
name = "pyjwt"
version = "2.10.1"
version = "2.10.0"
description = "JSON Web Token implementation in Python"
optional = false
python-versions = ">=3.9"
files = [
{file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"},
{file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"},
{file = "PyJWT-2.10.0-py3-none-any.whl", hash = "sha256:543b77207db656de204372350926bed5a86201c4cbff159f623f79c7bb487a15"},
{file = "pyjwt-2.10.0.tar.gz", hash = "sha256:7628a7eb7938959ac1b26e819a1df0fd3259505627b575e4bad6d08f76db695c"},
]
[package.extras]
@@ -1282,20 +1282,20 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments
[[package]]
name = "pytest-asyncio"
version = "0.25.0"
version = "0.24.0"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.9"
python-versions = ">=3.8"
files = [
{file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"},
{file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"},
{file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"},
{file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"},
]
[package.dependencies]
pytest = ">=8.2,<9"
[package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
[[package]]
@@ -1362,13 +1362,13 @@ websockets = ">=11,<13"
[[package]]
name = "redis"
version = "5.2.1"
version = "5.2.0"
description = "Python client for Redis database and key-value store"
optional = false
python-versions = ">=3.8"
files = [
{file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"},
{file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"},
{file = "redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897"},
{file = "redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0"},
]
[package.dependencies]
@@ -1415,29 +1415,29 @@ pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
version = "0.8.6"
version = "0.8.0"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.8.6-py3-none-linux_armv6l.whl", hash = "sha256:defed167955d42c68b407e8f2e6f56ba52520e790aba4ca707a9c88619e580e3"},
{file = "ruff-0.8.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:54799ca3d67ae5e0b7a7ac234baa657a9c1784b48ec954a094da7c206e0365b1"},
{file = "ruff-0.8.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e88b8f6d901477c41559ba540beeb5a671e14cd29ebd5683903572f4b40a9807"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0509e8da430228236a18a677fcdb0c1f102dd26d5520f71f79b094963322ed25"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a7ddb221779871cf226100e677b5ea38c2d54e9e2c8ed847450ebbdf99b32d"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:248b1fb3f739d01d528cc50b35ee9c4812aa58cc5935998e776bf8ed5b251e75"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:bc3c083c50390cf69e7e1b5a5a7303898966be973664ec0c4a4acea82c1d4315"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52d587092ab8df308635762386f45f4638badb0866355b2b86760f6d3c076188"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61323159cf21bc3897674e5adb27cd9e7700bab6b84de40d7be28c3d46dc67cf"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ae4478b1471fc0c44ed52a6fb787e641a2ac58b1c1f91763bafbc2faddc5117"},
{file = "ruff-0.8.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0c000a471d519b3e6cfc9c6680025d923b4ca140ce3e4612d1a2ef58e11f11fe"},
{file = "ruff-0.8.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9257aa841e9e8d9b727423086f0fa9a86b6b420fbf4bf9e1465d1250ce8e4d8d"},
{file = "ruff-0.8.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45a56f61b24682f6f6709636949ae8cc82ae229d8d773b4c76c09ec83964a95a"},
{file = "ruff-0.8.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:496dd38a53aa173481a7d8866bcd6451bd934d06976a2505028a50583e001b76"},
{file = "ruff-0.8.6-py3-none-win32.whl", hash = "sha256:e169ea1b9eae61c99b257dc83b9ee6c76f89042752cb2d83486a7d6e48e8f764"},
{file = "ruff-0.8.6-py3-none-win_amd64.whl", hash = "sha256:f1d70bef3d16fdc897ee290d7d20da3cbe4e26349f62e8a0274e7a3f4ce7a905"},
{file = "ruff-0.8.6-py3-none-win_arm64.whl", hash = "sha256:7d7fc2377a04b6e04ffe588caad613d0c460eb2ecba4c0ccbbfe2bc973cbc162"},
{file = "ruff-0.8.6.tar.gz", hash = "sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5"},
{file = "ruff-0.8.0-py3-none-linux_armv6l.whl", hash = "sha256:fcb1bf2cc6706adae9d79c8d86478677e3bbd4ced796ccad106fd4776d395fea"},
{file = "ruff-0.8.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:295bb4c02d58ff2ef4378a1870c20af30723013f441c9d1637a008baaf928c8b"},
{file = "ruff-0.8.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b1f1c76b47c18fa92ee78b60d2d20d7e866c55ee603e7d19c1e991fad933a9a"},
{file = "ruff-0.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb0d4f250a7711b67ad513fde67e8870109e5ce590a801c3722580fe98c33a99"},
{file = "ruff-0.8.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e55cce9aa93c5d0d4e3937e47b169035c7e91c8655b0974e61bb79cf398d49c"},
{file = "ruff-0.8.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f4cd64916d8e732ce6b87f3f5296a8942d285bbbc161acee7fe561134af64f9"},
{file = "ruff-0.8.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c5c1466be2a2ebdf7c5450dd5d980cc87c8ba6976fb82582fea18823da6fa362"},
{file = "ruff-0.8.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2dabfd05b96b7b8f2da00d53c514eea842bff83e41e1cceb08ae1966254a51df"},
{file = "ruff-0.8.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:facebdfe5a5af6b1588a1d26d170635ead6892d0e314477e80256ef4a8470cf3"},
{file = "ruff-0.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a8e86bae0dbd749c815211ca11e3a7bd559b9710746c559ed63106d382bd9c"},
{file = "ruff-0.8.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85e654f0ded7befe2d61eeaf3d3b1e4ef3894469cd664ffa85006c7720f1e4a2"},
{file = "ruff-0.8.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:83a55679c4cb449fa527b8497cadf54f076603cc36779b2170b24f704171ce70"},
{file = "ruff-0.8.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:812e2052121634cf13cd6fddf0c1871d0ead1aad40a1a258753c04c18bb71bbd"},
{file = "ruff-0.8.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:780d5d8523c04202184405e60c98d7595bdb498c3c6abba3b6d4cdf2ca2af426"},
{file = "ruff-0.8.0-py3-none-win32.whl", hash = "sha256:5fdb6efecc3eb60bba5819679466471fd7d13c53487df7248d6e27146e985468"},
{file = "ruff-0.8.0-py3-none-win_amd64.whl", hash = "sha256:582891c57b96228d146725975fbb942e1f30a0c4ba19722e692ca3eb25cc9b4f"},
{file = "ruff-0.8.0-py3-none-win_arm64.whl", hash = "sha256:ba93e6294e9a737cd726b74b09a6972e36bb511f9a102f1d9a7e1ce94dd206a6"},
{file = "ruff-0.8.0.tar.gz", hash = "sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44"},
]
[[package]]
@@ -1852,4 +1852,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<4.0"
content-hash = "bf1b0125759dadb1369fff05ffba64fea3e82b9b7a43d0068e1c80974a4ebc1c"
content-hash = "54bf6e076ec4d09be2307f07240018459dd6594efdc55a2dc2dc1d673184587e"

View File

@@ -10,18 +10,18 @@ packages = [{ include = "autogpt_libs" }]
colorama = "^0.4.6"
expiringdict = "^1.2.2"
google-cloud-logging = "^3.11.3"
pydantic = "^2.10.3"
pydantic-settings = "^2.7.0"
pyjwt = "^2.10.1"
pytest-asyncio = "^0.25.0"
pydantic = "^2.10.2"
pydantic-settings = "^2.6.1"
pyjwt = "^2.10.0"
pytest-asyncio = "^0.24.0"
pytest-mock = "^3.14.0"
python = ">=3.10,<4.0"
python-dotenv = "^1.0.1"
supabase = "^2.10.0"
[tool.poetry.group.dev.dependencies]
redis = "^5.2.1"
ruff = "^0.8.6"
redis = "^5.2.0"
ruff = "^0.8.0"
[build-system]
requires = ["poetry-core"]

View File

@@ -58,21 +58,6 @@ GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Twitter (X) OAuth 2.0 with PKCE Configuration
# 1. Create a Twitter Developer Account:
# - Visit https://developer.x.com/en and sign up
# 2. Set up your application:
# - Navigate to Developer Portal > Projects > Create Project
# - Add a new app to your project
# 3. Configure app settings:
# - App Permissions: Read + Write + Direct Messages
# - App Type: Web App, Automated App or Bot
# - OAuth 2.0 Callback URL: http://localhost:3000/auth/integrations/oauth_callback
# - Save your Client ID and Client Secret below
TWITTER_CLIENT_ID=
TWITTER_CLIENT_SECRET=
## ===== OPTIONAL API KEYS ===== ##
# LLM

View File

@@ -6,21 +6,17 @@ ENV PYTHONUNBUFFERED 1
WORKDIR /app
RUN echo 'Acquire::http::Pipeline-Depth 0;\nAcquire::http::No-Cache true;\nAcquire::BrokenProxy true;\n' > /etc/apt/apt.conf.d/99fixbadproxy
RUN apt-get update --allow-releaseinfo-change --fix-missing
# Install build dependencies
RUN apt-get install -y build-essential
RUN apt-get install -y libpq5
RUN apt-get install -y libz-dev
RUN apt-get install -y libssl-dev
RUN apt-get install -y postgresql-client
RUN apt-get update \
&& apt-get install -y build-essential curl ffmpeg wget libcurl4-gnutls-dev libexpat1-dev libpq5 gettext libz-dev libssl-dev postgresql-client git \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
ENV POETRY_HOME=/opt/poetry
ENV POETRY_NO_INTERACTION=1
ENV POETRY_VIRTUALENVS_CREATE=false
ENV PATH=/opt/poetry/bin:$PATH
ENV POETRY_VERSION=1.8.3 \
POETRY_HOME="/opt/poetry" \
POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_CREATE=false \
PATH="$POETRY_HOME/bin:$PATH"
# Upgrade pip and setuptools to fix security vulnerabilities
RUN pip3 install --upgrade pip setuptools
@@ -31,20 +27,24 @@ RUN pip3 install poetry
COPY autogpt_platform/autogpt_libs /app/autogpt_platform/autogpt_libs
COPY autogpt_platform/backend/poetry.lock autogpt_platform/backend/pyproject.toml /app/autogpt_platform/backend/
WORKDIR /app/autogpt_platform/backend
RUN poetry install --no-ansi --no-root
RUN poetry config virtualenvs.create false \
&& poetry install --no-interaction --no-ansi
# Generate Prisma client
COPY autogpt_platform/backend/schema.prisma ./
RUN poetry run prisma generate
RUN poetry config virtualenvs.create false \
&& poetry run prisma generate
FROM python:3.11.10-slim-bookworm AS server_dependencies
WORKDIR /app
ENV POETRY_HOME=/opt/poetry \
ENV POETRY_VERSION=1.8.3 \
POETRY_HOME="/opt/poetry" \
POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_CREATE=false
ENV PATH=/opt/poetry/bin:$PATH
POETRY_VIRTUALENVS_CREATE=false \
PATH="$POETRY_HOME/bin:$PATH"
# Upgrade pip and setuptools to fix security vulnerabilities
RUN pip3 install --upgrade pip setuptools
@@ -71,7 +71,6 @@ WORKDIR /app/autogpt_platform/backend
FROM server_dependencies AS server
COPY autogpt_platform/backend /app/autogpt_platform/backend
RUN poetry install --no-ansi --only-root
ENV DATABASE_URL=""
ENV PORT=8000

View File

@@ -200,4 +200,4 @@ To add a new agent block, you need to create a new class that inherits from `Blo
* `run` method: the main logic of the block.
* `test_input` & `test_output`: the sample input and output data for the block, which will be used to auto-test the block.
* You can mock the functions declared in the block using the `test_mock` field for your unit tests.
* Once you finish creating the block, you can test it by running `poetry run pytest -s test/block/test_block.py`.
* Once you finish creating the block, you can test it by running `pytest -s test/block/test_block.py`.

View File

@@ -15,10 +15,10 @@ modules = [
if f.is_file() and f.name != "__init__.py"
]
for module in modules:
if not re.match("^[a-z0-9_.]+$", module):
if not re.match("^[a-z_.]+$", module):
raise ValueError(
f"Block module {module} error: module name must be lowercase, "
"and contain only alphanumeric characters and underscores."
"separated by underscores, and contain only alphabet characters"
)
importlib.import_module(f".{module}", package=__name__)

View File

@@ -76,11 +76,7 @@ class AgentExecutorBlock(Block):
)
if not event.node_id:
if event.status in [
ExecutionStatus.COMPLETED,
ExecutionStatus.TERMINATED,
ExecutionStatus.FAILED,
]:
if event.status in [ExecutionStatus.COMPLETED, ExecutionStatus.FAILED]:
logger.info(f"Execution {log_id} ended with status {event.status}")
break
else:

View File

@@ -12,7 +12,6 @@ from backend.data.model import (
CredentialsMetaInput,
SchemaField,
)
from backend.integrations.providers import ProviderName
class ImageSize(str, Enum):
@@ -102,10 +101,12 @@ class ImageGenModel(str, Enum):
class AIImageGeneratorBlock(Block):
class Input(BlockSchema):
credentials: CredentialsMetaInput[
Literal[ProviderName.REPLICATE], Literal["api_key"]
] = CredentialsField(
description="Enter your Replicate API key to access the image generation API. You can obtain an API key from https://replicate.com/account/api-tokens.",
credentials: CredentialsMetaInput[Literal["replicate"], Literal["api_key"]] = (
CredentialsField(
provider="replicate",
supported_credential_types={"api_key"},
description="Enter your Replicate API key to access the image generation API. You can obtain an API key from https://replicate.com/account/api-tokens.",
)
)
prompt: str = SchemaField(
description="Text prompt for image generation",

View File

@@ -13,7 +13,6 @@ from backend.data.model import (
CredentialsMetaInput,
SchemaField,
)
from backend.integrations.providers import ProviderName
logger = logging.getLogger(__name__)
@@ -55,11 +54,13 @@ class NormalizationStrategy(str, Enum):
class AIMusicGeneratorBlock(Block):
class Input(BlockSchema):
credentials: CredentialsMetaInput[
Literal[ProviderName.REPLICATE], Literal["api_key"]
] = CredentialsField(
description="The Replicate integration can be used with "
"any API key with sufficient permissions for the blocks it is used on.",
credentials: CredentialsMetaInput[Literal["replicate"], Literal["api_key"]] = (
CredentialsField(
provider="replicate",
supported_credential_types={"api_key"},
description="The Replicate integration can be used with "
"any API key with sufficient permissions for the blocks it is used on.",
)
)
prompt: str = SchemaField(
description="A description of the music you want to generate",

View File

@@ -12,7 +12,6 @@ from backend.data.model import (
CredentialsMetaInput,
SchemaField,
)
from backend.integrations.providers import ProviderName
from backend.util.request import requests
TEST_CREDENTIALS = APIKeyCredentials(
@@ -141,11 +140,13 @@ logger = logging.getLogger(__name__)
class AIShortformVideoCreatorBlock(Block):
class Input(BlockSchema):
credentials: CredentialsMetaInput[
Literal[ProviderName.REVID], Literal["api_key"]
] = CredentialsField(
description="The revid.ai integration can be used with "
"any API key with sufficient permissions for the blocks it is used on.",
credentials: CredentialsMetaInput[Literal["revid"], Literal["api_key"]] = (
CredentialsField(
provider="revid",
supported_credential_types={"api_key"},
description="The revid.ai integration can be used with "
"any API key with sufficient permissions for the blocks it is used on.",
)
)
script: str = SchemaField(
description="""1. Use short and punctuated sentences\n\n2. Use linebreaks to create a new clip\n\n3. Text outside of brackets is spoken by the AI, and [text between brackets] will be used to guide the visual generation. For example, [close-up of a cat] will show a close-up of a cat.""",

View File

@@ -1,11 +1,13 @@
import re
from typing import Any, List
from jinja2 import BaseLoader, Environment
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema, BlockType
from backend.data.model import SchemaField
from backend.util.mock import MockObject
from backend.util.text import TextFormatter
formatter = TextFormatter()
jinja = Environment(loader=BaseLoader())
class StoreValueBlock(Block):
@@ -302,9 +304,9 @@ class AgentOutputBlock(Block):
"""
if input_data.format:
try:
yield "output", formatter.format_string(
input_data.format, {input_data.name: input_data.value}
)
fmt = re.sub(r"(?<!{){[ a-zA-Z0-9_]+}", r"{\g<0>}", input_data.format)
template = jinja.from_string(fmt)
yield "output", template.render({input_data.name: input_data.value})
except Exception as e:
yield "output", f"Error: {e}, {input_data.value}"
else:
@@ -492,101 +494,3 @@ class NoteBlock(Block):
def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "output", input_data.text
class CreateDictionaryBlock(Block):
class Input(BlockSchema):
values: dict[str, Any] = SchemaField(
description="Key-value pairs to create the dictionary with",
placeholder="e.g., {'name': 'Alice', 'age': 25}",
)
class Output(BlockSchema):
dictionary: dict[str, Any] = SchemaField(
description="The created dictionary containing the specified key-value pairs"
)
error: str = SchemaField(
description="Error message if dictionary creation failed"
)
def __init__(self):
super().__init__(
id="b924ddf4-de4f-4b56-9a85-358930dcbc91",
description="Creates a dictionary with the specified key-value pairs. Use this when you know all the values you want to add upfront.",
categories={BlockCategory.DATA},
input_schema=CreateDictionaryBlock.Input,
output_schema=CreateDictionaryBlock.Output,
test_input=[
{
"values": {"name": "Alice", "age": 25, "city": "New York"},
},
{
"values": {"numbers": [1, 2, 3], "active": True, "score": 95.5},
},
],
test_output=[
(
"dictionary",
{"name": "Alice", "age": 25, "city": "New York"},
),
(
"dictionary",
{"numbers": [1, 2, 3], "active": True, "score": 95.5},
),
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
try:
# The values are already validated by Pydantic schema
yield "dictionary", input_data.values
except Exception as e:
yield "error", f"Failed to create dictionary: {str(e)}"
class CreateListBlock(Block):
class Input(BlockSchema):
values: List[Any] = SchemaField(
description="A list of values to be combined into a new list.",
placeholder="e.g., ['Alice', 25, True]",
)
class Output(BlockSchema):
list: List[Any] = SchemaField(
description="The created list containing the specified values."
)
error: str = SchemaField(description="Error message if list creation failed.")
def __init__(self):
super().__init__(
id="a912d5c7-6e00-4542-b2a9-8034136930e4",
description="Creates a list with the specified values. Use this when you know all the values you want to add upfront.",
categories={BlockCategory.DATA},
input_schema=CreateListBlock.Input,
output_schema=CreateListBlock.Output,
test_input=[
{
"values": ["Alice", 25, True],
},
{
"values": [1, 2, 3, "four", {"key": "value"}],
},
],
test_output=[
(
"list",
["Alice", 25, True],
),
(
"list",
[1, 2, 3, "four", {"key": "value"}],
),
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
try:
# The values are already validated by Pydantic schema
yield "list", input_data.values
except Exception as e:
yield "error", f"Failed to create list: {str(e)}"

View File

@@ -1,190 +0,0 @@
from enum import Enum
from typing import Literal
from e2b_code_interpreter import Sandbox
from pydantic import SecretStr
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import (
APIKeyCredentials,
CredentialsField,
CredentialsMetaInput,
SchemaField,
)
from backend.integrations.providers import ProviderName
TEST_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="e2b",
api_key=SecretStr("mock-e2b-api-key"),
title="Mock E2B API key",
expires_at=None,
)
TEST_CREDENTIALS_INPUT = {
"provider": TEST_CREDENTIALS.provider,
"id": TEST_CREDENTIALS.id,
"type": TEST_CREDENTIALS.type,
"title": TEST_CREDENTIALS.type,
}
class ProgrammingLanguage(Enum):
PYTHON = "python"
JAVASCRIPT = "js"
BASH = "bash"
R = "r"
JAVA = "java"
class CodeExecutionBlock(Block):
# TODO : Add support to upload and download files
# Currently, You can customized the CPU and Memory, only by creating a pre customized sandbox template
class Input(BlockSchema):
credentials: CredentialsMetaInput[
Literal[ProviderName.E2B], Literal["api_key"]
] = CredentialsField(
description="Enter your api key for the E2B Sandbox. You can get it in here - https://e2b.dev/docs",
)
# Todo : Option to run commond in background
setup_commands: list[str] = SchemaField(
description=(
"Shell commands to set up the sandbox before running the code. "
"You can use `curl` or `git` to install your desired Debian based "
"package manager. `pip` and `npm` are pre-installed.\n\n"
"These commands are executed with `sh`, in the foreground."
),
placeholder="pip install cowsay",
default=[],
advanced=False,
)
code: str = SchemaField(
description="Code to execute in the sandbox",
placeholder="print('Hello, World!')",
default="",
advanced=False,
)
language: ProgrammingLanguage = SchemaField(
description="Programming language to execute",
default=ProgrammingLanguage.PYTHON,
advanced=False,
)
timeout: int = SchemaField(
description="Execution timeout in seconds", default=300
)
template_id: str = SchemaField(
description=(
"You can use an E2B sandbox template by entering its ID here. "
"Check out the E2B docs for more details: "
"[E2B - Sandbox template](https://e2b.dev/docs/sandbox-template)"
),
default="",
advanced=True,
)
class Output(BlockSchema):
response: str = SchemaField(description="Response from code execution")
stdout_logs: str = SchemaField(
description="Standard output logs from execution"
)
stderr_logs: str = SchemaField(description="Standard error logs from execution")
error: str = SchemaField(description="Error message if execution failed")
def __init__(self):
super().__init__(
id="0b02b072-abe7-11ef-8372-fb5d162dd712",
description="Executes code in an isolated sandbox environment with internet access.",
categories={BlockCategory.DEVELOPER_TOOLS},
input_schema=CodeExecutionBlock.Input,
output_schema=CodeExecutionBlock.Output,
test_credentials=TEST_CREDENTIALS,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"code": "print('Hello World')",
"language": ProgrammingLanguage.PYTHON.value,
"setup_commands": [],
"timeout": 300,
"template_id": "",
},
test_output=[
("response", "Hello World"),
("stdout_logs", "Hello World\n"),
],
test_mock={
"execute_code": lambda code, language, setup_commands, timeout, api_key, template_id: (
"Hello World",
"Hello World\n",
"",
),
},
)
def execute_code(
self,
code: str,
language: ProgrammingLanguage,
setup_commands: list[str],
timeout: int,
api_key: str,
template_id: str,
):
try:
sandbox = None
if template_id:
sandbox = Sandbox(
template=template_id, api_key=api_key, timeout=timeout
)
else:
sandbox = Sandbox(api_key=api_key, timeout=timeout)
if not sandbox:
raise Exception("Sandbox not created")
# Running setup commands
for cmd in setup_commands:
sandbox.commands.run(cmd)
# Executing the code
execution = sandbox.run_code(
code,
language=language.value,
on_error=lambda e: sandbox.kill(), # Kill the sandbox if there is an error
)
if execution.error:
raise Exception(execution.error)
response = execution.text
stdout_logs = "".join(execution.logs.stdout)
stderr_logs = "".join(execution.logs.stderr)
return response, stdout_logs, stderr_logs
except Exception as e:
raise e
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
response, stdout_logs, stderr_logs = self.execute_code(
input_data.code,
input_data.language,
input_data.setup_commands,
input_data.timeout,
credentials.api_key.get_secret_value(),
input_data.template_id,
)
if response:
yield "response", response
if stdout_logs:
yield "stdout_logs", stdout_logs
if stderr_logs:
yield "stderr_logs", stderr_logs
except Exception as e:
yield "error", str(e)

View File

@@ -1,110 +0,0 @@
import re
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class CodeExtractionBlock(Block):
class Input(BlockSchema):
text: str = SchemaField(
description="Text containing code blocks to extract (e.g., AI response)",
placeholder="Enter text containing code blocks",
)
class Output(BlockSchema):
html: str = SchemaField(description="Extracted HTML code")
css: str = SchemaField(description="Extracted CSS code")
javascript: str = SchemaField(description="Extracted JavaScript code")
python: str = SchemaField(description="Extracted Python code")
sql: str = SchemaField(description="Extracted SQL code")
java: str = SchemaField(description="Extracted Java code")
cpp: str = SchemaField(description="Extracted C++ code")
csharp: str = SchemaField(description="Extracted C# code")
json_code: str = SchemaField(description="Extracted JSON code")
bash: str = SchemaField(description="Extracted Bash code")
php: str = SchemaField(description="Extracted PHP code")
ruby: str = SchemaField(description="Extracted Ruby code")
yaml: str = SchemaField(description="Extracted YAML code")
markdown: str = SchemaField(description="Extracted Markdown code")
typescript: str = SchemaField(description="Extracted TypeScript code")
xml: str = SchemaField(description="Extracted XML code")
remaining_text: str = SchemaField(
description="Remaining text after code extraction"
)
def __init__(self):
super().__init__(
id="d3a7d896-3b78-4f44-8b4b-48fbf4f0bcd8",
description="Extracts code blocks from text and identifies their programming languages",
categories={BlockCategory.TEXT},
input_schema=CodeExtractionBlock.Input,
output_schema=CodeExtractionBlock.Output,
test_input={
"text": "Here's a Python example:\n```python\nprint('Hello World')\n```\nAnd some HTML:\n```html\n<h1>Title</h1>\n```"
},
test_output=[
("html", "<h1>Title</h1>"),
("python", "print('Hello World')"),
("remaining_text", "Here's a Python example:\nAnd some HTML:"),
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
# List of supported programming languages with mapped aliases
language_aliases = {
"html": ["html", "htm"],
"css": ["css"],
"javascript": ["javascript", "js"],
"python": ["python", "py"],
"sql": ["sql"],
"java": ["java"],
"cpp": ["cpp", "c++"],
"csharp": ["csharp", "c#", "cs"],
"json_code": ["json"],
"bash": ["bash", "shell", "sh"],
"php": ["php"],
"ruby": ["ruby", "rb"],
"yaml": ["yaml", "yml"],
"markdown": ["markdown", "md"],
"typescript": ["typescript", "ts"],
"xml": ["xml"],
}
# Extract code for each language
for canonical_name, aliases in language_aliases.items():
code = ""
# Try each alias for the language
for alias in aliases:
code_for_alias = self.extract_code(input_data.text, alias)
if code_for_alias:
code = code + "\n\n" + code_for_alias if code else code_for_alias
if code: # Only yield if there's actual code content
yield canonical_name, code
# Remove all code blocks from the text to get remaining text
pattern = (
r"```(?:"
+ "|".join(
re.escape(alias)
for aliases in language_aliases.values()
for alias in aliases
)
+ r")\s+[\s\S]*?```"
)
remaining_text = re.sub(pattern, "", input_data.text).strip()
remaining_text = re.sub(r"\n\s*\n", "\n", remaining_text)
if remaining_text: # Only yield if there's remaining text
yield "remaining_text", remaining_text
def extract_code(self, text: str, language: str) -> str:
# Escape special regex characters in the language string
language = re.escape(language)
# Extract all code blocks enclosed in ```language``` blocks
pattern = re.compile(rf"```{language}\s+(.*?)```", re.DOTALL | re.IGNORECASE)
matches = pattern.finditer(text)
# Combine all code blocks for this language with newlines between them
code_blocks = [match.group(1).strip() for match in matches]
return "\n\n".join(code_blocks) if code_blocks else ""

View File

@@ -1,59 +0,0 @@
from pydantic import BaseModel
from backend.data.block import (
Block,
BlockCategory,
BlockManualWebhookConfig,
BlockOutput,
BlockSchema,
)
from backend.data.model import SchemaField
from backend.integrations.webhooks.compass import CompassWebhookType
class Transcription(BaseModel):
text: str
speaker: str
end: float
start: float
duration: float
class TranscriptionDataModel(BaseModel):
date: str
transcription: str
transcriptions: list[Transcription]
class CompassAITriggerBlock(Block):
class Input(BlockSchema):
payload: TranscriptionDataModel = SchemaField(hidden=True)
class Output(BlockSchema):
transcription: str = SchemaField(
description="The contents of the compass transcription."
)
def __init__(self):
super().__init__(
id="9464a020-ed1d-49e1-990f-7f2ac924a2b7",
description="This block will output the contents of the compass transcription.",
categories={BlockCategory.HARDWARE},
input_schema=CompassAITriggerBlock.Input,
output_schema=CompassAITriggerBlock.Output,
webhook_config=BlockManualWebhookConfig(
provider="compass",
webhook_type=CompassWebhookType.TRANSCRIPTION,
),
test_input=[
{"input": "Hello, World!"},
{"input": "Hello, World!", "data": "Existing Data"},
],
# test_output=[
# ("output", "Hello, World!"), # No data provided, so trigger is returned
# ("output", "Existing Data"), # Data is provided, so data is returned.
# ],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "transcription", input_data.payload.transcription

View File

@@ -12,15 +12,16 @@ from backend.data.model import (
CredentialsMetaInput,
SchemaField,
)
from backend.integrations.providers import ProviderName
DiscordCredentials = CredentialsMetaInput[
Literal[ProviderName.DISCORD], Literal["api_key"]
]
DiscordCredentials = CredentialsMetaInput[Literal["discord"], Literal["api_key"]]
def DiscordCredentialsField() -> DiscordCredentials:
return CredentialsField(description="Discord bot token")
return CredentialsField(
description="Discord bot token",
provider="discord",
supported_credential_types={"api_key"},
)
TEST_CREDENTIALS = APIKeyCredentials(

View File

@@ -1,32 +0,0 @@
from typing import Literal
from pydantic import SecretStr
from backend.data.model import APIKeyCredentials, CredentialsField, CredentialsMetaInput
from backend.integrations.providers import ProviderName
ExaCredentials = APIKeyCredentials
ExaCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.EXA],
Literal["api_key"],
]
TEST_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="exa",
api_key=SecretStr("mock-exa-api-key"),
title="Mock Exa API key",
expires_at=None,
)
TEST_CREDENTIALS_INPUT = {
"provider": TEST_CREDENTIALS.provider,
"id": TEST_CREDENTIALS.id,
"type": TEST_CREDENTIALS.type,
"title": TEST_CREDENTIALS.title,
}
def ExaCredentialsField() -> ExaCredentialsInput:
"""Creates an Exa credentials input on a block."""
return CredentialsField(description="The Exa integration requires an API Key.")

View File

@@ -1,87 +0,0 @@
from typing import List, Optional
from pydantic import BaseModel
from backend.blocks.exa._auth import (
ExaCredentials,
ExaCredentialsField,
ExaCredentialsInput,
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import requests
class ContentRetrievalSettings(BaseModel):
text: Optional[dict] = SchemaField(
description="Text content settings",
default={"maxCharacters": 1000, "includeHtmlTags": False},
advanced=True,
)
highlights: Optional[dict] = SchemaField(
description="Highlight settings",
default={
"numSentences": 3,
"highlightsPerUrl": 3,
"query": "",
},
advanced=True,
)
summary: Optional[dict] = SchemaField(
description="Summary settings",
default={"query": ""},
advanced=True,
)
class ExaContentsBlock(Block):
class Input(BlockSchema):
credentials: ExaCredentialsInput = ExaCredentialsField()
ids: List[str] = SchemaField(
description="Array of document IDs obtained from searches",
)
contents: ContentRetrievalSettings = SchemaField(
description="Content retrieval settings",
default=ContentRetrievalSettings(),
advanced=True,
)
class Output(BlockSchema):
results: list = SchemaField(
description="List of document contents",
default=[],
)
def __init__(self):
super().__init__(
id="c52be83f-f8cd-4180-b243-af35f986b461",
description="Retrieves document contents using Exa's contents API",
categories={BlockCategory.SEARCH},
input_schema=ExaContentsBlock.Input,
output_schema=ExaContentsBlock.Output,
)
def run(
self, input_data: Input, *, credentials: ExaCredentials, **kwargs
) -> BlockOutput:
url = "https://api.exa.ai/contents"
headers = {
"Content-Type": "application/json",
"x-api-key": credentials.api_key.get_secret_value(),
}
payload = {
"ids": input_data.ids,
"text": input_data.contents.text,
"highlights": input_data.contents.highlights,
"summary": input_data.contents.summary,
}
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
yield "results", data.get("results", [])
except Exception as e:
yield "error", str(e)
yield "results", []

View File

@@ -1,54 +0,0 @@
from typing import Optional
from pydantic import BaseModel
from backend.data.model import SchemaField
class TextSettings(BaseModel):
max_characters: int = SchemaField(
default=1000,
description="Maximum number of characters to return",
placeholder="1000",
)
include_html_tags: bool = SchemaField(
default=False,
description="Whether to include HTML tags in the text",
placeholder="False",
)
class HighlightSettings(BaseModel):
num_sentences: int = SchemaField(
default=3,
description="Number of sentences per highlight",
placeholder="3",
)
highlights_per_url: int = SchemaField(
default=3,
description="Number of highlights per URL",
placeholder="3",
)
class SummarySettings(BaseModel):
query: Optional[str] = SchemaField(
default="",
description="Query string for summarization",
placeholder="Enter query",
)
class ContentSettings(BaseModel):
text: TextSettings = SchemaField(
default=TextSettings(),
description="Text content settings",
)
highlights: HighlightSettings = SchemaField(
default=HighlightSettings(),
description="Highlight settings",
)
summary: SummarySettings = SchemaField(
default=SummarySettings(),
description="Summary settings",
)

View File

@@ -1,143 +0,0 @@
from datetime import datetime
from typing import List
from backend.blocks.exa._auth import (
ExaCredentials,
ExaCredentialsField,
ExaCredentialsInput,
)
from backend.blocks.exa.helpers import ContentSettings
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import requests
class ExaSearchBlock(Block):
class Input(BlockSchema):
credentials: ExaCredentialsInput = ExaCredentialsField()
query: str = SchemaField(description="The search query")
use_auto_prompt: bool = SchemaField(
description="Whether to use autoprompt",
default=True,
advanced=True,
)
type: str = SchemaField(
description="Type of search",
default="",
advanced=True,
)
category: str = SchemaField(
description="Category to search within",
default="",
advanced=True,
)
number_of_results: int = SchemaField(
description="Number of results to return",
default=10,
advanced=True,
)
include_domains: List[str] = SchemaField(
description="Domains to include in search",
default=[],
)
exclude_domains: List[str] = SchemaField(
description="Domains to exclude from search",
default=[],
advanced=True,
)
start_crawl_date: datetime = SchemaField(
description="Start date for crawled content",
)
end_crawl_date: datetime = SchemaField(
description="End date for crawled content",
)
start_published_date: datetime = SchemaField(
description="Start date for published content",
)
end_published_date: datetime = SchemaField(
description="End date for published content",
)
include_text: List[str] = SchemaField(
description="Text patterns to include",
default=[],
advanced=True,
)
exclude_text: List[str] = SchemaField(
description="Text patterns to exclude",
default=[],
advanced=True,
)
contents: ContentSettings = SchemaField(
description="Content retrieval settings",
default=ContentSettings(),
advanced=True,
)
class Output(BlockSchema):
results: list = SchemaField(
description="List of search results",
default=[],
)
def __init__(self):
super().__init__(
id="996cec64-ac40-4dde-982f-b0dc60a5824d",
description="Searches the web using Exa's advanced search API",
categories={BlockCategory.SEARCH},
input_schema=ExaSearchBlock.Input,
output_schema=ExaSearchBlock.Output,
)
def run(
self, input_data: Input, *, credentials: ExaCredentials, **kwargs
) -> BlockOutput:
url = "https://api.exa.ai/search"
headers = {
"Content-Type": "application/json",
"x-api-key": credentials.api_key.get_secret_value(),
}
payload = {
"query": input_data.query,
"useAutoprompt": input_data.use_auto_prompt,
"numResults": input_data.number_of_results,
"contents": input_data.contents.dict(),
}
date_field_mapping = {
"start_crawl_date": "startCrawlDate",
"end_crawl_date": "endCrawlDate",
"start_published_date": "startPublishedDate",
"end_published_date": "endPublishedDate",
}
# Add dates if they exist
for input_field, api_field in date_field_mapping.items():
value = getattr(input_data, input_field, None)
if value:
payload[api_field] = value.strftime("%Y-%m-%dT%H:%M:%S.000Z")
optional_field_mapping = {
"type": "type",
"category": "category",
"include_domains": "includeDomains",
"exclude_domains": "excludeDomains",
"include_text": "includeText",
"exclude_text": "excludeText",
}
# Add other fields
for input_field, api_field in optional_field_mapping.items():
value = getattr(input_data, input_field)
if value: # Only add non-empty values
payload[api_field] = value
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
# Extract just the results array from the response
yield "results", data.get("results", [])
except Exception as e:
yield "error", str(e)
yield "results", []

View File

@@ -1,128 +0,0 @@
from datetime import datetime
from typing import Any, List
from backend.blocks.exa._auth import (
ExaCredentials,
ExaCredentialsField,
ExaCredentialsInput,
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import requests
from .helpers import ContentSettings
class ExaFindSimilarBlock(Block):
class Input(BlockSchema):
credentials: ExaCredentialsInput = ExaCredentialsField()
url: str = SchemaField(
description="The url for which you would like to find similar links"
)
number_of_results: int = SchemaField(
description="Number of results to return",
default=10,
advanced=True,
)
include_domains: List[str] = SchemaField(
description="Domains to include in search",
default=[],
advanced=True,
)
exclude_domains: List[str] = SchemaField(
description="Domains to exclude from search",
default=[],
advanced=True,
)
start_crawl_date: datetime = SchemaField(
description="Start date for crawled content",
)
end_crawl_date: datetime = SchemaField(
description="End date for crawled content",
)
start_published_date: datetime = SchemaField(
description="Start date for published content",
)
end_published_date: datetime = SchemaField(
description="End date for published content",
)
include_text: List[str] = SchemaField(
description="Text patterns to include (max 1 string, up to 5 words)",
default=[],
advanced=True,
)
exclude_text: List[str] = SchemaField(
description="Text patterns to exclude (max 1 string, up to 5 words)",
default=[],
advanced=True,
)
contents: ContentSettings = SchemaField(
description="Content retrieval settings",
default=ContentSettings(),
advanced=True,
)
class Output(BlockSchema):
results: List[Any] = SchemaField(
description="List of similar documents with title, URL, published date, author, and score",
default=[],
)
def __init__(self):
super().__init__(
id="5e7315d1-af61-4a0c-9350-7c868fa7438a",
description="Finds similar links using Exa's findSimilar API",
categories={BlockCategory.SEARCH},
input_schema=ExaFindSimilarBlock.Input,
output_schema=ExaFindSimilarBlock.Output,
)
def run(
self, input_data: Input, *, credentials: ExaCredentials, **kwargs
) -> BlockOutput:
url = "https://api.exa.ai/findSimilar"
headers = {
"Content-Type": "application/json",
"x-api-key": credentials.api_key.get_secret_value(),
}
payload = {
"url": input_data.url,
"numResults": input_data.number_of_results,
"contents": input_data.contents.dict(),
}
optional_field_mapping = {
"include_domains": "includeDomains",
"exclude_domains": "excludeDomains",
"include_text": "includeText",
"exclude_text": "excludeText",
}
# Add optional fields if they have values
for input_field, api_field in optional_field_mapping.items():
value = getattr(input_data, input_field)
if value: # Only add non-empty values
payload[api_field] = value
date_field_mapping = {
"start_crawl_date": "startCrawlDate",
"end_crawl_date": "endCrawlDate",
"start_published_date": "startPublishedDate",
"end_published_date": "endPublishedDate",
}
# Add dates if they exist
for input_field, api_field in date_field_mapping.items():
value = getattr(input_data, input_field, None)
if value:
payload[api_field] = value.strftime("%Y-%m-%dT%H:%M:%S.000Z")
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
yield "results", data.get("results", [])
except Exception as e:
yield "error", str(e)
yield "results", []

View File

@@ -3,11 +3,10 @@ from typing import Literal
from pydantic import SecretStr
from backend.data.model import APIKeyCredentials, CredentialsField, CredentialsMetaInput
from backend.integrations.providers import ProviderName
FalCredentials = APIKeyCredentials
FalCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.FAL],
Literal["fal"],
Literal["api_key"],
]
@@ -31,5 +30,7 @@ def FalCredentialsField() -> FalCredentialsInput:
Creates a FAL credentials input on a block.
"""
return CredentialsField(
provider="fal",
supported_credential_types={"api_key"},
description="The FAL integration can be used with an API Key.",
)

View File

@@ -1,7 +1,7 @@
import logging
import time
from enum import Enum
from typing import Any
from typing import Any, Dict
import httpx
@@ -64,7 +64,7 @@ class AIVideoGeneratorBlock(Block):
},
)
def _get_headers(self, api_key: str) -> dict[str, str]:
def _get_headers(self, api_key: str) -> Dict[str, str]:
"""Get headers for FAL API requests."""
return {
"Authorization": f"Key {api_key}",
@@ -72,8 +72,8 @@ class AIVideoGeneratorBlock(Block):
}
def _submit_request(
self, url: str, headers: dict[str, str], data: dict[str, Any]
) -> dict[str, Any]:
self, url: str, headers: Dict[str, str], data: Dict[str, Any]
) -> Dict[str, Any]:
"""Submit a request to the FAL API."""
try:
response = httpx.post(url, headers=headers, json=data)
@@ -83,7 +83,7 @@ class AIVideoGeneratorBlock(Block):
logger.error(f"FAL API request failed: {str(e)}")
raise RuntimeError(f"Failed to submit request: {str(e)}")
def _poll_status(self, status_url: str, headers: dict[str, str]) -> dict[str, Any]:
def _poll_status(self, status_url: str, headers: Dict[str, str]) -> Dict[str, Any]:
"""Poll the status endpoint until completion or failure."""
try:
response = httpx.get(status_url, headers=headers)

View File

@@ -8,7 +8,6 @@ from backend.data.model import (
CredentialsMetaInput,
OAuth2Credentials,
)
from backend.integrations.providers import ProviderName
from backend.util.settings import Secrets
secrets = Secrets()
@@ -18,7 +17,7 @@ GITHUB_OAUTH_IS_CONFIGURED = bool(
GithubCredentials = APIKeyCredentials | OAuth2Credentials
GithubCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.GITHUB],
Literal["github"],
Literal["api_key", "oauth2"] if GITHUB_OAUTH_IS_CONFIGURED else Literal["api_key"],
]
@@ -31,6 +30,10 @@ def GithubCredentialsField(scope: str) -> GithubCredentialsInput:
scope: The authorization scope needed for the block to work. ([list of available scopes](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps#available-scopes))
""" # noqa
return CredentialsField(
provider="github",
supported_credential_types=(
{"api_key", "oauth2"} if GITHUB_OAUTH_IS_CONFIGURED else {"api_key"}
),
required_scopes={scope},
description="The GitHub integration can be used with OAuth, "
"or any API key with sufficient permissions for the blocks it is used on.",

View File

@@ -1,5 +1,3 @@
import re
from typing_extensions import TypedDict
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
@@ -255,7 +253,7 @@ class GithubReadPullRequestBlock(Block):
@staticmethod
def read_pr_changes(credentials: GithubCredentials, pr_url: str) -> str:
api = get_api(credentials)
files_url = prepare_pr_api_url(pr_url=pr_url, path="files")
files_url = pr_url + "/files"
response = api.get(files_url)
files = response.json()
changes = []
@@ -333,7 +331,7 @@ class GithubAssignPRReviewerBlock(Block):
credentials: GithubCredentials, pr_url: str, reviewer: str
) -> str:
api = get_api(credentials)
reviewers_url = prepare_pr_api_url(pr_url=pr_url, path="requested_reviewers")
reviewers_url = pr_url + "/requested_reviewers"
data = {"reviewers": [reviewer]}
api.post(reviewers_url, json=data)
return "Reviewer assigned successfully"
@@ -400,7 +398,7 @@ class GithubUnassignPRReviewerBlock(Block):
credentials: GithubCredentials, pr_url: str, reviewer: str
) -> str:
api = get_api(credentials)
reviewers_url = prepare_pr_api_url(pr_url=pr_url, path="requested_reviewers")
reviewers_url = pr_url + "/requested_reviewers"
data = {"reviewers": [reviewer]}
api.delete(reviewers_url, json=data)
return "Reviewer unassigned successfully"
@@ -480,7 +478,7 @@ class GithubListPRReviewersBlock(Block):
credentials: GithubCredentials, pr_url: str
) -> list[Output.ReviewerItem]:
api = get_api(credentials)
reviewers_url = prepare_pr_api_url(pr_url=pr_url, path="requested_reviewers")
reviewers_url = pr_url + "/requested_reviewers"
response = api.get(reviewers_url)
data = response.json()
reviewers: list[GithubListPRReviewersBlock.Output.ReviewerItem] = [
@@ -501,14 +499,3 @@ class GithubListPRReviewersBlock(Block):
input_data.pr_url,
)
yield from (("reviewer", reviewer) for reviewer in reviewers)
def prepare_pr_api_url(pr_url: str, path: str) -> str:
# Pattern to capture the base repository URL and the pull request number
pattern = r"^(?:https?://)?([^/]+/[^/]+/[^/]+)/pull/(\d+)"
match = re.match(pattern, pr_url)
if not match:
return pr_url
base_url, pr_number = match.groups()
return f"{base_url}/pulls/{pr_number}/{path}"

View File

@@ -699,420 +699,3 @@ class GithubDeleteBranchBlock(Block):
input_data.branch,
)
yield "status", status
class GithubCreateFileBlock(Block):
class Input(BlockSchema):
credentials: GithubCredentialsInput = GithubCredentialsField("repo")
repo_url: str = SchemaField(
description="URL of the GitHub repository",
placeholder="https://github.com/owner/repo",
)
file_path: str = SchemaField(
description="Path where the file should be created",
placeholder="path/to/file.txt",
)
content: str = SchemaField(
description="Content to write to the file",
placeholder="File content here",
)
branch: str = SchemaField(
description="Branch where the file should be created",
default="main",
)
commit_message: str = SchemaField(
description="Message for the commit",
default="Create new file",
)
class Output(BlockSchema):
url: str = SchemaField(description="URL of the created file")
sha: str = SchemaField(description="SHA of the commit")
error: str = SchemaField(
description="Error message if the file creation failed"
)
def __init__(self):
super().__init__(
id="8fd132ac-b917-428a-8159-d62893e8a3fe",
description="This block creates a new file in a GitHub repository.",
categories={BlockCategory.DEVELOPER_TOOLS},
input_schema=GithubCreateFileBlock.Input,
output_schema=GithubCreateFileBlock.Output,
test_input={
"repo_url": "https://github.com/owner/repo",
"file_path": "test/file.txt",
"content": "Test content",
"branch": "main",
"commit_message": "Create test file",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("url", "https://github.com/owner/repo/blob/main/test/file.txt"),
("sha", "abc123"),
],
test_mock={
"create_file": lambda *args, **kwargs: (
"https://github.com/owner/repo/blob/main/test/file.txt",
"abc123",
)
},
)
@staticmethod
def create_file(
credentials: GithubCredentials,
repo_url: str,
file_path: str,
content: str,
branch: str,
commit_message: str,
) -> tuple[str, str]:
api = get_api(credentials)
# Convert content to base64
content_bytes = content.encode("utf-8")
content_base64 = base64.b64encode(content_bytes).decode("utf-8")
# Create the file using the GitHub API
contents_url = f"{repo_url}/contents/{file_path}"
data = {
"message": commit_message,
"content": content_base64,
"branch": branch,
}
response = api.put(contents_url, json=data)
result = response.json()
return result["content"]["html_url"], result["commit"]["sha"]
def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
try:
url, sha = self.create_file(
credentials,
input_data.repo_url,
input_data.file_path,
input_data.content,
input_data.branch,
input_data.commit_message,
)
yield "url", url
yield "sha", sha
except Exception as e:
yield "error", str(e)
class GithubUpdateFileBlock(Block):
class Input(BlockSchema):
credentials: GithubCredentialsInput = GithubCredentialsField("repo")
repo_url: str = SchemaField(
description="URL of the GitHub repository",
placeholder="https://github.com/owner/repo",
)
file_path: str = SchemaField(
description="Path to the file to update",
placeholder="path/to/file.txt",
)
content: str = SchemaField(
description="New content for the file",
placeholder="Updated content here",
)
branch: str = SchemaField(
description="Branch containing the file",
default="main",
)
commit_message: str = SchemaField(
description="Message for the commit",
default="Update file",
)
class Output(BlockSchema):
url: str = SchemaField(description="URL of the updated file")
sha: str = SchemaField(description="SHA of the commit")
error: str = SchemaField(description="Error message if the file update failed")
def __init__(self):
super().__init__(
id="30be12a4-57cb-4aa4-baf5-fcc68d136076",
description="This block updates an existing file in a GitHub repository.",
categories={BlockCategory.DEVELOPER_TOOLS},
input_schema=GithubUpdateFileBlock.Input,
output_schema=GithubUpdateFileBlock.Output,
test_input={
"repo_url": "https://github.com/owner/repo",
"file_path": "test/file.txt",
"content": "Updated content",
"branch": "main",
"commit_message": "Update test file",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("url", "https://github.com/owner/repo/blob/main/test/file.txt"),
("sha", "def456"),
],
test_mock={
"update_file": lambda *args, **kwargs: (
"https://github.com/owner/repo/blob/main/test/file.txt",
"def456",
)
},
)
@staticmethod
def update_file(
credentials: GithubCredentials,
repo_url: str,
file_path: str,
content: str,
branch: str,
commit_message: str,
) -> tuple[str, str]:
api = get_api(credentials)
# First get the current file to get its SHA
contents_url = f"{repo_url}/contents/{file_path}"
params = {"ref": branch}
response = api.get(contents_url, params=params)
current_file = response.json()
# Convert new content to base64
content_bytes = content.encode("utf-8")
content_base64 = base64.b64encode(content_bytes).decode("utf-8")
# Update the file
data = {
"message": commit_message,
"content": content_base64,
"sha": current_file["sha"],
"branch": branch,
}
response = api.put(contents_url, json=data)
result = response.json()
return result["content"]["html_url"], result["commit"]["sha"]
def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
try:
url, sha = self.update_file(
credentials,
input_data.repo_url,
input_data.file_path,
input_data.content,
input_data.branch,
input_data.commit_message,
)
yield "url", url
yield "sha", sha
except Exception as e:
yield "error", str(e)
class GithubCreateRepositoryBlock(Block):
class Input(BlockSchema):
credentials: GithubCredentialsInput = GithubCredentialsField("repo")
name: str = SchemaField(
description="Name of the repository to create",
placeholder="my-new-repo",
)
description: str = SchemaField(
description="Description of the repository",
placeholder="A description of the repository",
default="",
)
private: bool = SchemaField(
description="Whether the repository should be private",
default=False,
)
auto_init: bool = SchemaField(
description="Whether to initialize the repository with a README",
default=True,
)
gitignore_template: str = SchemaField(
description="Git ignore template to use (e.g., Python, Node, Java)",
default="",
)
class Output(BlockSchema):
url: str = SchemaField(description="URL of the created repository")
clone_url: str = SchemaField(description="Git clone URL of the repository")
error: str = SchemaField(
description="Error message if the repository creation failed"
)
def __init__(self):
super().__init__(
id="029ec3b8-1cfd-46d3-b6aa-28e4a706efd1",
description="This block creates a new GitHub repository.",
categories={BlockCategory.DEVELOPER_TOOLS},
input_schema=GithubCreateRepositoryBlock.Input,
output_schema=GithubCreateRepositoryBlock.Output,
test_input={
"name": "test-repo",
"description": "A test repository",
"private": False,
"auto_init": True,
"gitignore_template": "Python",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("url", "https://github.com/owner/test-repo"),
("clone_url", "https://github.com/owner/test-repo.git"),
],
test_mock={
"create_repository": lambda *args, **kwargs: (
"https://github.com/owner/test-repo",
"https://github.com/owner/test-repo.git",
)
},
)
@staticmethod
def create_repository(
credentials: GithubCredentials,
name: str,
description: str,
private: bool,
auto_init: bool,
gitignore_template: str,
) -> tuple[str, str]:
api = get_api(credentials, convert_urls=False) # Disable URL conversion
data = {
"name": name,
"description": description,
"private": private,
"auto_init": auto_init,
}
if gitignore_template:
data["gitignore_template"] = gitignore_template
# Create repository using the user endpoint
response = api.post("https://api.github.com/user/repos", json=data)
result = response.json()
return result["html_url"], result["clone_url"]
def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
try:
url, clone_url = self.create_repository(
credentials,
input_data.name,
input_data.description,
input_data.private,
input_data.auto_init,
input_data.gitignore_template,
)
yield "url", url
yield "clone_url", clone_url
except Exception as e:
yield "error", str(e)
class GithubListStargazersBlock(Block):
class Input(BlockSchema):
credentials: GithubCredentialsInput = GithubCredentialsField("repo")
repo_url: str = SchemaField(
description="URL of the GitHub repository",
placeholder="https://github.com/owner/repo",
)
class Output(BlockSchema):
class StargazerItem(TypedDict):
username: str
url: str
stargazer: StargazerItem = SchemaField(
title="Stargazer",
description="Stargazers with their username and profile URL",
)
error: str = SchemaField(
description="Error message if listing stargazers failed"
)
def __init__(self):
super().__init__(
id="a4b9c2d1-e5f6-4g7h-8i9j-0k1l2m3n4o5p", # Generated unique UUID
description="This block lists all users who have starred a specified GitHub repository.",
categories={BlockCategory.DEVELOPER_TOOLS},
input_schema=GithubListStargazersBlock.Input,
output_schema=GithubListStargazersBlock.Output,
test_input={
"repo_url": "https://github.com/owner/repo",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
(
"stargazer",
{
"username": "octocat",
"url": "https://github.com/octocat",
},
)
],
test_mock={
"list_stargazers": lambda *args, **kwargs: [
{
"username": "octocat",
"url": "https://github.com/octocat",
}
]
},
)
@staticmethod
def list_stargazers(
credentials: GithubCredentials, repo_url: str
) -> list[Output.StargazerItem]:
api = get_api(credentials)
# Add /stargazers to the repo URL to get stargazers endpoint
stargazers_url = f"{repo_url}/stargazers"
# Set accept header to get starred_at timestamp
headers = {"Accept": "application/vnd.github.star+json"}
response = api.get(stargazers_url, headers=headers)
data = response.json()
stargazers: list[GithubListStargazersBlock.Output.StargazerItem] = [
{
"username": stargazer["login"],
"url": stargazer["html_url"],
}
for stargazer in data
]
return stargazers
def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
try:
stargazers = self.list_stargazers(
credentials,
input_data.repo_url,
)
yield from (("stargazer", stargazer) for stargazer in stargazers)
except Exception as e:
yield "error", str(e)

View File

@@ -111,9 +111,7 @@ class GithubPullRequestTriggerBlock(GitHubTriggerBase, Block):
def __init__(self):
from backend.integrations.webhooks.github import GithubWebhookType
example_payload = json.loads(
self.EXAMPLE_PAYLOAD_FILE.read_text(encoding="utf-8")
)
example_payload = json.loads(self.EXAMPLE_PAYLOAD_FILE.read_text())
super().__init__(
id="6c60ec01-8128-419e-988f-96a063ee2fea",

View File

@@ -3,7 +3,6 @@ from typing import Literal
from pydantic import SecretStr
from backend.data.model import CredentialsField, CredentialsMetaInput, OAuth2Credentials
from backend.integrations.providers import ProviderName
from backend.util.settings import Secrets
# --8<-- [start:GoogleOAuthIsConfigured]
@@ -13,9 +12,7 @@ GOOGLE_OAUTH_IS_CONFIGURED = bool(
)
# --8<-- [end:GoogleOAuthIsConfigured]
GoogleCredentials = OAuth2Credentials
GoogleCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.GOOGLE], Literal["oauth2"]
]
GoogleCredentialsInput = CredentialsMetaInput[Literal["google"], Literal["oauth2"]]
def GoogleCredentialsField(scopes: list[str]) -> GoogleCredentialsInput:
@@ -26,6 +23,8 @@ def GoogleCredentialsField(scopes: list[str]) -> GoogleCredentialsInput:
scopes: The authorization scopes needed for the block to work.
"""
return CredentialsField(
provider="google",
supported_credential_types={"oauth2"},
required_scopes=set(scopes),
description="The Google integration requires OAuth2 authentication.",
)

View File

@@ -10,7 +10,6 @@ from backend.data.model import (
CredentialsMetaInput,
SchemaField,
)
from backend.integrations.providers import ProviderName
TEST_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
@@ -39,8 +38,12 @@ class Place(BaseModel):
class GoogleMapsSearchBlock(Block):
class Input(BlockSchema):
credentials: CredentialsMetaInput[
Literal[ProviderName.GOOGLE_MAPS], Literal["api_key"]
] = CredentialsField(description="Google Maps API Key")
Literal["google_maps"], Literal["api_key"]
] = CredentialsField(
provider="google_maps",
supported_credential_types={"api_key"},
description="Google Maps API Key",
)
query: str = SchemaField(
description="Search query for local businesses",
placeholder="e.g., 'restaurants in New York'",

View File

@@ -56,24 +56,15 @@ class SendWebRequestBlock(Block):
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
body = input_data.body
if input_data.json_format:
if isinstance(body, str):
try:
# Try to parse as JSON first
body = json.loads(body)
except json.JSONDecodeError:
# If it's not valid JSON and just plain text,
# we should send it as plain text instead
input_data.json_format = False
if isinstance(input_data.body, str):
input_data.body = json.loads(input_data.body)
response = requests.request(
input_data.method.value,
input_data.url,
headers=input_data.headers,
json=body if input_data.json_format else None,
data=body if not input_data.json_format else None,
json=input_data.body if input_data.json_format else None,
data=input_data.body if not input_data.json_format else None,
)
result = response.json() if input_data.json_format else response.text

View File

@@ -3,11 +3,10 @@ from typing import Literal
from pydantic import SecretStr
from backend.data.model import APIKeyCredentials, CredentialsField, CredentialsMetaInput
from backend.integrations.providers import ProviderName
HubSpotCredentials = APIKeyCredentials
HubSpotCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.HUBSPOT],
Literal["hubspot"],
Literal["api_key"],
]
@@ -15,6 +14,8 @@ HubSpotCredentialsInput = CredentialsMetaInput[
def HubSpotCredentialsField() -> HubSpotCredentialsInput:
"""Creates a HubSpot credentials input on a block."""
return CredentialsField(
provider="hubspot",
supported_credential_types={"api_key"},
description="The HubSpot integration requires an API Key.",
)

View File

@@ -11,7 +11,6 @@ from backend.data.model import (
CredentialsMetaInput,
SchemaField,
)
from backend.integrations.providers import ProviderName
from backend.util.request import requests
TEST_CREDENTIALS = APIKeyCredentials(
@@ -84,10 +83,13 @@ class UpscaleOption(str, Enum):
class IdeogramModelBlock(Block):
class Input(BlockSchema):
credentials: CredentialsMetaInput[
Literal[ProviderName.IDEOGRAM], Literal["api_key"]
] = CredentialsField(
description="The Ideogram integration can be used with any API key with sufficient permissions for the blocks it is used on.",
credentials: CredentialsMetaInput[Literal["ideogram"], Literal["api_key"]] = (
CredentialsField(
provider="ideogram",
supported_credential_types={"api_key"},
description="The Ideogram integration can be used with any API key with sufficient permissions for the blocks it is used on.",
)
)
prompt: str = SchemaField(
description="Text prompt for image generation",

View File

@@ -3,14 +3,27 @@ from typing import Literal
from pydantic import SecretStr
from backend.data.model import APIKeyCredentials, CredentialsField, CredentialsMetaInput
from backend.integrations.providers import ProviderName
JinaCredentials = APIKeyCredentials
JinaCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.JINA],
Literal["jina"],
Literal["api_key"],
]
TEST_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="jina",
api_key=SecretStr("mock-jina-api-key"),
title="Mock Jina API key",
expires_at=None,
)
TEST_CREDENTIALS_INPUT = {
"provider": TEST_CREDENTIALS.provider,
"id": TEST_CREDENTIALS.id,
"type": TEST_CREDENTIALS.type,
"title": TEST_CREDENTIALS.type,
}
def JinaCredentialsField() -> JinaCredentialsInput:
"""
@@ -18,6 +31,8 @@ def JinaCredentialsField() -> JinaCredentialsInput:
"""
return CredentialsField(
provider="jina",
supported_credential_types={"api_key"},
description="The Jina integration can be used with an API Key.",
)

View File

@@ -1,59 +0,0 @@
from urllib.parse import quote
import requests
from backend.blocks.jina._auth import (
JinaCredentials,
JinaCredentialsField,
JinaCredentialsInput,
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class FactCheckerBlock(Block):
class Input(BlockSchema):
statement: str = SchemaField(
description="The statement to check for factuality"
)
credentials: JinaCredentialsInput = JinaCredentialsField()
class Output(BlockSchema):
factuality: float = SchemaField(
description="The factuality score of the statement"
)
result: bool = SchemaField(description="The result of the factuality check")
reason: str = SchemaField(description="The reason for the factuality result")
error: str = SchemaField(description="Error message if the check fails")
def __init__(self):
super().__init__(
id="d38b6c5e-9968-4271-8423-6cfe60d6e7e6",
description="This block checks the factuality of a given statement using Jina AI's Grounding API.",
categories={BlockCategory.SEARCH},
input_schema=FactCheckerBlock.Input,
output_schema=FactCheckerBlock.Output,
)
def run(
self, input_data: Input, *, credentials: JinaCredentials, **kwargs
) -> BlockOutput:
encoded_statement = quote(input_data.statement)
url = f"https://g.jina.ai/{encoded_statement}"
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {credentials.api_key.get_secret_value()}",
}
response = requests.get(url, headers=headers)
response.raise_for_status()
data = response.json()
if "data" in data:
data = data["data"]
yield "factuality", data["factuality"]
yield "result", data["result"]
yield "reason", data["reason"]
else:
raise RuntimeError(f"Expected 'data' key not found in response: {data}")

View File

@@ -7,8 +7,6 @@ from typing import TYPE_CHECKING, Any, List, Literal, NamedTuple
from pydantic import SecretStr
from backend.integrations.providers import ProviderName
if TYPE_CHECKING:
from enum import _EnumMemberT
@@ -29,13 +27,7 @@ from backend.util.settings import BehaveAs, Settings
logger = logging.getLogger(__name__)
LLMProviderName = Literal[
ProviderName.ANTHROPIC,
ProviderName.GROQ,
ProviderName.OLLAMA,
ProviderName.OPENAI,
ProviderName.OPEN_ROUTER,
]
LLMProviderName = Literal["anthropic", "groq", "openai", "ollama", "open_router"]
AICredentials = CredentialsMetaInput[LLMProviderName, Literal["api_key"]]
TEST_CREDENTIALS = APIKeyCredentials(
@@ -56,6 +48,8 @@ TEST_CREDENTIALS_INPUT = {
def AICredentialsField() -> AICredentials:
return CredentialsField(
description="API key for the LLM provider.",
provider=["anthropic", "groq", "openai", "ollama", "open_router"],
supported_credential_types={"api_key"},
discriminator="model",
discriminator_mapping={
model.value: model.metadata.provider for model in LlmModel
@@ -111,9 +105,9 @@ class LlmModel(str, Enum, metaclass=LlmModelMeta):
# Ollama models
OLLAMA_LLAMA3_8B = "llama3"
OLLAMA_LLAMA3_405B = "llama3.1:405b"
OLLAMA_DOLPHIN = "dolphin-mistral:latest"
# OpenRouter models
GEMINI_FLASH_1_5_8B = "google/gemini-flash-1.5"
GEMINI_FLASH_1_5_EXP = "google/gemini-flash-1.5-exp"
GROK_BETA = "x-ai/grok-beta"
MISTRAL_NEMO = "mistralai/mistral-nemo"
COHERE_COMMAND_R_08_2024 = "cohere/command-r-08-2024"
@@ -123,14 +117,6 @@ class LlmModel(str, Enum, metaclass=LlmModelMeta):
PERPLEXITY_LLAMA_3_1_SONAR_LARGE_128K_ONLINE = (
"perplexity/llama-3.1-sonar-large-128k-online"
)
QWEN_QWQ_32B_PREVIEW = "qwen/qwq-32b-preview"
NOUSRESEARCH_HERMES_3_LLAMA_3_1_405B = "nousresearch/hermes-3-llama-3.1-405b"
NOUSRESEARCH_HERMES_3_LLAMA_3_1_70B = "nousresearch/hermes-3-llama-3.1-70b"
AMAZON_NOVA_LITE_V1 = "amazon/nova-lite-v1"
AMAZON_NOVA_MICRO_V1 = "amazon/nova-micro-v1"
AMAZON_NOVA_PRO_V1 = "amazon/nova-pro-v1"
MICROSOFT_WIZARDLM_2_8X22B = "microsoft/wizardlm-2-8x22b"
GRYPHE_MYTHOMAX_L2_13B = "gryphe/mythomax-l2-13b"
@property
def metadata(self) -> ModelMetadata:
@@ -165,8 +151,8 @@ MODEL_METADATA = {
LlmModel.LLAMA3_1_8B: ModelMetadata("groq", 131072),
LlmModel.OLLAMA_LLAMA3_8B: ModelMetadata("ollama", 8192),
LlmModel.OLLAMA_LLAMA3_405B: ModelMetadata("ollama", 8192),
LlmModel.OLLAMA_DOLPHIN: ModelMetadata("ollama", 32768),
LlmModel.GEMINI_FLASH_1_5_8B: ModelMetadata("open_router", 8192),
LlmModel.GEMINI_FLASH_1_5_EXP: ModelMetadata("open_router", 8192),
LlmModel.GROK_BETA: ModelMetadata("open_router", 8192),
LlmModel.MISTRAL_NEMO: ModelMetadata("open_router", 4000),
LlmModel.COHERE_COMMAND_R_08_2024: ModelMetadata("open_router", 4000),
@@ -176,14 +162,6 @@ MODEL_METADATA = {
LlmModel.PERPLEXITY_LLAMA_3_1_SONAR_LARGE_128K_ONLINE: ModelMetadata(
"open_router", 8192
),
LlmModel.QWEN_QWQ_32B_PREVIEW: ModelMetadata("open_router", 4000),
LlmModel.NOUSRESEARCH_HERMES_3_LLAMA_3_1_405B: ModelMetadata("open_router", 4000),
LlmModel.NOUSRESEARCH_HERMES_3_LLAMA_3_1_70B: ModelMetadata("open_router", 4000),
LlmModel.AMAZON_NOVA_LITE_V1: ModelMetadata("open_router", 4000),
LlmModel.AMAZON_NOVA_MICRO_V1: ModelMetadata("open_router", 4000),
LlmModel.AMAZON_NOVA_PRO_V1: ModelMetadata("open_router", 4000),
LlmModel.MICROSOFT_WIZARDLM_2_8X22B: ModelMetadata("open_router", 4000),
LlmModel.GRYPHE_MYTHOMAX_L2_13B: ModelMetadata("open_router", 4000),
}
for model in LlmModel:
@@ -242,12 +220,6 @@ class AIStructuredResponseGeneratorBlock(Block):
description="The maximum number of tokens to generate in the chat completion.",
)
ollama_host: str = SchemaField(
advanced=True,
default="localhost:11434",
description="Ollama host for local models",
)
class Output(BlockSchema):
response: dict[str, Any] = SchemaField(
description="The response object generated by the language model."
@@ -293,7 +265,6 @@ class AIStructuredResponseGeneratorBlock(Block):
prompt: list[dict],
json_format: bool,
max_tokens: int | None = None,
ollama_host: str = "localhost:11434",
) -> tuple[str, int, int]:
"""
Args:
@@ -302,7 +273,6 @@ class AIStructuredResponseGeneratorBlock(Block):
prompt: The prompt to send to the LLM.
json_format: Whether the response should be in JSON format.
max_tokens: The maximum number of tokens to generate in the chat completion.
ollama_host: The host for ollama to use
Returns:
The response from the LLM.
@@ -392,10 +362,9 @@ class AIStructuredResponseGeneratorBlock(Block):
response.usage.completion_tokens if response.usage else 0,
)
elif provider == "ollama":
client = ollama.Client(host=ollama_host)
sys_messages = [p["content"] for p in prompt if p["role"] == "system"]
usr_messages = [p["content"] for p in prompt if p["role"] != "system"]
response = client.generate(
response = ollama.generate(
model=llm_model.value,
prompt=f"{sys_messages}\n\n{usr_messages}",
stream=False,
@@ -495,7 +464,6 @@ class AIStructuredResponseGeneratorBlock(Block):
llm_model=llm_model,
prompt=prompt,
json_format=bool(input_data.expected_format),
ollama_host=input_data.ollama_host,
max_tokens=input_data.max_tokens,
)
self.merge_stats(
@@ -578,11 +546,6 @@ class AITextGeneratorBlock(Block):
prompt_values: dict[str, str] = SchemaField(
advanced=False, default={}, description="Values used to fill in the prompt."
)
ollama_host: str = SchemaField(
advanced=True,
default="localhost:11434",
description="Ollama host for local models",
)
max_tokens: int | None = SchemaField(
advanced=True,
default=None,
@@ -673,11 +636,6 @@ class AITextSummarizerBlock(Block):
description="The number of overlapping tokens between chunks to maintain context.",
ge=0,
)
ollama_host: str = SchemaField(
advanced=True,
default="localhost:11434",
description="Ollama host for local models",
)
class Output(BlockSchema):
summary: str = SchemaField(description="The final summary of the text.")
@@ -816,11 +774,6 @@ class AIConversationBlock(Block):
default=None,
description="The maximum number of tokens to generate in the chat completion.",
)
ollama_host: str = SchemaField(
advanced=True,
default="localhost:11434",
description="Ollama host for local models",
)
class Output(BlockSchema):
response: str = SchemaField(
@@ -918,11 +871,6 @@ class AIListGeneratorBlock(Block):
default=None,
description="The maximum number of tokens to generate in the chat completion.",
)
ollama_host: str = SchemaField(
advanced=True,
default="localhost:11434",
description="Ollama host for local models",
)
class Output(BlockSchema):
generated_list: List[str] = SchemaField(description="The generated list.")
@@ -1074,7 +1022,6 @@ class AIListGeneratorBlock(Block):
credentials=input_data.credentials,
model=input_data.model,
expected_format={}, # Do not use structured response
ollama_host=input_data.ollama_host,
),
credentials=credentials,
)

View File

@@ -12,7 +12,6 @@ from backend.data.model import (
SchemaField,
SecretField,
)
from backend.integrations.providers import ProviderName
from backend.util.request import requests
TEST_CREDENTIALS = APIKeyCredentials(
@@ -78,10 +77,12 @@ class PublishToMediumBlock(Block):
description="Whether to notify followers that the user has published",
placeholder="False",
)
credentials: CredentialsMetaInput[
Literal[ProviderName.MEDIUM], Literal["api_key"]
] = CredentialsField(
description="The Medium integration can be used with any API key with sufficient permissions for the blocks it is used on.",
credentials: CredentialsMetaInput[Literal["medium"], Literal["api_key"]] = (
CredentialsField(
provider="medium",
supported_credential_types={"api_key"},
description="The Medium integration can be used with any API key with sufficient permissions for the blocks it is used on.",
)
)
class Output(BlockSchema):

View File

@@ -1,32 +0,0 @@
from typing import Literal
from pydantic import SecretStr
from backend.data.model import APIKeyCredentials, CredentialsField, CredentialsMetaInput
from backend.integrations.providers import ProviderName
NvidiaCredentials = APIKeyCredentials
NvidiaCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.NVIDIA],
Literal["api_key"],
]
TEST_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="nvidia",
api_key=SecretStr("mock-nvidia-api-key"),
title="Mock Nvidia API key",
expires_at=None,
)
TEST_CREDENTIALS_INPUT = {
"provider": TEST_CREDENTIALS.provider,
"id": TEST_CREDENTIALS.id,
"type": TEST_CREDENTIALS.type,
"title": TEST_CREDENTIALS.title,
}
def NvidiaCredentialsField() -> NvidiaCredentialsInput:
"""Creates an Nvidia credentials input on a block."""
return CredentialsField(description="The Nvidia integration requires an API Key.")

View File

@@ -1,90 +0,0 @@
from backend.blocks.nvidia._auth import (
NvidiaCredentials,
NvidiaCredentialsField,
NvidiaCredentialsInput,
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import requests
class NvidiaDeepfakeDetectBlock(Block):
class Input(BlockSchema):
credentials: NvidiaCredentialsInput = NvidiaCredentialsField()
image_base64: str = SchemaField(
description="Image to analyze for deepfakes", image_upload=True
)
return_image: bool = SchemaField(
description="Whether to return the processed image with markings",
default=False,
)
class Output(BlockSchema):
status: str = SchemaField(
description="Detection status (SUCCESS, ERROR, CONTENT_FILTERED)",
default="",
)
image: str = SchemaField(
description="Processed image with detection markings (if return_image=True)",
default="",
image_output=True,
)
is_deepfake: float = SchemaField(
description="Probability that the image is a deepfake (0-1)",
default=0.0,
)
def __init__(self):
super().__init__(
id="8c7d0d67-e79c-44f6-92a1-c2600c8aac7f",
description="Detects potential deepfakes in images using Nvidia's AI API",
categories={BlockCategory.SAFETY},
input_schema=NvidiaDeepfakeDetectBlock.Input,
output_schema=NvidiaDeepfakeDetectBlock.Output,
)
def run(
self, input_data: Input, *, credentials: NvidiaCredentials, **kwargs
) -> BlockOutput:
url = "https://ai.api.nvidia.com/v1/cv/hive/deepfake-image-detection"
headers = {
"accept": "application/json",
"content-type": "application/json",
"Authorization": f"Bearer {credentials.api_key.get_secret_value()}",
}
image_data = f"data:image/jpeg;base64,{input_data.image_base64}"
payload = {
"input": [image_data],
"return_image": input_data.return_image,
}
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
result = data.get("data", [{}])[0]
# Get deepfake probability from first bounding box if any
deepfake_prob = 0.0
if result.get("bounding_boxes"):
deepfake_prob = result["bounding_boxes"][0].get("is_deepfake", 0.0)
yield "status", result.get("status", "ERROR")
yield "is_deepfake", deepfake_prob
if input_data.return_image:
image_data = result.get("image", "")
output_data = f"data:image/jpeg;base64,{image_data}"
yield "image", output_data
else:
yield "image", ""
except Exception as e:
yield "error", str(e)
yield "status", "ERROR"
yield "is_deepfake", 0.0
yield "image", ""

View File

@@ -10,18 +10,22 @@ from backend.data.model import (
CredentialsMetaInput,
SchemaField,
)
from backend.integrations.providers import ProviderName
PineconeCredentials = APIKeyCredentials
PineconeCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.PINECONE],
Literal["pinecone"],
Literal["api_key"],
]
def PineconeCredentialsField() -> PineconeCredentialsInput:
"""Creates a Pinecone credentials input on a block."""
"""
Creates a Pinecone credentials input on a block.
"""
return CredentialsField(
provider="pinecone",
supported_credential_types={"api_key"},
description="The Pinecone integration can be used with an API Key.",
)
@@ -143,7 +147,7 @@ class PineconeQueryBlock(Block):
top_k=input_data.top_k,
include_values=input_data.include_values,
include_metadata=input_data.include_metadata,
).to_dict() # type: ignore
).to_dict()
combined_text = ""
if results["matches"]:
texts = [

View File

@@ -13,7 +13,6 @@ from backend.data.model import (
CredentialsMetaInput,
SchemaField,
)
from backend.integrations.providers import ProviderName
TEST_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
@@ -55,11 +54,13 @@ class ImageType(str, Enum):
class ReplicateFluxAdvancedModelBlock(Block):
class Input(BlockSchema):
credentials: CredentialsMetaInput[
Literal[ProviderName.REPLICATE], Literal["api_key"]
] = CredentialsField(
description="The Replicate integration can be used with "
"any API key with sufficient permissions for the blocks it is used on.",
credentials: CredentialsMetaInput[Literal["replicate"], Literal["api_key"]] = (
CredentialsField(
provider="replicate",
supported_credential_types={"api_key"},
description="The Replicate integration can be used with "
"any API key with sufficient permissions for the blocks it is used on.",
)
)
prompt: str = SchemaField(
description="Text prompt for image generation",

View File

@@ -11,7 +11,6 @@ from backend.data.model import (
CredentialsMetaInput,
SchemaField,
)
from backend.integrations.providers import ProviderName
class GetWikipediaSummaryBlock(Block, GetRequest):
@@ -66,8 +65,10 @@ class GetWeatherInformationBlock(Block, GetRequest):
description="Location to get weather information for"
)
credentials: CredentialsMetaInput[
Literal[ProviderName.OPENWEATHERMAP], Literal["api_key"]
Literal["openweathermap"], Literal["api_key"]
] = CredentialsField(
provider="openweathermap",
supported_credential_types={"api_key"},
description="The OpenWeatherMap integration can be used with "
"any API key with sufficient permissions for the blocks it is used on.",
)

View File

@@ -1,70 +0,0 @@
from enum import Enum
from typing import Literal
from pydantic import BaseModel, SecretStr
from backend.data.model import APIKeyCredentials, CredentialsField, CredentialsMetaInput
from backend.integrations.providers import ProviderName
Slant3DCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.SLANT3D], Literal["api_key"]
]
def Slant3DCredentialsField() -> Slant3DCredentialsInput:
return CredentialsField(description="Slant3D API key for authentication")
TEST_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="slant3d",
api_key=SecretStr("mock-slant3d-api-key"),
title="Mock Slant3D API key",
expires_at=None,
)
TEST_CREDENTIALS_INPUT = {
"provider": TEST_CREDENTIALS.provider,
"id": TEST_CREDENTIALS.id,
"type": TEST_CREDENTIALS.type,
"title": TEST_CREDENTIALS.title,
}
class CustomerDetails(BaseModel):
name: str
email: str
phone: str
address: str
city: str
state: str
zip: str
country_iso: str = "US"
is_residential: bool = True
class Color(Enum):
WHITE = "white"
BLACK = "black"
class Profile(Enum):
PLA = "PLA"
PETG = "PETG"
class OrderItem(BaseModel):
# filename: str
file_url: str
quantity: str # String as per API spec
color: Color = Color.WHITE
profile: Profile = Profile.PLA
# image_url: str = ""
# sku: str = ""
class Filament(BaseModel):
filament: str
hexColor: str
colorTag: str
profile: str

View File

@@ -1,94 +0,0 @@
from typing import Any, Dict
from backend.data.block import Block
from backend.util.request import requests
from ._api import Color, CustomerDetails, OrderItem, Profile
class Slant3DBlockBase(Block):
"""Base block class for Slant3D API interactions"""
BASE_URL = "https://www.slant3dapi.com/api"
def _get_headers(self, api_key: str) -> Dict[str, str]:
return {"api-key": api_key, "Content-Type": "application/json"}
def _make_request(self, method: str, endpoint: str, api_key: str, **kwargs) -> Dict:
url = f"{self.BASE_URL}/{endpoint}"
response = requests.request(
method=method, url=url, headers=self._get_headers(api_key), **kwargs
)
if not response.ok:
error_msg = response.json().get("error", "Unknown error")
raise RuntimeError(f"API request failed: {error_msg}")
return response.json()
def _check_valid_color(self, profile: Profile, color: Color, api_key: str) -> str:
response = self._make_request(
"GET",
"filament",
api_key,
params={"profile": profile.value, "color": color.value},
)
if profile == Profile.PLA:
color_tag = color.value
else:
color_tag = f"{profile.value.lower()}{color.value.capitalize()}"
valid_tags = [filament["colorTag"] for filament in response["filaments"]]
if color_tag not in valid_tags:
raise ValueError(
f"""Invalid color profile combination {color_tag}.
Valid colors for {profile.value} are:
{','.join([filament['colorTag'].replace(profile.value.lower(), '') for filament in response['filaments'] if filament['profile'] == profile.value])}
"""
)
return color_tag
def _convert_to_color(self, profile: Profile, color: Color, api_key: str) -> str:
return self._check_valid_color(profile, color, api_key)
def _format_order_data(
self,
customer: CustomerDetails,
order_number: str,
items: list[OrderItem],
api_key: str,
) -> list[dict[str, Any]]:
"""Helper function to format order data for API requests"""
orders = []
for item in items:
order_data = {
"email": customer.email,
"phone": customer.phone,
"name": customer.name,
"orderNumber": order_number,
"filename": item.file_url,
"fileURL": item.file_url,
"bill_to_street_1": customer.address,
"bill_to_city": customer.city,
"bill_to_state": customer.state,
"bill_to_zip": customer.zip,
"bill_to_country_as_iso": customer.country_iso,
"bill_to_is_US_residential": str(customer.is_residential).lower(),
"ship_to_name": customer.name,
"ship_to_street_1": customer.address,
"ship_to_city": customer.city,
"ship_to_state": customer.state,
"ship_to_zip": customer.zip,
"ship_to_country_as_iso": customer.country_iso,
"ship_to_is_US_residential": str(customer.is_residential).lower(),
"order_item_name": item.file_url,
"order_quantity": item.quantity,
"order_image_url": "",
"order_sku": "NOT_USED",
"order_item_color": self._convert_to_color(
item.profile, item.color, api_key
),
"profile": item.profile.value,
}
orders.append(order_data)
return orders

View File

@@ -1,85 +0,0 @@
from typing import List
from backend.data.block import BlockOutput, BlockSchema
from backend.data.model import APIKeyCredentials, SchemaField
from ._api import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
Filament,
Slant3DCredentialsField,
Slant3DCredentialsInput,
)
from .base import Slant3DBlockBase
class Slant3DFilamentBlock(Slant3DBlockBase):
"""Block for retrieving available filaments"""
class Input(BlockSchema):
credentials: Slant3DCredentialsInput = Slant3DCredentialsField()
class Output(BlockSchema):
filaments: List[Filament] = SchemaField(
description="List of available filaments"
)
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="7cc416f4-f305-4606-9b3b-452b8a81031c",
description="Get list of available filaments",
input_schema=self.Input,
output_schema=self.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT},
test_credentials=TEST_CREDENTIALS,
test_output=[
(
"filaments",
[
{
"filament": "PLA BLACK",
"hexColor": "000000",
"colorTag": "black",
"profile": "PLA",
},
{
"filament": "PLA WHITE",
"hexColor": "ffffff",
"colorTag": "white",
"profile": "PLA",
},
],
)
],
test_mock={
"_make_request": lambda *args, **kwargs: {
"filaments": [
{
"filament": "PLA BLACK",
"hexColor": "000000",
"colorTag": "black",
"profile": "PLA",
},
{
"filament": "PLA WHITE",
"hexColor": "ffffff",
"colorTag": "white",
"profile": "PLA",
},
]
}
},
)
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
result = self._make_request(
"GET", "filament", credentials.api_key.get_secret_value()
)
yield "filaments", result["filaments"]
except Exception as e:
yield "error", str(e)
raise

View File

@@ -1,418 +0,0 @@
import uuid
from typing import List
import requests as baserequests
from backend.data.block import BlockOutput, BlockSchema
from backend.data.model import APIKeyCredentials, SchemaField
from backend.util import settings
from backend.util.settings import BehaveAs
from ._api import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
CustomerDetails,
OrderItem,
Slant3DCredentialsField,
Slant3DCredentialsInput,
)
from .base import Slant3DBlockBase
class Slant3DCreateOrderBlock(Slant3DBlockBase):
"""Block for creating new orders"""
class Input(BlockSchema):
credentials: Slant3DCredentialsInput = Slant3DCredentialsField()
order_number: str = SchemaField(
description="Your custom order number (or leave blank for a random one)",
default_factory=lambda: str(uuid.uuid4()),
)
customer: CustomerDetails = SchemaField(
description="Customer details for where to ship the item",
advanced=False,
)
items: List[OrderItem] = SchemaField(
description="List of items to print",
advanced=False,
)
class Output(BlockSchema):
order_id: str = SchemaField(description="Slant3D order ID")
error: str = SchemaField(description="Error message if order failed")
def __init__(self):
super().__init__(
id="f73007d6-f48f-4aaf-9e6b-6883998a09b4",
description="Create a new print order",
input_schema=self.Input,
output_schema=self.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"order_number": "TEST-001",
"customer": {
"name": "John Doe",
"email": "john@example.com",
"phone": "123-456-7890",
"address": "123 Test St",
"city": "Test City",
"state": "TS",
"zip": "12345",
},
"items": [
{
"file_url": "https://example.com/model.stl",
"quantity": "1",
"color": "black",
"profile": "PLA",
}
],
},
test_credentials=TEST_CREDENTIALS,
test_output=[("order_id", "314144241")],
test_mock={
"_make_request": lambda *args, **kwargs: {"orderId": "314144241"},
"_convert_to_color": lambda *args, **kwargs: "black",
},
)
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
order_data = self._format_order_data(
input_data.customer,
input_data.order_number,
input_data.items,
credentials.api_key.get_secret_value(),
)
result = self._make_request(
"POST", "order", credentials.api_key.get_secret_value(), json=order_data
)
yield "order_id", result["orderId"]
except Exception as e:
yield "error", str(e)
raise
class Slant3DEstimateOrderBlock(Slant3DBlockBase):
"""Block for getting order cost estimates"""
class Input(BlockSchema):
credentials: Slant3DCredentialsInput = Slant3DCredentialsField()
order_number: str = SchemaField(
description="Your custom order number (or leave blank for a random one)",
default_factory=lambda: str(uuid.uuid4()),
)
customer: CustomerDetails = SchemaField(
description="Customer details for where to ship the item",
advanced=False,
)
items: List[OrderItem] = SchemaField(
description="List of items to print",
advanced=False,
)
class Output(BlockSchema):
total_price: float = SchemaField(description="Total price in USD")
shipping_cost: float = SchemaField(description="Shipping cost")
printing_cost: float = SchemaField(description="Printing cost")
error: str = SchemaField(description="Error message if estimation failed")
def __init__(self):
super().__init__(
id="bf8823d6-b42a-48c7-b558-d7c117f2ae85",
description="Get order cost estimate",
input_schema=self.Input,
output_schema=self.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"order_number": "TEST-001",
"customer": {
"name": "John Doe",
"email": "john@example.com",
"phone": "123-456-7890",
"address": "123 Test St",
"city": "Test City",
"state": "TS",
"zip": "12345",
},
"items": [
{
"file_url": "https://example.com/model.stl",
"quantity": "1",
"color": "black",
"profile": "PLA",
}
],
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("total_price", 9.31),
("shipping_cost", 5.56),
("printing_cost", 3.75),
],
test_mock={
"_make_request": lambda *args, **kwargs: {
"totalPrice": 9.31,
"shippingCost": 5.56,
"printingCost": 3.75,
},
"_convert_to_color": lambda *args, **kwargs: "black",
},
)
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
order_data = self._format_order_data(
input_data.customer,
input_data.order_number,
input_data.items,
credentials.api_key.get_secret_value(),
)
try:
result = self._make_request(
"POST",
"order/estimate",
credentials.api_key.get_secret_value(),
json=order_data,
)
yield "total_price", result["totalPrice"]
yield "shipping_cost", result["shippingCost"]
yield "printing_cost", result["printingCost"]
except baserequests.HTTPError as e:
yield "error", str(f"Error estimating order: {e} {e.response.text}")
raise
class Slant3DEstimateShippingBlock(Slant3DBlockBase):
"""Block for getting shipping cost estimates"""
class Input(BlockSchema):
credentials: Slant3DCredentialsInput = Slant3DCredentialsField()
order_number: str = SchemaField(
description="Your custom order number (or leave blank for a random one)",
default_factory=lambda: str(uuid.uuid4()),
)
customer: CustomerDetails = SchemaField(
description="Customer details for where to ship the item"
)
items: List[OrderItem] = SchemaField(
description="List of items to print",
advanced=False,
)
class Output(BlockSchema):
shipping_cost: float = SchemaField(description="Estimated shipping cost")
currency_code: str = SchemaField(description="Currency code (e.g., 'usd')")
error: str = SchemaField(description="Error message if estimation failed")
def __init__(self):
super().__init__(
id="00aae2a1-caf6-4a74-8175-39a0615d44e1",
description="Get shipping cost estimate",
input_schema=self.Input,
output_schema=self.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"order_number": "TEST-001",
"customer": {
"name": "John Doe",
"email": "john@example.com",
"phone": "123-456-7890",
"address": "123 Test St",
"city": "Test City",
"state": "TS",
"zip": "12345",
},
"items": [
{
"file_url": "https://example.com/model.stl",
"quantity": "1",
"color": "black",
"profile": "PLA",
}
],
},
test_credentials=TEST_CREDENTIALS,
test_output=[("shipping_cost", 4.81), ("currency_code", "usd")],
test_mock={
"_make_request": lambda *args, **kwargs: {
"shippingCost": 4.81,
"currencyCode": "usd",
},
"_convert_to_color": lambda *args, **kwargs: "black",
},
)
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
order_data = self._format_order_data(
input_data.customer,
input_data.order_number,
input_data.items,
credentials.api_key.get_secret_value(),
)
result = self._make_request(
"POST",
"order/estimateShipping",
credentials.api_key.get_secret_value(),
json=order_data,
)
yield "shipping_cost", result["shippingCost"]
yield "currency_code", result["currencyCode"]
except Exception as e:
yield "error", str(e)
raise
class Slant3DGetOrdersBlock(Slant3DBlockBase):
"""Block for retrieving all orders"""
class Input(BlockSchema):
credentials: Slant3DCredentialsInput = Slant3DCredentialsField()
class Output(BlockSchema):
orders: List[str] = SchemaField(description="List of orders with their details")
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="42283bf5-8a32-4fb4-92a2-60a9ea48e105",
description="Get all orders for the account",
input_schema=self.Input,
output_schema=self.Output,
# This block is disabled for cloud hosted because it allows access to all orders for the account
disabled=settings.Settings().config.behave_as == BehaveAs.CLOUD,
test_input={"credentials": TEST_CREDENTIALS_INPUT},
test_credentials=TEST_CREDENTIALS,
test_output=[
(
"orders",
[
"1234567890",
],
)
],
test_mock={
"_make_request": lambda *args, **kwargs: {
"ordersData": [
{
"orderId": 1234567890,
"orderTimestamp": {
"_seconds": 1719510986,
"_nanoseconds": 710000000,
},
}
]
}
},
)
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
result = self._make_request(
"GET", "order", credentials.api_key.get_secret_value()
)
yield "orders", [str(order["orderId"]) for order in result["ordersData"]]
except Exception as e:
yield "error", str(e)
raise
class Slant3DTrackingBlock(Slant3DBlockBase):
"""Block for tracking order status and shipping"""
class Input(BlockSchema):
credentials: Slant3DCredentialsInput = Slant3DCredentialsField()
order_id: str = SchemaField(description="Slant3D order ID to track")
class Output(BlockSchema):
status: str = SchemaField(description="Order status")
tracking_numbers: List[str] = SchemaField(
description="List of tracking numbers"
)
error: str = SchemaField(description="Error message if tracking failed")
def __init__(self):
super().__init__(
id="dd7c0293-c5af-4551-ba3e-fc162fb1fb89",
description="Track order status and shipping",
input_schema=self.Input,
output_schema=self.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"order_id": "314144241",
},
test_credentials=TEST_CREDENTIALS,
test_output=[("status", "awaiting_shipment"), ("tracking_numbers", [])],
test_mock={
"_make_request": lambda *args, **kwargs: {
"status": "awaiting_shipment",
"trackingNumbers": [],
}
},
)
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
result = self._make_request(
"GET",
f"order/{input_data.order_id}/get-tracking",
credentials.api_key.get_secret_value(),
)
yield "status", result["status"]
yield "tracking_numbers", result["trackingNumbers"]
except Exception as e:
yield "error", str(e)
raise
class Slant3DCancelOrderBlock(Slant3DBlockBase):
"""Block for canceling orders"""
class Input(BlockSchema):
credentials: Slant3DCredentialsInput = Slant3DCredentialsField()
order_id: str = SchemaField(description="Slant3D order ID to cancel")
class Output(BlockSchema):
status: str = SchemaField(description="Cancellation status message")
error: str = SchemaField(description="Error message if cancellation failed")
def __init__(self):
super().__init__(
id="54de35e1-407f-450b-b5fa-3b5e2eba8185",
description="Cancel an existing order",
input_schema=self.Input,
output_schema=self.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"order_id": "314144241",
},
test_credentials=TEST_CREDENTIALS,
test_output=[("status", "Order cancelled")],
test_mock={
"_make_request": lambda *args, **kwargs: {"status": "Order cancelled"}
},
)
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
result = self._make_request(
"DELETE",
f"order/{input_data.order_id}",
credentials.api_key.get_secret_value(),
)
yield "status", result["status"]
except Exception as e:
yield "error", str(e)
raise

View File

@@ -1,61 +0,0 @@
from backend.data.block import BlockOutput, BlockSchema
from backend.data.model import APIKeyCredentials, SchemaField
from ._api import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
Slant3DCredentialsField,
Slant3DCredentialsInput,
)
from .base import Slant3DBlockBase
class Slant3DSlicerBlock(Slant3DBlockBase):
"""Block for slicing 3D model files"""
class Input(BlockSchema):
credentials: Slant3DCredentialsInput = Slant3DCredentialsField()
file_url: str = SchemaField(
description="URL of the 3D model file to slice (STL)"
)
class Output(BlockSchema):
message: str = SchemaField(description="Response message")
price: float = SchemaField(description="Calculated price for printing")
error: str = SchemaField(description="Error message if slicing failed")
def __init__(self):
super().__init__(
id="f8a12c8d-3e4b-4d5f-b6a7-8c9d0e1f2g3h",
description="Slice a 3D model file and get pricing information",
input_schema=self.Input,
output_schema=self.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"file_url": "https://example.com/model.stl",
},
test_credentials=TEST_CREDENTIALS,
test_output=[("message", "Slicing successful"), ("price", 8.23)],
test_mock={
"_make_request": lambda *args, **kwargs: {
"message": "Slicing successful",
"data": {"price": 8.23},
}
},
)
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
result = self._make_request(
"POST",
"slicer",
credentials.api_key.get_secret_value(),
json={"fileURL": input_data.file_url},
)
yield "message", result["message"]
yield "price", result["data"]["price"]
except Exception as e:
yield "error", str(e)
raise

View File

@@ -1,125 +0,0 @@
from pydantic import BaseModel
from backend.data.block import (
Block,
BlockCategory,
BlockOutput,
BlockSchema,
BlockWebhookConfig,
)
from backend.data.model import SchemaField
from backend.util import settings
from backend.util.settings import AppEnvironment, BehaveAs
from ._api import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
Slant3DCredentialsField,
Slant3DCredentialsInput,
)
class Slant3DTriggerBase:
"""Base class for Slant3D webhook triggers"""
class Input(BlockSchema):
credentials: Slant3DCredentialsInput = Slant3DCredentialsField()
# Webhook URL is handled by the webhook system
payload: dict = SchemaField(hidden=True, default={})
class Output(BlockSchema):
payload: dict = SchemaField(
description="The complete webhook payload received from Slant3D"
)
order_id: str = SchemaField(description="The ID of the affected order")
error: str = SchemaField(
description="Error message if payload processing failed"
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "payload", input_data.payload
yield "order_id", input_data.payload["orderId"]
class Slant3DOrderWebhookBlock(Slant3DTriggerBase, Block):
"""Block for handling Slant3D order webhooks"""
class Input(Slant3DTriggerBase.Input):
class EventsFilter(BaseModel):
"""
Currently Slant3D only supports 'SHIPPED' status updates
Could be expanded in the future with more status types
"""
shipped: bool = True
events: EventsFilter = SchemaField(
title="Events",
description="Order status events to subscribe to",
default=EventsFilter(shipped=True),
)
class Output(Slant3DTriggerBase.Output):
status: str = SchemaField(description="The new status of the order")
tracking_number: str = SchemaField(
description="The tracking number for the shipment"
)
carrier_code: str = SchemaField(description="The carrier code (e.g., 'usps')")
def __init__(self):
super().__init__(
id="8a74c2ad-0104-4640-962f-26c6b69e58cd",
description=(
"This block triggers on Slant3D order status updates and outputs "
"the event details, including tracking information when orders are shipped."
),
# All webhooks are currently subscribed to for all orders. This works for self hosted, but not for cloud hosted prod
disabled=(
settings.Settings().config.behave_as == BehaveAs.CLOUD
and settings.Settings().config.app_env != AppEnvironment.LOCAL
),
categories={BlockCategory.DEVELOPER_TOOLS},
input_schema=self.Input,
output_schema=self.Output,
webhook_config=BlockWebhookConfig(
provider="slant3d",
webhook_type="orders", # Only one type for now
resource_format="", # No resource format needed
event_filter_input="events",
event_format="order.{event}",
),
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"events": {"shipped": True},
"payload": {
"orderId": "1234567890",
"status": "SHIPPED",
"trackingNumber": "ABCDEF123456",
"carrierCode": "usps",
},
},
test_credentials=TEST_CREDENTIALS,
test_output=[
(
"payload",
{
"orderId": "1234567890",
"status": "SHIPPED",
"trackingNumber": "ABCDEF123456",
"carrierCode": "usps",
},
),
("order_id", "1234567890"),
("status", "SHIPPED"),
("tracking_number", "ABCDEF123456"),
("carrier_code", "usps"),
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput: # type: ignore
yield from super().run(input_data, **kwargs)
# Extract and normalize values from the payload
yield "status", input_data.payload["status"]
yield "tracking_number", input_data.payload["trackingNumber"]
yield "carrier_code", input_data.payload["carrierCode"]

View File

@@ -10,7 +10,6 @@ from backend.data.model import (
CredentialsMetaInput,
SchemaField,
)
from backend.integrations.providers import ProviderName
from backend.util.request import requests
TEST_CREDENTIALS = APIKeyCredentials(
@@ -30,11 +29,13 @@ TEST_CREDENTIALS_INPUT = {
class CreateTalkingAvatarVideoBlock(Block):
class Input(BlockSchema):
credentials: CredentialsMetaInput[
Literal[ProviderName.D_ID], Literal["api_key"]
] = CredentialsField(
description="The D-ID integration can be used with "
"any API key with sufficient permissions for the blocks it is used on.",
credentials: CredentialsMetaInput[Literal["d_id"], Literal["api_key"]] = (
CredentialsField(
provider="d_id",
supported_credential_types={"api_key"},
description="The D-ID integration can be used with "
"any API key with sufficient permissions for the blocks it is used on.",
)
)
script_input: str = SchemaField(
description="The text input for the script",

View File

@@ -1,11 +1,13 @@
import re
from typing import Any
from jinja2 import BaseLoader, Environment
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util import json, text
from backend.util import json
formatter = text.TextFormatter()
jinja = Environment(loader=BaseLoader())
class MatchTextPatternBlock(Block):
@@ -71,7 +73,6 @@ class ExtractTextInformationBlock(Block):
description="Case sensitive match", default=True
)
dot_all: bool = SchemaField(description="Dot matches all", default=True)
find_all: bool = SchemaField(description="Find all matches", default=False)
class Output(BlockSchema):
positive: str = SchemaField(description="Extracted text")
@@ -89,27 +90,12 @@ class ExtractTextInformationBlock(Block):
{"text": "Hello, World!", "pattern": "Hello, (.+)", "group": 0},
{"text": "Hello, World!", "pattern": "Hello, (.+)", "group": 2},
{"text": "Hello, World!", "pattern": "hello,", "case_sensitive": False},
{
"text": "Hello, World!! Hello, Earth!!",
"pattern": "Hello, (\\S+)",
"group": 1,
"find_all": False,
},
{
"text": "Hello, World!! Hello, Earth!!",
"pattern": "Hello, (\\S+)",
"group": 1,
"find_all": True,
},
],
test_output=[
("positive", "World!"),
("positive", "Hello, World!"),
("negative", "Hello, World!"),
("positive", "Hello,"),
("positive", "World!!"),
("positive", "World!!"),
("positive", "Earth!!"),
],
)
@@ -121,21 +107,15 @@ class ExtractTextInformationBlock(Block):
flags = flags | re.DOTALL
if isinstance(input_data.text, str):
txt = input_data.text
text = input_data.text
else:
txt = json.dumps(input_data.text)
text = json.dumps(input_data.text)
matches = [
match.group(input_data.group)
for match in re.finditer(input_data.pattern, txt, flags)
if input_data.group <= len(match.groups())
]
for match in matches:
yield "positive", match
if not input_data.find_all:
return
if not matches:
yield "negative", input_data.text
match = re.search(input_data.pattern, text, flags)
if match and input_data.group <= len(match.groups()):
yield "positive", match.group(input_data.group)
else:
yield "negative", text
class FillTextTemplateBlock(Block):
@@ -166,20 +146,19 @@ class FillTextTemplateBlock(Block):
"values": {"list": ["Hello", " World!"]},
"format": "{% for item in list %}{{ item }}{% endfor %}",
},
{
"values": {},
"format": "{% set name = 'Alice' %}Hello, World! {{ name }}",
},
],
test_output=[
("output", "Hello, World! Alice"),
("output", "Hello World!"),
("output", "Hello, World! Alice"),
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "output", formatter.format_string(input_data.format, input_data.values)
# For python.format compatibility: replace all {...} with {{..}}.
# But avoid replacing {{...}} to {{{...}}}.
fmt = re.sub(r"(?<!{){[ a-zA-Z0-9_]+}", r"{\g<0>}", input_data.format)
template = jinja.from_string(fmt)
yield "output", template.render(**input_data.values)
class CombineTextsBlock(Block):

View File

@@ -9,7 +9,6 @@ from backend.data.model import (
CredentialsMetaInput,
SchemaField,
)
from backend.integrations.providers import ProviderName
from backend.util.request import requests
TEST_CREDENTIALS = APIKeyCredentials(
@@ -39,8 +38,10 @@ class UnrealTextToSpeechBlock(Block):
default="Scarlett",
)
credentials: CredentialsMetaInput[
Literal[ProviderName.UNREAL_SPEECH], Literal["api_key"]
Literal["unreal_speech"], Literal["api_key"]
] = CredentialsField(
provider="unreal_speech",
supported_credential_types={"api_key"},
description="The Unreal Speech integration can be used with "
"any API key with sufficient permissions for the blocks it is used on.",
)

View File

@@ -1,60 +0,0 @@
from typing import Literal
from pydantic import SecretStr
from backend.data.model import (
CredentialsField,
CredentialsMetaInput,
OAuth2Credentials,
ProviderName,
)
from backend.integrations.oauth.twitter import TwitterOAuthHandler
from backend.util.settings import Secrets
# --8<-- [start:TwitterOAuthIsConfigured]
secrets = Secrets()
TWITTER_OAUTH_IS_CONFIGURED = bool(
secrets.twitter_client_id and secrets.twitter_client_secret
)
# --8<-- [end:TwitterOAuthIsConfigured]
TwitterCredentials = OAuth2Credentials
TwitterCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.TWITTER], Literal["oauth2"]
]
# Currently, We are getting all the permission from the Twitter API initally
# In future, If we need to add incremental permission, we can use these requested_scopes
def TwitterCredentialsField(scopes: list[str]) -> TwitterCredentialsInput:
"""
Creates a Twitter credentials input on a block.
Params:
scopes: The authorization scopes needed for the block to work.
"""
return CredentialsField(
# required_scopes=set(scopes),
required_scopes=set(TwitterOAuthHandler.DEFAULT_SCOPES + scopes),
description="The Twitter integration requires OAuth2 authentication.",
)
TEST_CREDENTIALS = OAuth2Credentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="twitter",
access_token=SecretStr("mock-twitter-access-token"),
refresh_token=SecretStr("mock-twitter-refresh-token"),
access_token_expires_at=1234567890,
scopes=["tweet.read", "tweet.write", "users.read", "offline.access"],
title="Mock Twitter OAuth2 Credentials",
username="mock-twitter-username",
refresh_token_expires_at=1234567890,
)
TEST_CREDENTIALS_INPUT = {
"provider": TEST_CREDENTIALS.provider,
"id": TEST_CREDENTIALS.id,
"type": TEST_CREDENTIALS.type,
"title": TEST_CREDENTIALS.title,
}

View File

@@ -1,418 +0,0 @@
from datetime import datetime
from typing import Any, Dict
from backend.blocks.twitter._mappers import (
get_backend_expansion,
get_backend_field,
get_backend_list_expansion,
get_backend_list_field,
get_backend_media_field,
get_backend_place_field,
get_backend_poll_field,
get_backend_space_expansion,
get_backend_space_field,
get_backend_user_field,
)
from backend.blocks.twitter._types import ( # DMEventFieldFilter,
DMEventExpansionFilter,
DMEventTypeFilter,
DMMediaFieldFilter,
DMTweetFieldFilter,
ExpansionFilter,
ListExpansionsFilter,
ListFieldsFilter,
SpaceExpansionsFilter,
SpaceFieldsFilter,
TweetFieldsFilter,
TweetMediaFieldsFilter,
TweetPlaceFieldsFilter,
TweetPollFieldsFilter,
TweetReplySettingsFilter,
TweetUserFieldsFilter,
UserExpansionsFilter,
)
# Common Builder
class TweetExpansionsBuilder:
def __init__(self, param: Dict[str, Any]):
self.params: Dict[str, Any] = param
def add_expansions(self, expansions: ExpansionFilter | None):
if expansions:
filtered_expansions = [
name for name, value in expansions.dict().items() if value is True
]
if filtered_expansions:
self.params["expansions"] = ",".join(
[get_backend_expansion(exp) for exp in filtered_expansions]
)
return self
def add_media_fields(self, media_fields: TweetMediaFieldsFilter | None):
if media_fields:
filtered_fields = [
name for name, value in media_fields.dict().items() if value is True
]
if filtered_fields:
self.params["media.fields"] = ",".join(
[get_backend_media_field(field) for field in filtered_fields]
)
return self
def add_place_fields(self, place_fields: TweetPlaceFieldsFilter | None):
if place_fields:
filtered_fields = [
name for name, value in place_fields.dict().items() if value is True
]
if filtered_fields:
self.params["place.fields"] = ",".join(
[get_backend_place_field(field) for field in filtered_fields]
)
return self
def add_poll_fields(self, poll_fields: TweetPollFieldsFilter | None):
if poll_fields:
filtered_fields = [
name for name, value in poll_fields.dict().items() if value is True
]
if filtered_fields:
self.params["poll.fields"] = ",".join(
[get_backend_poll_field(field) for field in filtered_fields]
)
return self
def add_tweet_fields(self, tweet_fields: TweetFieldsFilter | None):
if tweet_fields:
filtered_fields = [
name for name, value in tweet_fields.dict().items() if value is True
]
if filtered_fields:
self.params["tweet.fields"] = ",".join(
[get_backend_field(field) for field in filtered_fields]
)
return self
def add_user_fields(self, user_fields: TweetUserFieldsFilter | None):
if user_fields:
filtered_fields = [
name for name, value in user_fields.dict().items() if value is True
]
if filtered_fields:
self.params["user.fields"] = ",".join(
[get_backend_user_field(field) for field in filtered_fields]
)
return self
def build(self):
return self.params
class UserExpansionsBuilder:
def __init__(self, param: Dict[str, Any]):
self.params: Dict[str, Any] = param
def add_expansions(self, expansions: UserExpansionsFilter | None):
if expansions:
filtered_expansions = [
name for name, value in expansions.dict().items() if value is True
]
if filtered_expansions:
self.params["expansions"] = ",".join(filtered_expansions)
return self
def add_tweet_fields(self, tweet_fields: TweetFieldsFilter | None):
if tweet_fields:
filtered_fields = [
name for name, value in tweet_fields.dict().items() if value is True
]
if filtered_fields:
self.params["tweet.fields"] = ",".join(
[get_backend_field(field) for field in filtered_fields]
)
return self
def add_user_fields(self, user_fields: TweetUserFieldsFilter | None):
if user_fields:
filtered_fields = [
name for name, value in user_fields.dict().items() if value is True
]
if filtered_fields:
self.params["user.fields"] = ",".join(
[get_backend_user_field(field) for field in filtered_fields]
)
return self
def build(self):
return self.params
class ListExpansionsBuilder:
def __init__(self, param: Dict[str, Any]):
self.params: Dict[str, Any] = param
def add_expansions(self, expansions: ListExpansionsFilter | None):
if expansions:
filtered_expansions = [
name for name, value in expansions.dict().items() if value is True
]
if filtered_expansions:
self.params["expansions"] = ",".join(
[get_backend_list_expansion(exp) for exp in filtered_expansions]
)
return self
def add_list_fields(self, list_fields: ListFieldsFilter | None):
if list_fields:
filtered_fields = [
name for name, value in list_fields.dict().items() if value is True
]
if filtered_fields:
self.params["list.fields"] = ",".join(
[get_backend_list_field(field) for field in filtered_fields]
)
return self
def add_user_fields(self, user_fields: TweetUserFieldsFilter | None):
if user_fields:
filtered_fields = [
name for name, value in user_fields.dict().items() if value is True
]
if filtered_fields:
self.params["user.fields"] = ",".join(
[get_backend_user_field(field) for field in filtered_fields]
)
return self
def build(self):
return self.params
class SpaceExpansionsBuilder:
def __init__(self, param: Dict[str, Any]):
self.params: Dict[str, Any] = param
def add_expansions(self, expansions: SpaceExpansionsFilter | None):
if expansions:
filtered_expansions = [
name for name, value in expansions.dict().items() if value is True
]
if filtered_expansions:
self.params["expansions"] = ",".join(
[get_backend_space_expansion(exp) for exp in filtered_expansions]
)
return self
def add_space_fields(self, space_fields: SpaceFieldsFilter | None):
if space_fields:
filtered_fields = [
name for name, value in space_fields.dict().items() if value is True
]
if filtered_fields:
self.params["space.fields"] = ",".join(
[get_backend_space_field(field) for field in filtered_fields]
)
return self
def add_user_fields(self, user_fields: TweetUserFieldsFilter | None):
if user_fields:
filtered_fields = [
name for name, value in user_fields.dict().items() if value is True
]
if filtered_fields:
self.params["user.fields"] = ",".join(
[get_backend_user_field(field) for field in filtered_fields]
)
return self
def build(self):
return self.params
class TweetDurationBuilder:
def __init__(self, param: Dict[str, Any]):
self.params: Dict[str, Any] = param
def add_start_time(self, start_time: datetime | None):
if start_time:
self.params["start_time"] = start_time
return self
def add_end_time(self, end_time: datetime | None):
if end_time:
self.params["end_time"] = end_time
return self
def add_since_id(self, since_id: str | None):
if since_id:
self.params["since_id"] = since_id
return self
def add_until_id(self, until_id: str | None):
if until_id:
self.params["until_id"] = until_id
return self
def add_sort_order(self, sort_order: str | None):
if sort_order:
self.params["sort_order"] = sort_order
return self
def build(self):
return self.params
class DMExpansionsBuilder:
def __init__(self, param: Dict[str, Any]):
self.params: Dict[str, Any] = param
def add_expansions(self, expansions: DMEventExpansionFilter):
if expansions:
filtered_expansions = [
name for name, value in expansions.dict().items() if value is True
]
if filtered_expansions:
self.params["expansions"] = ",".join(filtered_expansions)
return self
def add_event_types(self, event_types: DMEventTypeFilter):
if event_types:
filtered_types = [
name for name, value in event_types.dict().items() if value is True
]
if filtered_types:
self.params["event_types"] = ",".join(filtered_types)
return self
def add_media_fields(self, media_fields: DMMediaFieldFilter):
if media_fields:
filtered_fields = [
name for name, value in media_fields.dict().items() if value is True
]
if filtered_fields:
self.params["media.fields"] = ",".join(filtered_fields)
return self
def add_tweet_fields(self, tweet_fields: DMTweetFieldFilter):
if tweet_fields:
filtered_fields = [
name for name, value in tweet_fields.dict().items() if value is True
]
if filtered_fields:
self.params["tweet.fields"] = ",".join(filtered_fields)
return self
def add_user_fields(self, user_fields: TweetUserFieldsFilter):
if user_fields:
filtered_fields = [
name for name, value in user_fields.dict().items() if value is True
]
if filtered_fields:
self.params["user.fields"] = ",".join(filtered_fields)
return self
def build(self):
return self.params
# Specific Builders
class TweetSearchBuilder:
def __init__(self):
self.params: Dict[str, Any] = {"user_auth": False}
def add_query(self, query: str):
if query:
self.params["query"] = query
return self
def add_pagination(self, max_results: int, pagination: str | None):
if max_results:
self.params["max_results"] = max_results
if pagination:
self.params["pagination_token"] = pagination
return self
def build(self):
return self.params
class TweetPostBuilder:
def __init__(self):
self.params: Dict[str, Any] = {"user_auth": False}
def add_text(self, text: str | None):
if text:
self.params["text"] = text
return self
def add_media(self, media_ids: list, tagged_user_ids: list):
if media_ids:
self.params["media_ids"] = media_ids
if tagged_user_ids:
self.params["media_tagged_user_ids"] = tagged_user_ids
return self
def add_deep_link(self, link: str):
if link:
self.params["direct_message_deep_link"] = link
return self
def add_super_followers(self, for_super_followers: bool):
if for_super_followers:
self.params["for_super_followers_only"] = for_super_followers
return self
def add_place(self, place_id: str):
if place_id:
self.params["place_id"] = place_id
return self
def add_poll_options(self, poll_options: list):
if poll_options:
self.params["poll_options"] = poll_options
return self
def add_poll_duration(self, poll_duration_minutes: int):
if poll_duration_minutes:
self.params["poll_duration_minutes"] = poll_duration_minutes
return self
def add_quote(self, quote_id: str):
if quote_id:
self.params["quote_tweet_id"] = quote_id
return self
def add_reply_settings(
self,
exclude_user_ids: list,
reply_to_id: str,
settings: TweetReplySettingsFilter,
):
if exclude_user_ids:
self.params["exclude_reply_user_ids"] = exclude_user_ids
if reply_to_id:
self.params["in_reply_to_tweet_id"] = reply_to_id
if settings.All_Users:
self.params["reply_settings"] = None
elif settings.Following_Users_Only:
self.params["reply_settings"] = "following"
elif settings.Mentioned_Users_Only:
self.params["reply_settings"] = "mentionedUsers"
return self
def build(self):
return self.params
class TweetGetsBuilder:
def __init__(self):
self.params: Dict[str, Any] = {"user_auth": False}
def add_id(self, tweet_id: list[str]):
self.params["id"] = tweet_id
return self
def build(self):
return self.params

View File

@@ -1,234 +0,0 @@
# -------------- Tweets -----------------
# Tweet Expansions
EXPANSION_FRONTEND_TO_BACKEND_MAPPING = {
"Poll_IDs": "attachments.poll_ids",
"Media_Keys": "attachments.media_keys",
"Author_User_ID": "author_id",
"Edit_History_Tweet_IDs": "edit_history_tweet_ids",
"Mentioned_Usernames": "entities.mentions.username",
"Place_ID": "geo.place_id",
"Reply_To_User_ID": "in_reply_to_user_id",
"Referenced_Tweet_ID": "referenced_tweets.id",
"Referenced_Tweet_Author_ID": "referenced_tweets.id.author_id",
}
def get_backend_expansion(frontend_key: str) -> str:
result = EXPANSION_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid expansion key: {frontend_key}")
return result
# TweetReplySettings
REPLY_SETTINGS_FRONTEND_TO_BACKEND_MAPPING = {
"Mentioned_Users_Only": "mentionedUsers",
"Following_Users_Only": "following",
"All_Users": "all",
}
# TweetUserFields
def get_backend_reply_setting(frontend_key: str) -> str:
result = REPLY_SETTINGS_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid reply setting key: {frontend_key}")
return result
USER_FIELDS_FRONTEND_TO_BACKEND_MAPPING = {
"Account_Creation_Date": "created_at",
"User_Bio": "description",
"User_Entities": "entities",
"User_ID": "id",
"User_Location": "location",
"Latest_Tweet_ID": "most_recent_tweet_id",
"Display_Name": "name",
"Pinned_Tweet_ID": "pinned_tweet_id",
"Profile_Picture_URL": "profile_image_url",
"Is_Protected_Account": "protected",
"Account_Statistics": "public_metrics",
"Profile_URL": "url",
"Username": "username",
"Is_Verified": "verified",
"Verification_Type": "verified_type",
"Content_Withholding_Info": "withheld",
}
def get_backend_user_field(frontend_key: str) -> str:
result = USER_FIELDS_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid user field key: {frontend_key}")
return result
# TweetFields
FIELDS_FRONTEND_TO_BACKEND_MAPPING = {
"Tweet_Attachments": "attachments",
"Author_ID": "author_id",
"Context_Annotations": "context_annotations",
"Conversation_ID": "conversation_id",
"Creation_Time": "created_at",
"Edit_Controls": "edit_controls",
"Tweet_Entities": "entities",
"Geographic_Location": "geo",
"Tweet_ID": "id",
"Reply_To_User_ID": "in_reply_to_user_id",
"Language": "lang",
"Public_Metrics": "public_metrics",
"Sensitive_Content_Flag": "possibly_sensitive",
"Referenced_Tweets": "referenced_tweets",
"Reply_Settings": "reply_settings",
"Tweet_Source": "source",
"Tweet_Text": "text",
"Withheld_Content": "withheld",
}
def get_backend_field(frontend_key: str) -> str:
result = FIELDS_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid field key: {frontend_key}")
return result
# TweetPollFields
POLL_FIELDS_FRONTEND_TO_BACKEND_MAPPING = {
"Duration_Minutes": "duration_minutes",
"End_DateTime": "end_datetime",
"Poll_ID": "id",
"Poll_Options": "options",
"Voting_Status": "voting_status",
}
def get_backend_poll_field(frontend_key: str) -> str:
result = POLL_FIELDS_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid poll field key: {frontend_key}")
return result
PLACE_FIELDS_FRONTEND_TO_BACKEND_MAPPING = {
"Contained_Within_Places": "contained_within",
"Country": "country",
"Country_Code": "country_code",
"Full_Location_Name": "full_name",
"Geographic_Coordinates": "geo",
"Place_ID": "id",
"Place_Name": "name",
"Place_Type": "place_type",
}
def get_backend_place_field(frontend_key: str) -> str:
result = PLACE_FIELDS_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid place field key: {frontend_key}")
return result
# TweetMediaFields
MEDIA_FIELDS_FRONTEND_TO_BACKEND_MAPPING = {
"Duration_in_Milliseconds": "duration_ms",
"Height": "height",
"Media_Key": "media_key",
"Preview_Image_URL": "preview_image_url",
"Media_Type": "type",
"Media_URL": "url",
"Width": "width",
"Public_Metrics": "public_metrics",
"Non_Public_Metrics": "non_public_metrics",
"Organic_Metrics": "organic_metrics",
"Promoted_Metrics": "promoted_metrics",
"Alternative_Text": "alt_text",
"Media_Variants": "variants",
}
def get_backend_media_field(frontend_key: str) -> str:
result = MEDIA_FIELDS_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid media field key: {frontend_key}")
return result
# -------------- Spaces -----------------
# SpaceExpansions
EXPANSION_FRONTEND_TO_BACKEND_MAPPING_SPACE = {
"Invited_Users": "invited_user_ids",
"Speakers": "speaker_ids",
"Creator": "creator_id",
"Hosts": "host_ids",
"Topics": "topic_ids",
}
def get_backend_space_expansion(frontend_key: str) -> str:
result = EXPANSION_FRONTEND_TO_BACKEND_MAPPING_SPACE.get(frontend_key)
if result is None:
raise KeyError(f"Invalid expansion key: {frontend_key}")
return result
# SpaceFields
SPACE_FIELDS_FRONTEND_TO_BACKEND_MAPPING = {
"Space_ID": "id",
"Space_State": "state",
"Creation_Time": "created_at",
"End_Time": "ended_at",
"Host_User_IDs": "host_ids",
"Language": "lang",
"Is_Ticketed": "is_ticketed",
"Invited_User_IDs": "invited_user_ids",
"Participant_Count": "participant_count",
"Subscriber_Count": "subscriber_count",
"Scheduled_Start_Time": "scheduled_start",
"Speaker_User_IDs": "speaker_ids",
"Start_Time": "started_at",
"Space_Title": "title",
"Topic_IDs": "topic_ids",
"Last_Updated_Time": "updated_at",
}
def get_backend_space_field(frontend_key: str) -> str:
result = SPACE_FIELDS_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid space field key: {frontend_key}")
return result
# -------------- List Expansions -----------------
# ListExpansions
LIST_EXPANSION_FRONTEND_TO_BACKEND_MAPPING = {"List_Owner_ID": "owner_id"}
def get_backend_list_expansion(frontend_key: str) -> str:
result = LIST_EXPANSION_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid list expansion key: {frontend_key}")
return result
LIST_FIELDS_FRONTEND_TO_BACKEND_MAPPING = {
"List_ID": "id",
"List_Name": "name",
"Creation_Date": "created_at",
"Description": "description",
"Follower_Count": "follower_count",
"Member_Count": "member_count",
"Is_Private": "private",
"Owner_ID": "owner_id",
}
def get_backend_list_field(frontend_key: str) -> str:
result = LIST_FIELDS_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid list field key: {frontend_key}")
return result

View File

@@ -1,76 +0,0 @@
from typing import Any, Dict, List
class BaseSerializer:
@staticmethod
def _serialize_value(value: Any) -> Any:
"""Helper method to serialize individual values"""
if hasattr(value, "data"):
return value.data
return value
class IncludesSerializer(BaseSerializer):
@classmethod
def serialize(cls, includes: Dict[str, Any]) -> Dict[str, Any]:
"""Serializes the includes dictionary"""
if not includes:
return {}
serialized_includes = {}
for key, value in includes.items():
if isinstance(value, list):
serialized_includes[key] = [
cls._serialize_value(item) for item in value
]
else:
serialized_includes[key] = cls._serialize_value(value)
return serialized_includes
class ResponseDataSerializer(BaseSerializer):
@classmethod
def serialize_dict(cls, item: Dict[str, Any]) -> Dict[str, Any]:
"""Serializes a single dictionary item"""
serialized_item = {}
if hasattr(item, "__dict__"):
items = item.__dict__.items()
else:
items = item.items()
for key, value in items:
if isinstance(value, list):
serialized_item[key] = [
cls._serialize_value(sub_item) for sub_item in value
]
else:
serialized_item[key] = cls._serialize_value(value)
return serialized_item
@classmethod
def serialize_list(cls, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Serializes a list of dictionary items"""
return [cls.serialize_dict(item) for item in data]
class ResponseSerializer:
@classmethod
def serialize(cls, response) -> Dict[str, Any]:
"""Main serializer that handles both data and includes"""
result = {"data": None, "included": {}}
# Handle response.data
if response.data:
if isinstance(response.data, list):
result["data"] = ResponseDataSerializer.serialize_list(response.data)
else:
result["data"] = ResponseDataSerializer.serialize_dict(response.data)
# Handle includes
if hasattr(response, "includes") and response.includes:
result["included"] = IncludesSerializer.serialize(response.includes)
return result

View File

@@ -1,443 +0,0 @@
from datetime import datetime
from enum import Enum
from pydantic import BaseModel
from backend.data.block import BlockSchema
from backend.data.model import SchemaField
# -------------- Tweets -----------------
class TweetReplySettingsFilter(BaseModel):
Mentioned_Users_Only: bool = False
Following_Users_Only: bool = False
All_Users: bool = False
class TweetUserFieldsFilter(BaseModel):
Account_Creation_Date: bool = False
User_Bio: bool = False
User_Entities: bool = False
User_ID: bool = False
User_Location: bool = False
Latest_Tweet_ID: bool = False
Display_Name: bool = False
Pinned_Tweet_ID: bool = False
Profile_Picture_URL: bool = False
Is_Protected_Account: bool = False
Account_Statistics: bool = False
Profile_URL: bool = False
Username: bool = False
Is_Verified: bool = False
Verification_Type: bool = False
Content_Withholding_Info: bool = False
class TweetFieldsFilter(BaseModel):
Tweet_Attachments: bool = False
Author_ID: bool = False
Context_Annotations: bool = False
Conversation_ID: bool = False
Creation_Time: bool = False
Edit_Controls: bool = False
Tweet_Entities: bool = False
Geographic_Location: bool = False
Tweet_ID: bool = False
Reply_To_User_ID: bool = False
Language: bool = False
Public_Metrics: bool = False
Sensitive_Content_Flag: bool = False
Referenced_Tweets: bool = False
Reply_Settings: bool = False
Tweet_Source: bool = False
Tweet_Text: bool = False
Withheld_Content: bool = False
class PersonalTweetFieldsFilter(BaseModel):
attachments: bool = False
author_id: bool = False
context_annotations: bool = False
conversation_id: bool = False
created_at: bool = False
edit_controls: bool = False
entities: bool = False
geo: bool = False
id: bool = False
in_reply_to_user_id: bool = False
lang: bool = False
non_public_metrics: bool = False
public_metrics: bool = False
organic_metrics: bool = False
promoted_metrics: bool = False
possibly_sensitive: bool = False
referenced_tweets: bool = False
reply_settings: bool = False
source: bool = False
text: bool = False
withheld: bool = False
class TweetPollFieldsFilter(BaseModel):
Duration_Minutes: bool = False
End_DateTime: bool = False
Poll_ID: bool = False
Poll_Options: bool = False
Voting_Status: bool = False
class TweetPlaceFieldsFilter(BaseModel):
Contained_Within_Places: bool = False
Country: bool = False
Country_Code: bool = False
Full_Location_Name: bool = False
Geographic_Coordinates: bool = False
Place_ID: bool = False
Place_Name: bool = False
Place_Type: bool = False
class TweetMediaFieldsFilter(BaseModel):
Duration_in_Milliseconds: bool = False
Height: bool = False
Media_Key: bool = False
Preview_Image_URL: bool = False
Media_Type: bool = False
Media_URL: bool = False
Width: bool = False
Public_Metrics: bool = False
Non_Public_Metrics: bool = False
Organic_Metrics: bool = False
Promoted_Metrics: bool = False
Alternative_Text: bool = False
Media_Variants: bool = False
class ExpansionFilter(BaseModel):
Poll_IDs: bool = False
Media_Keys: bool = False
Author_User_ID: bool = False
Edit_History_Tweet_IDs: bool = False
Mentioned_Usernames: bool = False
Place_ID: bool = False
Reply_To_User_ID: bool = False
Referenced_Tweet_ID: bool = False
Referenced_Tweet_Author_ID: bool = False
class TweetExcludesFilter(BaseModel):
retweets: bool = False
replies: bool = False
# -------------- Users -----------------
class UserExpansionsFilter(BaseModel):
pinned_tweet_id: bool = False
# -------------- DM's' -----------------
class DMEventFieldFilter(BaseModel):
id: bool = False
text: bool = False
event_type: bool = False
created_at: bool = False
dm_conversation_id: bool = False
sender_id: bool = False
participant_ids: bool = False
referenced_tweets: bool = False
attachments: bool = False
class DMEventTypeFilter(BaseModel):
MessageCreate: bool = False
ParticipantsJoin: bool = False
ParticipantsLeave: bool = False
class DMEventExpansionFilter(BaseModel):
attachments_media_keys: bool = False
referenced_tweets_id: bool = False
sender_id: bool = False
participant_ids: bool = False
class DMMediaFieldFilter(BaseModel):
duration_ms: bool = False
height: bool = False
media_key: bool = False
preview_image_url: bool = False
type: bool = False
url: bool = False
width: bool = False
public_metrics: bool = False
alt_text: bool = False
variants: bool = False
class DMTweetFieldFilter(BaseModel):
attachments: bool = False
author_id: bool = False
context_annotations: bool = False
conversation_id: bool = False
created_at: bool = False
edit_controls: bool = False
entities: bool = False
geo: bool = False
id: bool = False
in_reply_to_user_id: bool = False
lang: bool = False
public_metrics: bool = False
possibly_sensitive: bool = False
referenced_tweets: bool = False
reply_settings: bool = False
source: bool = False
text: bool = False
withheld: bool = False
# -------------- Spaces -----------------
class SpaceExpansionsFilter(BaseModel):
Invited_Users: bool = False
Speakers: bool = False
Creator: bool = False
Hosts: bool = False
Topics: bool = False
class SpaceFieldsFilter(BaseModel):
Space_ID: bool = False
Space_State: bool = False
Creation_Time: bool = False
End_Time: bool = False
Host_User_IDs: bool = False
Language: bool = False
Is_Ticketed: bool = False
Invited_User_IDs: bool = False
Participant_Count: bool = False
Subscriber_Count: bool = False
Scheduled_Start_Time: bool = False
Speaker_User_IDs: bool = False
Start_Time: bool = False
Space_Title: bool = False
Topic_IDs: bool = False
Last_Updated_Time: bool = False
class SpaceStatesFilter(str, Enum):
live = "live"
scheduled = "scheduled"
all = "all"
# -------------- List Expansions -----------------
class ListExpansionsFilter(BaseModel):
List_Owner_ID: bool = False
class ListFieldsFilter(BaseModel):
List_ID: bool = False
List_Name: bool = False
Creation_Date: bool = False
Description: bool = False
Follower_Count: bool = False
Member_Count: bool = False
Is_Private: bool = False
Owner_ID: bool = False
# --------- [Input Types] -------------
class TweetExpansionInputs(BlockSchema):
expansions: ExpansionFilter | None = SchemaField(
description="Choose what extra information you want to get with your tweets. For example:\n- Select 'Media_Keys' to get media details\n- Select 'Author_User_ID' to get user information\n- Select 'Place_ID' to get location details",
placeholder="Pick the extra information you want to see",
default=None,
advanced=True,
)
media_fields: TweetMediaFieldsFilter | None = SchemaField(
description="Select what media information you want to see (images, videos, etc). To use this, you must first select 'Media_Keys' in the expansions above.",
placeholder="Choose what media details you want to see",
default=None,
advanced=True,
)
place_fields: TweetPlaceFieldsFilter | None = SchemaField(
description="Select what location information you want to see (country, coordinates, etc). To use this, you must first select 'Place_ID' in the expansions above.",
placeholder="Choose what location details you want to see",
default=None,
advanced=True,
)
poll_fields: TweetPollFieldsFilter | None = SchemaField(
description="Select what poll information you want to see (options, voting status, etc). To use this, you must first select 'Poll_IDs' in the expansions above.",
placeholder="Choose what poll details you want to see",
default=None,
advanced=True,
)
tweet_fields: TweetFieldsFilter | None = SchemaField(
description="Select what tweet information you want to see. For referenced tweets (like retweets), select 'Referenced_Tweet_ID' in the expansions above.",
placeholder="Choose what tweet details you want to see",
default=None,
advanced=True,
)
user_fields: TweetUserFieldsFilter | None = SchemaField(
description="Select what user information you want to see. To use this, you must first select one of these in expansions above:\n- 'Author_User_ID' for tweet authors\n- 'Mentioned_Usernames' for mentioned users\n- 'Reply_To_User_ID' for users being replied to\n- 'Referenced_Tweet_Author_ID' for authors of referenced tweets",
placeholder="Choose what user details you want to see",
default=None,
advanced=True,
)
class DMEventExpansionInputs(BlockSchema):
expansions: DMEventExpansionFilter | None = SchemaField(
description="Select expansions to include related data objects in the 'includes' section.",
placeholder="Enter expansions",
default=None,
advanced=True,
)
event_types: DMEventTypeFilter | None = SchemaField(
description="Select DM event types to include in the response.",
placeholder="Enter event types",
default=None,
advanced=True,
)
media_fields: DMMediaFieldFilter | None = SchemaField(
description="Select media fields to include in the response (requires expansions=attachments.media_keys).",
placeholder="Enter media fields",
default=None,
advanced=True,
)
tweet_fields: DMTweetFieldFilter | None = SchemaField(
description="Select tweet fields to include in the response (requires expansions=referenced_tweets.id).",
placeholder="Enter tweet fields",
default=None,
advanced=True,
)
user_fields: TweetUserFieldsFilter | None = SchemaField(
description="Select user fields to include in the response (requires expansions=sender_id or participant_ids).",
placeholder="Enter user fields",
default=None,
advanced=True,
)
class UserExpansionInputs(BlockSchema):
expansions: UserExpansionsFilter | None = SchemaField(
description="Choose what extra information you want to get with user data. Currently only 'pinned_tweet_id' is available to see a user's pinned tweet.",
placeholder="Select extra user information to include",
default=None,
advanced=True,
)
tweet_fields: TweetFieldsFilter | None = SchemaField(
description="Select what tweet information you want to see in pinned tweets. This only works if you select 'pinned_tweet_id' in expansions above.",
placeholder="Choose what details to see in pinned tweets",
default=None,
advanced=True,
)
user_fields: TweetUserFieldsFilter | None = SchemaField(
description="Select what user information you want to see, like username, bio, profile picture, etc.",
placeholder="Choose what user details you want to see",
default=None,
advanced=True,
)
class SpaceExpansionInputs(BlockSchema):
expansions: SpaceExpansionsFilter | None = SchemaField(
description="Choose additional information you want to get with your Twitter Spaces:\n- Select 'Invited_Users' to see who was invited\n- Select 'Speakers' to see who can speak\n- Select 'Creator' to get details about who made the Space\n- Select 'Hosts' to see who's hosting\n- Select 'Topics' to see Space topics",
placeholder="Pick what extra information you want to see about the Space",
default=None,
advanced=True,
)
space_fields: SpaceFieldsFilter | None = SchemaField(
description="Choose what Space details you want to see, such as:\n- Title\n- Start/End times\n- Number of participants\n- Language\n- State (live/scheduled)\n- And more",
placeholder="Choose what Space information you want to get",
default=SpaceFieldsFilter(Space_Title=True, Host_User_IDs=True),
advanced=True,
)
user_fields: TweetUserFieldsFilter | None = SchemaField(
description="Choose what user information you want to see. This works when you select any of these in expansions above:\n- 'Creator' for Space creator details\n- 'Hosts' for host information\n- 'Speakers' for speaker details\n- 'Invited_Users' for invited user information",
placeholder="Pick what details you want to see about the users",
default=None,
advanced=True,
)
class ListExpansionInputs(BlockSchema):
expansions: ListExpansionsFilter | None = SchemaField(
description="Choose what extra information you want to get with your Twitter Lists:\n- Select 'List_Owner_ID' to get details about who owns the list\n\nThis will let you see more details about the list owner when you also select user fields below.",
placeholder="Pick what extra list information you want to see",
default=ListExpansionsFilter(List_Owner_ID=True),
advanced=True,
)
user_fields: TweetUserFieldsFilter | None = SchemaField(
description="Choose what information you want to see about list owners. This only works when you select 'List_Owner_ID' in expansions above.\n\nYou can see things like:\n- Their username\n- Profile picture\n- Account details\n- And more",
placeholder="Select what details you want to see about list owners",
default=TweetUserFieldsFilter(User_ID=True, Username=True),
advanced=True,
)
list_fields: ListFieldsFilter | None = SchemaField(
description="Choose what information you want to see about the Twitter Lists themselves, such as:\n- List name\n- Description\n- Number of followers\n- Number of members\n- Whether it's private\n- Creation date\n- And more",
placeholder="Pick what list details you want to see",
default=ListFieldsFilter(Owner_ID=True),
advanced=True,
)
class TweetTimeWindowInputs(BlockSchema):
start_time: datetime | None = SchemaField(
description="Start time in YYYY-MM-DDTHH:mm:ssZ format",
placeholder="Enter start time",
default=None,
advanced=False,
)
end_time: datetime | None = SchemaField(
description="End time in YYYY-MM-DDTHH:mm:ssZ format",
placeholder="Enter end time",
default=None,
advanced=False,
)
since_id: str | None = SchemaField(
description="Returns results with Tweet ID greater than this (more recent than), we give priority to since_id over start_time",
placeholder="Enter since ID",
default=None,
advanced=True,
)
until_id: str | None = SchemaField(
description="Returns results with Tweet ID less than this (that is, older than), and used with since_id",
placeholder="Enter until ID",
default=None,
advanced=True,
)
sort_order: str | None = SchemaField(
description="Order of returned tweets (recency or relevancy)",
placeholder="Enter sort order",
default=None,
advanced=True,
)

View File

@@ -1,201 +0,0 @@
# Todo : Add new Type support
# from typing import cast
# import tweepy
# from tweepy.client import Response
# from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
# from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
# from backend.data.model import SchemaField
# from backend.blocks.twitter._builders import DMExpansionsBuilder
# from backend.blocks.twitter._types import DMEventExpansion, DMEventExpansionInputs, DMEventType, DMMediaField, DMTweetField, TweetUserFields
# from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
# from backend.blocks.twitter._auth import (
# TEST_CREDENTIALS,
# TEST_CREDENTIALS_INPUT,
# TwitterCredentials,
# TwitterCredentialsField,
# TwitterCredentialsInput,
# )
# Require Pro or Enterprise plan [Manual Testing Required]
# class TwitterGetDMEventsBlock(Block):
# """
# Gets a list of Direct Message events for the authenticated user
# """
# class Input(DMEventExpansionInputs):
# credentials: TwitterCredentialsInput = TwitterCredentialsField(
# ["dm.read", "offline.access", "user.read", "tweet.read"]
# )
# dm_conversation_id: str = SchemaField(
# description="The ID of the Direct Message conversation",
# placeholder="Enter conversation ID",
# required=True
# )
# max_results: int = SchemaField(
# description="Maximum number of results to return (1-100)",
# placeholder="Enter max results",
# advanced=True,
# default=10,
# )
# pagination_token: str = SchemaField(
# description="Token for pagination",
# placeholder="Enter pagination token",
# advanced=True,
# default=""
# )
# class Output(BlockSchema):
# # Common outputs
# event_ids: list[str] = SchemaField(description="DM Event IDs")
# event_texts: list[str] = SchemaField(description="DM Event text contents")
# event_types: list[str] = SchemaField(description="Types of DM events")
# next_token: str = SchemaField(description="Token for next page of results")
# # Complete outputs
# data: list[dict] = SchemaField(description="Complete DM events data")
# included: dict = SchemaField(description="Additional data requested via expansions")
# meta: dict = SchemaField(description="Metadata about the response")
# error: str = SchemaField(description="Error message if request failed")
# def __init__(self):
# super().__init__(
# id="dc37a6d4-a62e-11ef-a3a5-03061375737b",
# description="This block retrieves Direct Message events for the authenticated user.",
# categories={BlockCategory.SOCIAL},
# input_schema=TwitterGetDMEventsBlock.Input,
# output_schema=TwitterGetDMEventsBlock.Output,
# test_input={
# "dm_conversation_id": "1234567890",
# "max_results": 10,
# "credentials": TEST_CREDENTIALS_INPUT,
# "expansions": [],
# "event_types": [],
# "media_fields": [],
# "tweet_fields": [],
# "user_fields": []
# },
# test_credentials=TEST_CREDENTIALS,
# test_output=[
# ("event_ids", ["1346889436626259968"]),
# ("event_texts", ["Hello just you..."]),
# ("event_types", ["MessageCreate"]),
# ("next_token", None),
# ("data", [{"id": "1346889436626259968", "text": "Hello just you...", "event_type": "MessageCreate"}]),
# ("included", {}),
# ("meta", {}),
# ("error", "")
# ],
# test_mock={
# "get_dm_events": lambda *args, **kwargs: (
# [{"id": "1346889436626259968", "text": "Hello just you...", "event_type": "MessageCreate"}],
# {},
# {},
# ["1346889436626259968"],
# ["Hello just you..."],
# ["MessageCreate"],
# None
# )
# }
# )
# @staticmethod
# def get_dm_events(
# credentials: TwitterCredentials,
# dm_conversation_id: str,
# max_results: int,
# pagination_token: str,
# expansions: list[DMEventExpansion],
# event_types: list[DMEventType],
# media_fields: list[DMMediaField],
# tweet_fields: list[DMTweetField],
# user_fields: list[TweetUserFields]
# ):
# try:
# client = tweepy.Client(
# bearer_token=credentials.access_token.get_secret_value()
# )
# params = {
# "dm_conversation_id": dm_conversation_id,
# "max_results": max_results,
# "pagination_token": None if pagination_token == "" else pagination_token,
# "user_auth": False
# }
# params = (DMExpansionsBuilder(params)
# .add_expansions(expansions)
# .add_event_types(event_types)
# .add_media_fields(media_fields)
# .add_tweet_fields(tweet_fields)
# .add_user_fields(user_fields)
# .build())
# response = cast(Response, client.get_direct_message_events(**params))
# meta = {}
# event_ids = []
# event_texts = []
# event_types = []
# next_token = None
# if response.meta:
# meta = response.meta
# next_token = meta.get("next_token")
# included = IncludesSerializer.serialize(response.includes)
# data = ResponseDataSerializer.serialize_list(response.data)
# if response.data:
# event_ids = [str(item.id) for item in response.data]
# event_texts = [item.text if hasattr(item, "text") else None for item in response.data]
# event_types = [item.event_type for item in response.data]
# return data, included, meta, event_ids, event_texts, event_types, next_token
# raise Exception("No DM events found")
# except tweepy.TweepyException:
# raise
# def run(
# self,
# input_data: Input,
# *,
# credentials: TwitterCredentials,
# **kwargs,
# ) -> BlockOutput:
# try:
# event_data, included, meta, event_ids, event_texts, event_types, next_token = self.get_dm_events(
# credentials,
# input_data.dm_conversation_id,
# input_data.max_results,
# input_data.pagination_token,
# input_data.expansions,
# input_data.event_types,
# input_data.media_fields,
# input_data.tweet_fields,
# input_data.user_fields
# )
# if event_ids:
# yield "event_ids", event_ids
# if event_texts:
# yield "event_texts", event_texts
# if event_types:
# yield "event_types", event_types
# if next_token:
# yield "next_token", next_token
# if event_data:
# yield "data", event_data
# if included:
# yield "included", included
# if meta:
# yield "meta", meta
# except Exception as e:
# yield "error", handle_tweepy_exception(e)

View File

@@ -1,260 +0,0 @@
# Todo : Add new Type support
# from typing import cast
# import tweepy
# from tweepy.client import Response
# from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
# from backend.data.model import SchemaField
# from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
# from backend.blocks.twitter._auth import (
# TEST_CREDENTIALS,
# TEST_CREDENTIALS_INPUT,
# TwitterCredentials,
# TwitterCredentialsField,
# TwitterCredentialsInput,
# )
# Pro and Enterprise plan [Manual Testing Required]
# class TwitterSendDirectMessageBlock(Block):
# """
# Sends a direct message to a Twitter user
# """
# class Input(BlockSchema):
# credentials: TwitterCredentialsInput = TwitterCredentialsField(
# ["offline.access", "direct_messages.write"]
# )
# participant_id: str = SchemaField(
# description="The User ID of the account to send DM to",
# placeholder="Enter recipient user ID",
# default="",
# advanced=False
# )
# dm_conversation_id: str = SchemaField(
# description="The conversation ID to send message to",
# placeholder="Enter conversation ID",
# default="",
# advanced=False
# )
# text: str = SchemaField(
# description="Text of the Direct Message (up to 10,000 characters)",
# placeholder="Enter message text",
# default="",
# advanced=False
# )
# media_id: str = SchemaField(
# description="Media ID to attach to the message",
# placeholder="Enter media ID",
# default=""
# )
# class Output(BlockSchema):
# dm_event_id: str = SchemaField(description="ID of the sent direct message")
# dm_conversation_id_: str = SchemaField(description="ID of the conversation")
# error: str = SchemaField(description="Error message if sending failed")
# def __init__(self):
# super().__init__(
# id="f32f2786-a62e-11ef-a93d-a3ef199dde7f",
# description="This block sends a direct message to a specified Twitter user.",
# categories={BlockCategory.SOCIAL},
# input_schema=TwitterSendDirectMessageBlock.Input,
# output_schema=TwitterSendDirectMessageBlock.Output,
# test_input={
# "participant_id": "783214",
# "dm_conversation_id": "",
# "text": "Hello from Twitter API",
# "media_id": "",
# "credentials": TEST_CREDENTIALS_INPUT
# },
# test_credentials=TEST_CREDENTIALS,
# test_output=[
# ("dm_event_id", "0987654321"),
# ("dm_conversation_id_", "1234567890"),
# ("error", "")
# ],
# test_mock={
# "send_direct_message": lambda *args, **kwargs: (
# "0987654321",
# "1234567890"
# )
# },
# )
# @staticmethod
# def send_direct_message(
# credentials: TwitterCredentials,
# participant_id: str,
# dm_conversation_id: str,
# text: str,
# media_id: str
# ):
# try:
# client = tweepy.Client(
# bearer_token=credentials.access_token.get_secret_value()
# )
# response = cast(
# Response,
# client.create_direct_message(
# participant_id=None if participant_id == "" else participant_id,
# dm_conversation_id=None if dm_conversation_id == "" else dm_conversation_id,
# text=None if text == "" else text,
# media_id=None if media_id == "" else media_id,
# user_auth=False
# )
# )
# if not response.data:
# raise Exception("Failed to send direct message")
# return response.data["dm_event_id"], response.data["dm_conversation_id"]
# except tweepy.TweepyException:
# raise
# except Exception as e:
# print(f"Unexpected error: {str(e)}")
# raise
# def run(
# self,
# input_data: Input,
# *,
# credentials: TwitterCredentials,
# **kwargs,
# ) -> BlockOutput:
# try:
# dm_event_id, dm_conversation_id = self.send_direct_message(
# credentials,
# input_data.participant_id,
# input_data.dm_conversation_id,
# input_data.text,
# input_data.media_id
# )
# yield "dm_event_id", dm_event_id
# yield "dm_conversation_id", dm_conversation_id
# except Exception as e:
# yield "error", handle_tweepy_exception(e)
# class TwitterCreateDMConversationBlock(Block):
# """
# Creates a new group direct message conversation on Twitter
# """
# class Input(BlockSchema):
# credentials: TwitterCredentialsInput = TwitterCredentialsField(
# ["offline.access", "dm.write","dm.read","tweet.read","user.read"]
# )
# participant_ids: list[str] = SchemaField(
# description="Array of User IDs to create conversation with (max 50)",
# placeholder="Enter participant user IDs",
# default=[],
# advanced=False
# )
# text: str = SchemaField(
# description="Text of the Direct Message (up to 10,000 characters)",
# placeholder="Enter message text",
# default="",
# advanced=False
# )
# media_id: str = SchemaField(
# description="Media ID to attach to the message",
# placeholder="Enter media ID",
# default="",
# advanced=False
# )
# class Output(BlockSchema):
# dm_event_id: str = SchemaField(description="ID of the sent direct message")
# dm_conversation_id: str = SchemaField(description="ID of the conversation")
# error: str = SchemaField(description="Error message if sending failed")
# def __init__(self):
# super().__init__(
# id="ec11cabc-a62e-11ef-8c0e-3fe37ba2ec92",
# description="This block creates a new group DM conversation with specified Twitter users.",
# categories={BlockCategory.SOCIAL},
# input_schema=TwitterCreateDMConversationBlock.Input,
# output_schema=TwitterCreateDMConversationBlock.Output,
# test_input={
# "participant_ids": ["783214", "2244994945"],
# "text": "Hello from Twitter API",
# "media_id": "",
# "credentials": TEST_CREDENTIALS_INPUT
# },
# test_credentials=TEST_CREDENTIALS,
# test_output=[
# ("dm_event_id", "0987654321"),
# ("dm_conversation_id", "1234567890"),
# ("error", "")
# ],
# test_mock={
# "create_dm_conversation": lambda *args, **kwargs: (
# "0987654321",
# "1234567890"
# )
# },
# )
# @staticmethod
# def create_dm_conversation(
# credentials: TwitterCredentials,
# participant_ids: list[str],
# text: str,
# media_id: str
# ):
# try:
# client = tweepy.Client(
# bearer_token=credentials.access_token.get_secret_value()
# )
# response = cast(
# Response,
# client.create_direct_message_conversation(
# participant_ids=participant_ids,
# text=None if text == "" else text,
# media_id=None if media_id == "" else media_id,
# user_auth=False
# )
# )
# if not response.data:
# raise Exception("Failed to create DM conversation")
# return response.data["dm_event_id"], response.data["dm_conversation_id"]
# except tweepy.TweepyException:
# raise
# except Exception as e:
# print(f"Unexpected error: {str(e)}")
# raise
# def run(
# self,
# input_data: Input,
# *,
# credentials: TwitterCredentials,
# **kwargs,
# ) -> BlockOutput:
# try:
# dm_event_id, dm_conversation_id = self.create_dm_conversation(
# credentials,
# input_data.participant_ids,
# input_data.text,
# input_data.media_id
# )
# yield "dm_event_id", dm_event_id
# yield "dm_conversation_id", dm_conversation_id
# except Exception as e:
# yield "error", handle_tweepy_exception(e)

View File

@@ -1,470 +0,0 @@
# from typing import cast
import tweepy
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
# from backend.blocks.twitter._builders import UserExpansionsBuilder
# from backend.blocks.twitter._types import TweetFields, TweetUserFields, UserExpansionInputs, UserExpansions
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
# from tweepy.client import Response
class TwitterUnfollowListBlock(Block):
"""
Unfollows a Twitter list for the authenticated user
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["follows.write", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to unfollow",
placeholder="Enter list ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the unfollow was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="1f43310a-a62f-11ef-8276-2b06a1bbae1a",
description="This block unfollows a specified Twitter list for the authenticated user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterUnfollowListBlock.Input,
output_schema=TwitterUnfollowListBlock.Output,
test_input={"list_id": "123456789", "credentials": TEST_CREDENTIALS_INPUT},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"unfollow_list": lambda *args, **kwargs: True},
)
@staticmethod
def unfollow_list(credentials: TwitterCredentials, list_id: str):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.unfollow_list(list_id=list_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.unfollow_list(credentials, input_data.list_id)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterFollowListBlock(Block):
"""
Follows a Twitter list for the authenticated user
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "list.write", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to follow",
placeholder="Enter list ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the follow was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="03d8acf6-a62f-11ef-b17f-b72b04a09e79",
description="This block follows a specified Twitter list for the authenticated user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterFollowListBlock.Input,
output_schema=TwitterFollowListBlock.Output,
test_input={"list_id": "123456789", "credentials": TEST_CREDENTIALS_INPUT},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"follow_list": lambda *args, **kwargs: True},
)
@staticmethod
def follow_list(credentials: TwitterCredentials, list_id: str):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.follow_list(list_id=list_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.follow_list(credentials, input_data.list_id)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
# Enterprise Level [Need to do Manual testing], There is a high possibility that we might get error in this
# Needs Type Input in this
# class TwitterListGetFollowersBlock(Block):
# """
# Gets followers of a specified Twitter list
# """
# class Input(UserExpansionInputs):
# credentials: TwitterCredentialsInput = TwitterCredentialsField(
# ["tweet.read","users.read", "list.read", "offline.access"]
# )
# list_id: str = SchemaField(
# description="The ID of the List to get followers for",
# placeholder="Enter list ID",
# required=True
# )
# max_results: int = SchemaField(
# description="Max number of results per page (1-100)",
# placeholder="Enter max results",
# default=10,
# advanced=True,
# )
# pagination_token: str = SchemaField(
# description="Token for pagination",
# placeholder="Enter pagination token",
# default="",
# advanced=True,
# )
# class Output(BlockSchema):
# user_ids: list[str] = SchemaField(description="List of user IDs of followers")
# usernames: list[str] = SchemaField(description="List of usernames of followers")
# next_token: str = SchemaField(description="Token for next page of results")
# data: list[dict] = SchemaField(description="Complete follower data")
# included: dict = SchemaField(description="Additional data requested via expansions")
# meta: dict = SchemaField(description="Metadata about the response")
# error: str = SchemaField(description="Error message if the request failed")
# def __init__(self):
# super().__init__(
# id="16b289b4-a62f-11ef-95d4-bb29b849eb99",
# description="This block retrieves followers of a specified Twitter list.",
# categories={BlockCategory.SOCIAL},
# input_schema=TwitterListGetFollowersBlock.Input,
# output_schema=TwitterListGetFollowersBlock.Output,
# test_input={
# "list_id": "123456789",
# "max_results": 10,
# "pagination_token": None,
# "credentials": TEST_CREDENTIALS_INPUT,
# "expansions": [],
# "tweet_fields": [],
# "user_fields": []
# },
# test_credentials=TEST_CREDENTIALS,
# test_output=[
# ("user_ids", ["2244994945"]),
# ("usernames", ["testuser"]),
# ("next_token", None),
# ("data", {"followers": [{"id": "2244994945", "username": "testuser"}]}),
# ("included", {}),
# ("meta", {}),
# ("error", "")
# ],
# test_mock={
# "get_list_followers": lambda *args, **kwargs: ({
# "followers": [{"id": "2244994945", "username": "testuser"}]
# }, {}, {}, ["2244994945"], ["testuser"], None)
# }
# )
# @staticmethod
# def get_list_followers(
# credentials: TwitterCredentials,
# list_id: str,
# max_results: int,
# pagination_token: str,
# expansions: list[UserExpansions],
# tweet_fields: list[TweetFields],
# user_fields: list[TweetUserFields]
# ):
# try:
# client = tweepy.Client(
# bearer_token=credentials.access_token.get_secret_value(),
# )
# params = {
# "id": list_id,
# "max_results": max_results,
# "pagination_token": None if pagination_token == "" else pagination_token,
# "user_auth": False
# }
# params = (UserExpansionsBuilder(params)
# .add_expansions(expansions)
# .add_tweet_fields(tweet_fields)
# .add_user_fields(user_fields)
# .build())
# response = cast(
# Response,
# client.get_list_followers(**params)
# )
# meta = {}
# user_ids = []
# usernames = []
# next_token = None
# if response.meta:
# meta = response.meta
# next_token = meta.get("next_token")
# included = IncludesSerializer.serialize(response.includes)
# data = ResponseDataSerializer.serialize_list(response.data)
# if response.data:
# user_ids = [str(item.id) for item in response.data]
# usernames = [item.username for item in response.data]
# return data, included, meta, user_ids, usernames, next_token
# raise Exception("No followers found")
# except tweepy.TweepyException:
# raise
# def run(
# self,
# input_data: Input,
# *,
# credentials: TwitterCredentials,
# **kwargs,
# ) -> BlockOutput:
# try:
# followers_data, included, meta, user_ids, usernames, next_token = self.get_list_followers(
# credentials,
# input_data.list_id,
# input_data.max_results,
# input_data.pagination_token,
# input_data.expansions,
# input_data.tweet_fields,
# input_data.user_fields
# )
# if user_ids:
# yield "user_ids", user_ids
# if usernames:
# yield "usernames", usernames
# if next_token:
# yield "next_token", next_token
# if followers_data:
# yield "data", followers_data
# if included:
# yield "included", included
# if meta:
# yield "meta", meta
# except Exception as e:
# yield "error", handle_tweepy_exception(e)
# class TwitterGetFollowedListsBlock(Block):
# """
# Gets lists followed by a specified Twitter user
# """
# class Input(UserExpansionInputs):
# credentials: TwitterCredentialsInput = TwitterCredentialsField(
# ["follows.read", "users.read", "list.read", "offline.access"]
# )
# user_id: str = SchemaField(
# description="The user ID whose followed Lists to retrieve",
# placeholder="Enter user ID",
# required=True
# )
# max_results: int = SchemaField(
# description="Max number of results per page (1-100)",
# placeholder="Enter max results",
# default=10,
# advanced=True,
# )
# pagination_token: str = SchemaField(
# description="Token for pagination",
# placeholder="Enter pagination token",
# default="",
# advanced=True,
# )
# class Output(BlockSchema):
# list_ids: list[str] = SchemaField(description="List of list IDs")
# list_names: list[str] = SchemaField(description="List of list names")
# data: list[dict] = SchemaField(description="Complete list data")
# includes: dict = SchemaField(description="Additional data requested via expansions")
# meta: dict = SchemaField(description="Metadata about the response")
# next_token: str = SchemaField(description="Token for next page of results")
# error: str = SchemaField(description="Error message if the request failed")
# def __init__(self):
# super().__init__(
# id="0e18bbfc-a62f-11ef-94fa-1f1e174b809e",
# description="This block retrieves all Lists a specified user follows.",
# categories={BlockCategory.SOCIAL},
# input_schema=TwitterGetFollowedListsBlock.Input,
# output_schema=TwitterGetFollowedListsBlock.Output,
# test_input={
# "user_id": "123456789",
# "max_results": 10,
# "pagination_token": None,
# "credentials": TEST_CREDENTIALS_INPUT,
# "expansions": [],
# "tweet_fields": [],
# "user_fields": []
# },
# test_credentials=TEST_CREDENTIALS,
# test_output=[
# ("list_ids", ["12345"]),
# ("list_names", ["Test List"]),
# ("data", {"followed_lists": [{"id": "12345", "name": "Test List"}]}),
# ("includes", {}),
# ("meta", {}),
# ("next_token", None),
# ("error", "")
# ],
# test_mock={
# "get_followed_lists": lambda *args, **kwargs: ({
# "followed_lists": [{"id": "12345", "name": "Test List"}]
# }, {}, {}, ["12345"], ["Test List"], None)
# }
# )
# @staticmethod
# def get_followed_lists(
# credentials: TwitterCredentials,
# user_id: str,
# max_results: int,
# pagination_token: str,
# expansions: list[UserExpansions],
# tweet_fields: list[TweetFields],
# user_fields: list[TweetUserFields]
# ):
# try:
# client = tweepy.Client(
# bearer_token=credentials.access_token.get_secret_value(),
# )
# params = {
# "id": user_id,
# "max_results": max_results,
# "pagination_token": None if pagination_token == "" else pagination_token,
# "user_auth": False
# }
# params = (UserExpansionsBuilder(params)
# .add_expansions(expansions)
# .add_tweet_fields(tweet_fields)
# .add_user_fields(user_fields)
# .build())
# response = cast(
# Response,
# client.get_followed_lists(**params)
# )
# meta = {}
# list_ids = []
# list_names = []
# next_token = None
# if response.meta:
# meta = response.meta
# next_token = meta.get("next_token")
# included = IncludesSerializer.serialize(response.includes)
# data = ResponseDataSerializer.serialize_list(response.data)
# if response.data:
# list_ids = [str(item.id) for item in response.data]
# list_names = [item.name for item in response.data]
# return data, included, meta, list_ids, list_names, next_token
# raise Exception("No followed lists found")
# except tweepy.TweepyException:
# raise
# def run(
# self,
# input_data: Input,
# *,
# credentials: TwitterCredentials,
# **kwargs,
# ) -> BlockOutput:
# try:
# lists_data, included, meta, list_ids, list_names, next_token = self.get_followed_lists(
# credentials,
# input_data.user_id,
# input_data.max_results,
# input_data.pagination_token,
# input_data.expansions,
# input_data.tweet_fields,
# input_data.user_fields
# )
# if list_ids:
# yield "list_ids", list_ids
# if list_names:
# yield "list_names", list_names
# if next_token:
# yield "next_token", next_token
# if lists_data:
# yield "data", lists_data
# if included:
# yield "includes", included
# if meta:
# yield "meta", meta
# except Exception as e:
# yield "error", handle_tweepy_exception(e)

View File

@@ -1,348 +0,0 @@
from typing import cast
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter._builders import ListExpansionsBuilder
from backend.blocks.twitter._serializer import (
IncludesSerializer,
ResponseDataSerializer,
)
from backend.blocks.twitter._types import (
ListExpansionInputs,
ListExpansionsFilter,
ListFieldsFilter,
TweetUserFieldsFilter,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TwitterGetListBlock(Block):
"""
Gets information about a Twitter List specified by ID
"""
class Input(ListExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to lookup",
placeholder="Enter list ID",
required=True,
)
class Output(BlockSchema):
# Common outputs
id: str = SchemaField(description="ID of the Twitter List")
name: str = SchemaField(description="Name of the Twitter List")
owner_id: str = SchemaField(description="ID of the List owner")
owner_username: str = SchemaField(description="Username of the List owner")
# Complete outputs
data: dict = SchemaField(description="Complete list data")
included: dict = SchemaField(
description="Additional data requested via expansions"
)
meta: dict = SchemaField(description="Metadata about the response")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="34ebc80a-a62f-11ef-9c2a-3fcab6c07079",
description="This block retrieves information about a specified Twitter List.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetListBlock.Input,
output_schema=TwitterGetListBlock.Output,
test_input={
"list_id": "84839422",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"list_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "84839422"),
("name", "Official Twitter Accounts"),
("owner_id", "2244994945"),
("owner_username", "TwitterAPI"),
("data", {"id": "84839422", "name": "Official Twitter Accounts"}),
],
test_mock={
"get_list": lambda *args, **kwargs: (
{"id": "84839422", "name": "Official Twitter Accounts"},
{},
{},
"2244994945",
"TwitterAPI",
)
},
)
@staticmethod
def get_list(
credentials: TwitterCredentials,
list_id: str,
expansions: ListExpansionsFilter | None,
user_fields: TweetUserFieldsFilter | None,
list_fields: ListFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {"id": list_id, "user_auth": False}
params = (
ListExpansionsBuilder(params)
.add_expansions(expansions)
.add_user_fields(user_fields)
.add_list_fields(list_fields)
.build()
)
response = cast(Response, client.get_list(**params))
meta = {}
owner_id = ""
owner_username = ""
included = {}
if response.includes:
included = IncludesSerializer.serialize(response.includes)
if "users" in included:
owner_id = str(included["users"][0]["id"])
owner_username = included["users"][0]["username"]
if response.meta:
meta = response.meta
if response.data:
data_dict = ResponseDataSerializer.serialize_dict(response.data)
return data_dict, included, meta, owner_id, owner_username
raise Exception("List not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
list_data, included, meta, owner_id, owner_username = self.get_list(
credentials,
input_data.list_id,
input_data.expansions,
input_data.user_fields,
input_data.list_fields,
)
yield "id", str(list_data["id"])
yield "name", list_data["name"]
if owner_id:
yield "owner_id", owner_id
if owner_username:
yield "owner_username", owner_username
yield "data", {"id": list_data["id"], "name": list_data["name"]}
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetOwnedListsBlock(Block):
"""
Gets all Lists owned by the specified user
"""
class Input(ListExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "list.read", "offline.access"]
)
user_id: str = SchemaField(
description="The user ID whose owned Lists to retrieve",
placeholder="Enter user ID",
required=True,
)
max_results: int | None = SchemaField(
description="Maximum number of results per page (1-100)",
placeholder="Enter max results (default 100)",
advanced=True,
default=10,
)
pagination_token: str | None = SchemaField(
description="Token for pagination",
placeholder="Enter pagination token",
advanced=True,
default="",
)
class Output(BlockSchema):
# Common outputs
list_ids: list[str] = SchemaField(description="List ids of the owned lists")
list_names: list[str] = SchemaField(description="List names of the owned lists")
next_token: str = SchemaField(description="Token for next page of results")
# Complete outputs
data: list[dict] = SchemaField(description="Complete owned lists data")
included: dict = SchemaField(
description="Additional data requested via expansions"
)
meta: dict = SchemaField(description="Metadata about the response")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="2b6bdb26-a62f-11ef-a9ce-ff89c2568726",
description="This block retrieves all Lists owned by a specified Twitter user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetOwnedListsBlock.Input,
output_schema=TwitterGetOwnedListsBlock.Output,
test_input={
"user_id": "2244994945",
"max_results": 10,
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"list_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("list_ids", ["84839422"]),
("list_names", ["Official Twitter Accounts"]),
("data", [{"id": "84839422", "name": "Official Twitter Accounts"}]),
],
test_mock={
"get_owned_lists": lambda *args, **kwargs: (
[{"id": "84839422", "name": "Official Twitter Accounts"}],
{},
{},
["84839422"],
["Official Twitter Accounts"],
None,
)
},
)
@staticmethod
def get_owned_lists(
credentials: TwitterCredentials,
user_id: str,
max_results: int | None,
pagination_token: str | None,
expansions: ListExpansionsFilter | None,
user_fields: TweetUserFieldsFilter | None,
list_fields: ListFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": user_id,
"max_results": max_results,
"pagination_token": (
None if pagination_token == "" else pagination_token
),
"user_auth": False,
}
params = (
ListExpansionsBuilder(params)
.add_expansions(expansions)
.add_user_fields(user_fields)
.add_list_fields(list_fields)
.build()
)
response = cast(Response, client.get_owned_lists(**params))
meta = {}
included = {}
list_ids = []
list_names = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
if response.includes:
included = IncludesSerializer.serialize(response.includes)
if response.data:
data = ResponseDataSerializer.serialize_list(response.data)
list_ids = [
str(item.id) for item in response.data if hasattr(item, "id")
]
list_names = [
item.name for item in response.data if hasattr(item, "name")
]
return data, included, meta, list_ids, list_names, next_token
raise Exception("User have no owned list")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
list_data, included, meta, list_ids, list_names, next_token = (
self.get_owned_lists(
credentials,
input_data.user_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.user_fields,
input_data.list_fields,
)
)
if list_ids:
yield "list_ids", list_ids
if list_names:
yield "list_names", list_names
if next_token:
yield "next_token", next_token
if list_data:
yield "data", list_data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,527 +0,0 @@
from typing import cast
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter._builders import (
ListExpansionsBuilder,
UserExpansionsBuilder,
)
from backend.blocks.twitter._serializer import (
IncludesSerializer,
ResponseDataSerializer,
)
from backend.blocks.twitter._types import (
ListExpansionInputs,
ListExpansionsFilter,
ListFieldsFilter,
TweetFieldsFilter,
TweetUserFieldsFilter,
UserExpansionInputs,
UserExpansionsFilter,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TwitterRemoveListMemberBlock(Block):
"""
Removes a member from a Twitter List that the authenticated user owns
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.write", "users.read", "tweet.read", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to remove the member from",
placeholder="Enter list ID",
required=True,
)
user_id: str = SchemaField(
description="The ID of the user to remove from the List",
placeholder="Enter user ID to remove",
required=True,
)
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the member was successfully removed"
)
error: str = SchemaField(description="Error message if the removal failed")
def __init__(self):
super().__init__(
id="5a3d1320-a62f-11ef-b7ce-a79e7656bcb0",
description="This block removes a specified user from a Twitter List owned by the authenticated user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterRemoveListMemberBlock.Input,
output_schema=TwitterRemoveListMemberBlock.Output,
test_input={
"list_id": "123456789",
"user_id": "987654321",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"remove_list_member": lambda *args, **kwargs: True},
)
@staticmethod
def remove_list_member(credentials: TwitterCredentials, list_id: str, user_id: str):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.remove_list_member(id=list_id, user_id=user_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
except Exception:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.remove_list_member(
credentials, input_data.list_id, input_data.user_id
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterAddListMemberBlock(Block):
"""
Adds a member to a Twitter List that the authenticated user owns
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.write", "users.read", "tweet.read", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to add the member to",
placeholder="Enter list ID",
required=True,
)
user_id: str = SchemaField(
description="The ID of the user to add to the List",
placeholder="Enter user ID to add",
required=True,
)
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the member was successfully added"
)
error: str = SchemaField(description="Error message if the addition failed")
def __init__(self):
super().__init__(
id="3ee8284e-a62f-11ef-84e4-8f6e2cbf0ddb",
description="This block adds a specified user to a Twitter List owned by the authenticated user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterAddListMemberBlock.Input,
output_schema=TwitterAddListMemberBlock.Output,
test_input={
"list_id": "123456789",
"user_id": "987654321",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"add_list_member": lambda *args, **kwargs: True},
)
@staticmethod
def add_list_member(credentials: TwitterCredentials, list_id: str, user_id: str):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.add_list_member(id=list_id, user_id=user_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
except Exception:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.add_list_member(
credentials, input_data.list_id, input_data.user_id
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetListMembersBlock(Block):
"""
Gets the members of a specified Twitter List
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.read", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to get members from",
placeholder="Enter list ID",
required=True,
)
max_results: int | None = SchemaField(
description="Maximum number of results per page (1-100)",
placeholder="Enter max results",
default=10,
advanced=True,
)
pagination_token: str | None = SchemaField(
description="Token for pagination of results",
placeholder="Enter pagination token",
default="",
advanced=True,
)
class Output(BlockSchema):
ids: list[str] = SchemaField(description="List of member user IDs")
usernames: list[str] = SchemaField(description="List of member usernames")
next_token: str = SchemaField(description="Next token for pagination")
data: list[dict] = SchemaField(
description="Complete user data for list members"
)
included: dict = SchemaField(
description="Additional data requested via expansions"
)
meta: dict = SchemaField(description="Metadata including pagination info")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="4dba046e-a62f-11ef-b69a-87240c84b4c7",
description="This block retrieves the members of a specified Twitter List.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetListMembersBlock.Input,
output_schema=TwitterGetListMembersBlock.Output,
test_input={
"list_id": "123456789",
"max_results": 2,
"pagination_token": None,
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"tweet_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["12345", "67890"]),
("usernames", ["testuser1", "testuser2"]),
(
"data",
[
{"id": "12345", "username": "testuser1"},
{"id": "67890", "username": "testuser2"},
],
),
],
test_mock={
"get_list_members": lambda *args, **kwargs: (
["12345", "67890"],
["testuser1", "testuser2"],
[
{"id": "12345", "username": "testuser1"},
{"id": "67890", "username": "testuser2"},
],
{},
{},
None,
)
},
)
@staticmethod
def get_list_members(
credentials: TwitterCredentials,
list_id: str,
max_results: int | None,
pagination_token: str | None,
expansions: UserExpansionsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": list_id,
"max_results": max_results,
"pagination_token": (
None if pagination_token == "" else pagination_token
),
"user_auth": False,
}
params = (
UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_list_members(**params))
meta = {}
included = {}
next_token = None
user_ids = []
usernames = []
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
if response.includes:
included = IncludesSerializer.serialize(response.includes)
if response.data:
data = ResponseDataSerializer.serialize_list(response.data)
user_ids = [str(user.id) for user in response.data]
usernames = [user.username for user in response.data]
return user_ids, usernames, data, included, meta, next_token
raise Exception("List members not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, usernames, data, included, meta, next_token = self.get_list_members(
credentials,
input_data.list_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields,
)
if ids:
yield "ids", ids
if usernames:
yield "usernames", usernames
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetListMembershipsBlock(Block):
"""
Gets all Lists that a specified user is a member of
"""
class Input(ListExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.read", "offline.access"]
)
user_id: str = SchemaField(
description="The ID of the user whose List memberships to retrieve",
placeholder="Enter user ID",
required=True,
)
max_results: int | None = SchemaField(
description="Maximum number of results per page (1-100)",
placeholder="Enter max results",
advanced=True,
default=10,
)
pagination_token: str | None = SchemaField(
description="Token for pagination of results",
placeholder="Enter pagination token",
advanced=True,
default="",
)
class Output(BlockSchema):
list_ids: list[str] = SchemaField(description="List of list IDs")
next_token: str = SchemaField(description="Next token for pagination")
data: list[dict] = SchemaField(description="List membership data")
included: dict = SchemaField(
description="Additional data requested via expansions"
)
meta: dict = SchemaField(description="Metadata about pagination")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="46e6429c-a62f-11ef-81c0-2b55bc7823ba",
description="This block retrieves all Lists that a specified user is a member of.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetListMembershipsBlock.Input,
output_schema=TwitterGetListMembershipsBlock.Output,
test_input={
"user_id": "123456789",
"max_results": 1,
"pagination_token": None,
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"list_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("list_ids", ["84839422"]),
("data", [{"id": "84839422"}]),
],
test_mock={
"get_list_memberships": lambda *args, **kwargs: (
[{"id": "84839422"}],
{},
{},
["84839422"],
None,
)
},
)
@staticmethod
def get_list_memberships(
credentials: TwitterCredentials,
user_id: str,
max_results: int | None,
pagination_token: str | None,
expansions: ListExpansionsFilter | None,
user_fields: TweetUserFieldsFilter | None,
list_fields: ListFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": user_id,
"max_results": max_results,
"pagination_token": (
None if pagination_token == "" else pagination_token
),
"user_auth": False,
}
params = (
ListExpansionsBuilder(params)
.add_expansions(expansions)
.add_user_fields(user_fields)
.add_list_fields(list_fields)
.build()
)
response = cast(Response, client.get_list_memberships(**params))
meta = {}
included = {}
next_token = None
list_ids = []
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
if response.includes:
included = IncludesSerializer.serialize(response.includes)
if response.data:
data = ResponseDataSerializer.serialize_list(response.data)
list_ids = [str(lst.id) for lst in response.data]
return data, included, meta, list_ids, next_token
raise Exception("List memberships not found")
except tweepy.TweepyException:
raise
except Exception:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
data, included, meta, list_ids, next_token = self.get_list_memberships(
credentials,
input_data.user_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.user_fields,
input_data.list_fields,
)
if list_ids:
yield "list_ids", list_ids
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,217 +0,0 @@
from typing import cast
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter._builders import TweetExpansionsBuilder
from backend.blocks.twitter._serializer import (
IncludesSerializer,
ResponseDataSerializer,
)
from backend.blocks.twitter._types import (
ExpansionFilter,
TweetExpansionInputs,
TweetFieldsFilter,
TweetMediaFieldsFilter,
TweetPlaceFieldsFilter,
TweetPollFieldsFilter,
TweetUserFieldsFilter,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TwitterGetListTweetsBlock(Block):
"""
Gets tweets from a specified Twitter list
"""
class Input(TweetExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List whose Tweets you would like to retrieve",
placeholder="Enter list ID",
required=True,
)
max_results: int | None = SchemaField(
description="Maximum number of results per page (1-100)",
placeholder="Enter max results",
default=10,
advanced=True,
)
pagination_token: str | None = SchemaField(
description="Token for paginating through results",
placeholder="Enter pagination token",
default="",
advanced=True,
)
class Output(BlockSchema):
# Common outputs
tweet_ids: list[str] = SchemaField(description="List of tweet IDs")
texts: list[str] = SchemaField(description="List of tweet texts")
next_token: str = SchemaField(description="Token for next page of results")
# Complete outputs
data: list[dict] = SchemaField(description="Complete list tweets data")
included: dict = SchemaField(
description="Additional data requested via expansions"
)
meta: dict = SchemaField(
description="Response metadata including pagination tokens"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="6657edb0-a62f-11ef-8c10-0326d832467d",
description="This block retrieves tweets from a specified Twitter list.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetListTweetsBlock.Input,
output_schema=TwitterGetListTweetsBlock.Output,
test_input={
"list_id": "84839422",
"max_results": 1,
"pagination_token": None,
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"media_fields": None,
"place_fields": None,
"poll_fields": None,
"tweet_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("tweet_ids", ["1234567890"]),
("texts", ["Test tweet"]),
("data", [{"id": "1234567890", "text": "Test tweet"}]),
],
test_mock={
"get_list_tweets": lambda *args, **kwargs: (
[{"id": "1234567890", "text": "Test tweet"}],
{},
{},
["1234567890"],
["Test tweet"],
None,
)
},
)
@staticmethod
def get_list_tweets(
credentials: TwitterCredentials,
list_id: str,
max_results: int | None,
pagination_token: str | None,
expansions: ExpansionFilter | None,
media_fields: TweetMediaFieldsFilter | None,
place_fields: TweetPlaceFieldsFilter | None,
poll_fields: TweetPollFieldsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": list_id,
"max_results": max_results,
"pagination_token": (
None if pagination_token == "" else pagination_token
),
"user_auth": False,
}
params = (
TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_list_tweets(**params))
meta = {}
included = {}
tweet_ids = []
texts = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
if response.includes:
included = IncludesSerializer.serialize(response.includes)
if response.data:
data = ResponseDataSerializer.serialize_list(response.data)
tweet_ids = [str(item.id) for item in response.data]
texts = [item.text for item in response.data]
return data, included, meta, tweet_ids, texts, next_token
raise Exception("No tweets found in this list")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
list_data, included, meta, tweet_ids, texts, next_token = (
self.get_list_tweets(
credentials,
input_data.list_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields,
)
)
if tweet_ids:
yield "tweet_ids", tweet_ids
if texts:
yield "texts", texts
if next_token:
yield "next_token", next_token
if list_data:
yield "data", list_data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,278 +0,0 @@
from typing import cast
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TwitterDeleteListBlock(Block):
"""
Deletes a Twitter List owned by the authenticated user
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.write", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to be deleted",
placeholder="Enter list ID",
required=True,
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the deletion was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="843c6892-a62f-11ef-a5c8-b71239a78d3b",
description="This block deletes a specified Twitter List owned by the authenticated user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterDeleteListBlock.Input,
output_schema=TwitterDeleteListBlock.Output,
test_input={"list_id": "1234567890", "credentials": TEST_CREDENTIALS_INPUT},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"delete_list": lambda *args, **kwargs: True},
)
@staticmethod
def delete_list(credentials: TwitterCredentials, list_id: str):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.delete_list(id=list_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
except Exception:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.delete_list(credentials, input_data.list_id)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterUpdateListBlock(Block):
"""
Updates a Twitter List owned by the authenticated user
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.write", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to be updated",
placeholder="Enter list ID",
advanced=False,
)
name: str | None = SchemaField(
description="New name for the List",
placeholder="Enter list name",
default="",
advanced=False,
)
description: str | None = SchemaField(
description="New description for the List",
placeholder="Enter list description",
default="",
advanced=False,
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the update was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="7d12630a-a62f-11ef-90c9-8f5a996612c3",
description="This block updates a specified Twitter List owned by the authenticated user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterUpdateListBlock.Input,
output_schema=TwitterUpdateListBlock.Output,
test_input={
"list_id": "1234567890",
"name": "Updated List Name",
"description": "Updated List Description",
"private": True,
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"update_list": lambda *args, **kwargs: True},
)
@staticmethod
def update_list(
credentials: TwitterCredentials,
list_id: str,
name: str | None,
description: str | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.update_list(
id=list_id,
name=None if name == "" else name,
description=None if description == "" else description,
user_auth=False,
)
return True
except tweepy.TweepyException:
raise
except Exception:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.update_list(
credentials, input_data.list_id, input_data.name, input_data.description
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterCreateListBlock(Block):
"""
Creates a Twitter List owned by the authenticated user
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.write", "offline.access"]
)
name: str = SchemaField(
description="The name of the List to be created",
placeholder="Enter list name",
advanced=False,
default="",
)
description: str | None = SchemaField(
description="Description of the List",
placeholder="Enter list description",
advanced=False,
default="",
)
private: bool = SchemaField(
description="Whether the List should be private",
advanced=False,
default=False,
)
class Output(BlockSchema):
url: str = SchemaField(description="URL of the created list")
list_id: str = SchemaField(description="ID of the created list")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="724148ba-a62f-11ef-89ba-5349b813ef5f",
description="This block creates a new Twitter List for the authenticated user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterCreateListBlock.Input,
output_schema=TwitterCreateListBlock.Output,
test_input={
"name": "New List Name",
"description": "New List Description",
"private": True,
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("list_id", "1234567890"),
("url", "https://twitter.com/i/lists/1234567890"),
],
test_mock={"create_list": lambda *args, **kwargs: ("1234567890")},
)
@staticmethod
def create_list(
credentials: TwitterCredentials,
name: str,
description: str | None,
private: bool,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
response = cast(
Response,
client.create_list(
name=None if name == "" else name,
description=None if description == "" else description,
private=private,
user_auth=False,
),
)
list_id = str(response.data["id"])
return list_id
except tweepy.TweepyException:
raise
except Exception:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
list_id = self.create_list(
credentials, input_data.name, input_data.description, input_data.private
)
yield "list_id", list_id
yield "url", f"https://twitter.com/i/lists/{list_id}"
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,285 +0,0 @@
from typing import cast
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter._builders import ListExpansionsBuilder
from backend.blocks.twitter._serializer import (
IncludesSerializer,
ResponseDataSerializer,
)
from backend.blocks.twitter._types import (
ListExpansionInputs,
ListExpansionsFilter,
ListFieldsFilter,
TweetUserFieldsFilter,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TwitterUnpinListBlock(Block):
"""
Enables the authenticated user to unpin a List.
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.write", "users.read", "tweet.read", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to unpin",
placeholder="Enter list ID",
required=True,
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the unpin was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="a099c034-a62f-11ef-9622-47d0ceb73555",
description="This block allows the authenticated user to unpin a specified List.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterUnpinListBlock.Input,
output_schema=TwitterUnpinListBlock.Output,
test_input={"list_id": "123456789", "credentials": TEST_CREDENTIALS_INPUT},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"unpin_list": lambda *args, **kwargs: True},
)
@staticmethod
def unpin_list(credentials: TwitterCredentials, list_id: str):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.unpin_list(list_id=list_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
except Exception:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.unpin_list(credentials, input_data.list_id)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterPinListBlock(Block):
"""
Enables the authenticated user to pin a List.
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.write", "users.read", "tweet.read", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to pin",
placeholder="Enter list ID",
required=True,
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the pin was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="8ec16e48-a62f-11ef-9f35-f3d6de43a802",
description="This block allows the authenticated user to pin a specified List.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterPinListBlock.Input,
output_schema=TwitterPinListBlock.Output,
test_input={"list_id": "123456789", "credentials": TEST_CREDENTIALS_INPUT},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"pin_list": lambda *args, **kwargs: True},
)
@staticmethod
def pin_list(credentials: TwitterCredentials, list_id: str):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.pin_list(list_id=list_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
except Exception:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.pin_list(credentials, input_data.list_id)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetPinnedListsBlock(Block):
"""
Returns the Lists pinned by the authenticated user.
"""
class Input(ListExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["lists.read", "users.read", "offline.access"]
)
class Output(BlockSchema):
list_ids: list[str] = SchemaField(description="List IDs of the pinned lists")
list_names: list[str] = SchemaField(
description="List names of the pinned lists"
)
data: list[dict] = SchemaField(
description="Response data containing pinned lists"
)
included: dict = SchemaField(
description="Additional data requested via expansions"
)
meta: dict = SchemaField(description="Metadata about the response")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="97e03aae-a62f-11ef-bc53-5b89cb02888f",
description="This block returns the Lists pinned by the authenticated user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetPinnedListsBlock.Input,
output_schema=TwitterGetPinnedListsBlock.Output,
test_input={
"expansions": None,
"list_fields": None,
"user_fields": None,
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("list_ids", ["84839422"]),
("list_names", ["Twitter List"]),
("data", [{"id": "84839422", "name": "Twitter List"}]),
],
test_mock={
"get_pinned_lists": lambda *args, **kwargs: (
[{"id": "84839422", "name": "Twitter List"}],
{},
{},
["84839422"],
["Twitter List"],
)
},
)
@staticmethod
def get_pinned_lists(
credentials: TwitterCredentials,
expansions: ListExpansionsFilter | None,
user_fields: TweetUserFieldsFilter | None,
list_fields: ListFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {"user_auth": False}
params = (
ListExpansionsBuilder(params)
.add_expansions(expansions)
.add_user_fields(user_fields)
.add_list_fields(list_fields)
.build()
)
response = cast(Response, client.get_pinned_lists(**params))
meta = {}
included = {}
list_ids = []
list_names = []
if response.meta:
meta = response.meta
if response.includes:
included = IncludesSerializer.serialize(response.includes)
if response.data:
data = ResponseDataSerializer.serialize_list(response.data)
list_ids = [str(item.id) for item in response.data]
list_names = [item.name for item in response.data]
return data, included, meta, list_ids, list_names
raise Exception("Lists not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
list_data, included, meta, list_ids, list_names = self.get_pinned_lists(
credentials,
input_data.expansions,
input_data.user_fields,
input_data.list_fields,
)
if list_ids:
yield "list_ids", list_ids
if list_names:
yield "list_names", list_names
if list_data:
yield "data", list_data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,195 +0,0 @@
from typing import cast
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter._builders import SpaceExpansionsBuilder
from backend.blocks.twitter._serializer import (
IncludesSerializer,
ResponseDataSerializer,
)
from backend.blocks.twitter._types import (
SpaceExpansionInputs,
SpaceExpansionsFilter,
SpaceFieldsFilter,
SpaceStatesFilter,
TweetUserFieldsFilter,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TwitterSearchSpacesBlock(Block):
"""
Returns live or scheduled Spaces matching specified search terms [for a week only]
"""
class Input(SpaceExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["spaces.read", "users.read", "tweet.read", "offline.access"]
)
query: str = SchemaField(
description="Search term to find in Space titles",
placeholder="Enter search query",
)
max_results: int | None = SchemaField(
description="Maximum number of results to return (1-100)",
placeholder="Enter max results",
default=10,
advanced=True,
)
state: SpaceStatesFilter = SchemaField(
description="Type of Spaces to return (live, scheduled, or all)",
placeholder="Enter state filter",
default=SpaceStatesFilter.all,
)
class Output(BlockSchema):
# Common outputs that user commonly uses
ids: list[str] = SchemaField(description="List of space IDs")
titles: list[str] = SchemaField(description="List of space titles")
host_ids: list = SchemaField(description="List of host IDs")
next_token: str = SchemaField(description="Next token for pagination")
# Complete outputs for advanced use
data: list[dict] = SchemaField(description="Complete space data")
includes: dict = SchemaField(
description="Additional data requested via expansions"
)
meta: dict = SchemaField(description="Metadata including pagination info")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="aaefdd48-a62f-11ef-a73c-3f44df63e276",
description="This block searches for Twitter Spaces based on specified terms.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterSearchSpacesBlock.Input,
output_schema=TwitterSearchSpacesBlock.Output,
test_input={
"query": "tech",
"max_results": 1,
"state": "live",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"space_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["1234"]),
("titles", ["Tech Talk"]),
("host_ids", ["5678"]),
("data", [{"id": "1234", "title": "Tech Talk", "host_ids": ["5678"]}]),
],
test_mock={
"search_spaces": lambda *args, **kwargs: (
[{"id": "1234", "title": "Tech Talk", "host_ids": ["5678"]}],
{},
{},
["1234"],
["Tech Talk"],
["5678"],
None,
)
},
)
@staticmethod
def search_spaces(
credentials: TwitterCredentials,
query: str,
max_results: int | None,
state: SpaceStatesFilter,
expansions: SpaceExpansionsFilter | None,
space_fields: SpaceFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {"query": query, "max_results": max_results, "state": state.value}
params = (
SpaceExpansionsBuilder(params)
.add_expansions(expansions)
.add_space_fields(space_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.search_spaces(**params))
meta = {}
next_token = ""
if response.meta:
meta = response.meta
if "next_token" in meta:
next_token = meta["next_token"]
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
ids = [str(space["id"]) for space in response.data if "id" in space]
titles = [space["title"] for space in data if "title" in space]
host_ids = [space["host_ids"] for space in data if "host_ids" in space]
return data, included, meta, ids, titles, host_ids, next_token
raise Exception("Spaces not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
data, included, meta, ids, titles, host_ids, next_token = (
self.search_spaces(
credentials,
input_data.query,
input_data.max_results,
input_data.state,
input_data.expansions,
input_data.space_fields,
input_data.user_fields,
)
)
if ids:
yield "ids", ids
if titles:
yield "titles", titles
if host_ids:
yield "host_ids", host_ids
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "includes", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,651 +0,0 @@
from typing import Literal, Union, cast
import tweepy
from pydantic import BaseModel
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter._builders import (
SpaceExpansionsBuilder,
TweetExpansionsBuilder,
UserExpansionsBuilder,
)
from backend.blocks.twitter._serializer import (
IncludesSerializer,
ResponseDataSerializer,
)
from backend.blocks.twitter._types import (
ExpansionFilter,
SpaceExpansionInputs,
SpaceExpansionsFilter,
SpaceFieldsFilter,
TweetExpansionInputs,
TweetFieldsFilter,
TweetMediaFieldsFilter,
TweetPlaceFieldsFilter,
TweetPollFieldsFilter,
TweetUserFieldsFilter,
UserExpansionInputs,
UserExpansionsFilter,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class SpaceList(BaseModel):
discriminator: Literal["space_list"]
space_ids: list[str] = SchemaField(
description="List of Space IDs to lookup (up to 100)",
placeholder="Enter Space IDs",
default=[],
advanced=False,
)
class UserList(BaseModel):
discriminator: Literal["user_list"]
user_ids: list[str] = SchemaField(
description="List of user IDs to lookup their Spaces (up to 100)",
placeholder="Enter user IDs",
default=[],
advanced=False,
)
class TwitterGetSpacesBlock(Block):
"""
Gets information about multiple Twitter Spaces specified by Space IDs or creator user IDs
"""
class Input(SpaceExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["spaces.read", "users.read", "offline.access"]
)
identifier: Union[SpaceList, UserList] = SchemaField(
discriminator="discriminator",
description="Choose whether to lookup spaces by their IDs or by creator user IDs",
advanced=False,
)
class Output(BlockSchema):
# Common outputs
ids: list[str] = SchemaField(description="List of space IDs")
titles: list[str] = SchemaField(description="List of space titles")
# Complete outputs for advanced use
data: list[dict] = SchemaField(description="Complete space data")
includes: dict = SchemaField(
description="Additional data requested via expansions"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="d75bd7d8-a62f-11ef-b0d8-c7a9496f617f",
description="This block retrieves information about multiple Twitter Spaces.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetSpacesBlock.Input,
output_schema=TwitterGetSpacesBlock.Output,
test_input={
"identifier": {
"discriminator": "space_list",
"space_ids": ["1DXxyRYNejbKM"],
},
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"space_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["1DXxyRYNejbKM"]),
("titles", ["Test Space"]),
(
"data",
[
{
"id": "1DXxyRYNejbKM",
"title": "Test Space",
"host_id": "1234567",
}
],
),
],
test_mock={
"get_spaces": lambda *args, **kwargs: (
[
{
"id": "1DXxyRYNejbKM",
"title": "Test Space",
"host_id": "1234567",
}
],
{},
["1DXxyRYNejbKM"],
["Test Space"],
)
},
)
@staticmethod
def get_spaces(
credentials: TwitterCredentials,
identifier: Union[SpaceList, UserList],
expansions: SpaceExpansionsFilter | None,
space_fields: SpaceFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"ids": (
identifier.space_ids if isinstance(identifier, SpaceList) else None
),
"user_ids": (
identifier.user_ids if isinstance(identifier, UserList) else None
),
}
params = (
SpaceExpansionsBuilder(params)
.add_expansions(expansions)
.add_space_fields(space_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_spaces(**params))
ids = []
titles = []
included = IncludesSerializer.serialize(response.includes)
if response.data:
data = ResponseDataSerializer.serialize_list(response.data)
ids = [space["id"] for space in data if "id" in space]
titles = [space["title"] for space in data if "title" in space]
return data, included, ids, titles
raise Exception("No spaces found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
data, included, ids, titles = self.get_spaces(
credentials,
input_data.identifier,
input_data.expansions,
input_data.space_fields,
input_data.user_fields,
)
if ids:
yield "ids", ids
if titles:
yield "titles", titles
if data:
yield "data", data
if included:
yield "includes", included
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetSpaceByIdBlock(Block):
"""
Gets information about a single Twitter Space specified by Space ID
"""
class Input(SpaceExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["spaces.read", "users.read", "offline.access"]
)
space_id: str = SchemaField(
description="Space ID to lookup",
placeholder="Enter Space ID",
required=True,
)
class Output(BlockSchema):
# Common outputs
id: str = SchemaField(description="Space ID")
title: str = SchemaField(description="Space title")
host_ids: list[str] = SchemaField(description="Host ID")
# Complete outputs for advanced use
data: dict = SchemaField(description="Complete space data")
includes: dict = SchemaField(
description="Additional data requested via expansions"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="c79700de-a62f-11ef-ab20-fb32bf9d5a9d",
description="This block retrieves information about a single Twitter Space.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetSpaceByIdBlock.Input,
output_schema=TwitterGetSpaceByIdBlock.Output,
test_input={
"space_id": "1DXxyRYNejbKM",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"space_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "1DXxyRYNejbKM"),
("title", "Test Space"),
("host_ids", ["1234567"]),
(
"data",
{
"id": "1DXxyRYNejbKM",
"title": "Test Space",
"host_ids": ["1234567"],
},
),
],
test_mock={
"get_space": lambda *args, **kwargs: (
{
"id": "1DXxyRYNejbKM",
"title": "Test Space",
"host_ids": ["1234567"],
},
{},
)
},
)
@staticmethod
def get_space(
credentials: TwitterCredentials,
space_id: str,
expansions: SpaceExpansionsFilter | None,
space_fields: SpaceFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": space_id,
}
params = (
SpaceExpansionsBuilder(params)
.add_expansions(expansions)
.add_space_fields(space_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_space(**params))
includes = {}
if response.includes:
for key, value in response.includes.items():
if isinstance(value, list):
includes[key] = [
item.data if hasattr(item, "data") else item
for item in value
]
else:
includes[key] = value.data if hasattr(value, "data") else value
data = {}
if response.data:
for key, value in response.data.items():
if isinstance(value, list):
data[key] = [
item.data if hasattr(item, "data") else item
for item in value
]
else:
data[key] = value.data if hasattr(value, "data") else value
return data, includes
raise Exception("Space not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
space_data, includes = self.get_space(
credentials,
input_data.space_id,
input_data.expansions,
input_data.space_fields,
input_data.user_fields,
)
# Common outputs
if space_data:
if "id" in space_data:
yield "id", space_data.get("id")
if "title" in space_data:
yield "title", space_data.get("title")
if "host_ids" in space_data:
yield "host_ids", space_data.get("host_ids")
if space_data:
yield "data", space_data
if includes:
yield "includes", includes
except Exception as e:
yield "error", handle_tweepy_exception(e)
# Not tested yet, might have some problem
class TwitterGetSpaceBuyersBlock(Block):
"""
Gets list of users who purchased a ticket to the requested Space
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["spaces.read", "users.read", "offline.access"]
)
space_id: str = SchemaField(
description="Space ID to lookup buyers for",
placeholder="Enter Space ID",
required=True,
)
class Output(BlockSchema):
# Common outputs
buyer_ids: list[str] = SchemaField(description="List of buyer IDs")
usernames: list[str] = SchemaField(description="List of buyer usernames")
# Complete outputs for advanced use
data: list[dict] = SchemaField(description="Complete space buyers data")
includes: dict = SchemaField(
description="Additional data requested via expansions"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="c1c121a8-a62f-11ef-8b0e-d7b85f96a46f",
description="This block retrieves a list of users who purchased tickets to a Twitter Space.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetSpaceBuyersBlock.Input,
output_schema=TwitterGetSpaceBuyersBlock.Output,
test_input={
"space_id": "1DXxyRYNejbKM",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("buyer_ids", ["2244994945"]),
("usernames", ["testuser"]),
(
"data",
[{"id": "2244994945", "username": "testuser", "name": "Test User"}],
),
],
test_mock={
"get_space_buyers": lambda *args, **kwargs: (
[{"id": "2244994945", "username": "testuser", "name": "Test User"}],
{},
["2244994945"],
["testuser"],
)
},
)
@staticmethod
def get_space_buyers(
credentials: TwitterCredentials,
space_id: str,
expansions: UserExpansionsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": space_id,
}
params = (
UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_space_buyers(**params))
included = IncludesSerializer.serialize(response.includes)
if response.data:
data = ResponseDataSerializer.serialize_list(response.data)
buyer_ids = [buyer["id"] for buyer in data]
usernames = [buyer["username"] for buyer in data]
return data, included, buyer_ids, usernames
raise Exception("No buyers found for this Space")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
buyers_data, included, buyer_ids, usernames = self.get_space_buyers(
credentials,
input_data.space_id,
input_data.expansions,
input_data.user_fields,
)
if buyer_ids:
yield "buyer_ids", buyer_ids
if usernames:
yield "usernames", usernames
if buyers_data:
yield "data", buyers_data
if included:
yield "includes", included
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetSpaceTweetsBlock(Block):
"""
Gets list of Tweets shared in the requested Space
"""
class Input(TweetExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["spaces.read", "users.read", "offline.access"]
)
space_id: str = SchemaField(
description="Space ID to lookup tweets for",
placeholder="Enter Space ID",
required=True,
)
class Output(BlockSchema):
# Common outputs
tweet_ids: list[str] = SchemaField(description="List of tweet IDs")
texts: list[str] = SchemaField(description="List of tweet texts")
# Complete outputs for advanced use
data: list[dict] = SchemaField(description="Complete space tweets data")
includes: dict = SchemaField(
description="Additional data requested via expansions"
)
meta: dict = SchemaField(description="Response metadata")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="b69731e6-a62f-11ef-b2d4-1bf14dd6aee4",
description="This block retrieves tweets shared in a Twitter Space.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetSpaceTweetsBlock.Input,
output_schema=TwitterGetSpaceTweetsBlock.Output,
test_input={
"space_id": "1DXxyRYNejbKM",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"media_fields": None,
"place_fields": None,
"poll_fields": None,
"tweet_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("tweet_ids", ["1234567890"]),
("texts", ["Test tweet"]),
("data", [{"id": "1234567890", "text": "Test tweet"}]),
],
test_mock={
"get_space_tweets": lambda *args, **kwargs: (
[{"id": "1234567890", "text": "Test tweet"}], # data
{},
["1234567890"],
["Test tweet"],
{},
)
},
)
@staticmethod
def get_space_tweets(
credentials: TwitterCredentials,
space_id: str,
expansions: ExpansionFilter | None,
media_fields: TweetMediaFieldsFilter | None,
place_fields: TweetPlaceFieldsFilter | None,
poll_fields: TweetPollFieldsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": space_id,
}
params = (
TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_space_tweets(**params))
included = IncludesSerializer.serialize(response.includes)
if response.data:
data = ResponseDataSerializer.serialize_list(response.data)
tweet_ids = [str(tweet["id"]) for tweet in data]
texts = [tweet["text"] for tweet in data]
meta = response.meta or {}
return data, included, tweet_ids, texts, meta
raise Exception("No tweets found for this Space")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
tweets_data, included, tweet_ids, texts, meta = self.get_space_tweets(
credentials,
input_data.space_id,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields,
)
if tweet_ids:
yield "tweet_ids", tweet_ids
if texts:
yield "texts", texts
if tweets_data:
yield "data", tweets_data
if included:
yield "includes", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,20 +0,0 @@
import tweepy
def handle_tweepy_exception(e: Exception) -> str:
if isinstance(e, tweepy.BadRequest):
return f"Bad Request (400): {str(e)}"
elif isinstance(e, tweepy.Unauthorized):
return f"Unauthorized (401): {str(e)}"
elif isinstance(e, tweepy.Forbidden):
return f"Forbidden (403): {str(e)}"
elif isinstance(e, tweepy.NotFound):
return f"Not Found (404): {str(e)}"
elif isinstance(e, tweepy.TooManyRequests):
return f"Too Many Requests (429): {str(e)}"
elif isinstance(e, tweepy.TwitterServerError):
return f"Twitter Server Error (5xx): {str(e)}"
elif isinstance(e, tweepy.TweepyException):
return f"Tweepy Error: {str(e)}"
else:
return f"Unexpected error: {str(e)}"

View File

@@ -1,372 +0,0 @@
from typing import cast
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter._builders import TweetExpansionsBuilder
from backend.blocks.twitter._serializer import (
IncludesSerializer,
ResponseDataSerializer,
)
from backend.blocks.twitter._types import (
ExpansionFilter,
TweetExpansionInputs,
TweetFieldsFilter,
TweetMediaFieldsFilter,
TweetPlaceFieldsFilter,
TweetPollFieldsFilter,
TweetUserFieldsFilter,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TwitterBookmarkTweetBlock(Block):
"""
Bookmark a tweet on Twitter
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "bookmark.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to bookmark",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the bookmark was successful")
error: str = SchemaField(description="Error message if the bookmark failed")
def __init__(self):
super().__init__(
id="f33d67be-a62f-11ef-a797-ff83ec29ee8e",
description="This block bookmarks a tweet on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterBookmarkTweetBlock.Input,
output_schema=TwitterBookmarkTweetBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"bookmark_tweet": lambda *args, **kwargs: True},
)
@staticmethod
def bookmark_tweet(
credentials: TwitterCredentials,
tweet_id: str,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.bookmark(tweet_id)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.bookmark_tweet(credentials, input_data.tweet_id)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetBookmarkedTweetsBlock(Block):
"""
Get All your bookmarked tweets from Twitter
"""
class Input(TweetExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "bookmark.read", "users.read", "offline.access"]
)
max_results: int | None = SchemaField(
description="Maximum number of results to return (1-100)",
placeholder="Enter max results",
default=10,
advanced=True,
)
pagination_token: str | None = SchemaField(
description="Token for pagination",
placeholder="Enter pagination token",
default="",
advanced=True,
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
id: list[str] = SchemaField(description="All Tweet IDs")
text: list[str] = SchemaField(description="All Tweet texts")
userId: list[str] = SchemaField(description="IDs of the tweet authors")
userName: list[str] = SchemaField(description="Usernames of the tweet authors")
# Complete Outputs for advanced use
data: list[dict] = SchemaField(description="Complete Tweet data")
included: dict = SchemaField(
description="Additional data that you have requested (Optional) via Expansions field"
)
meta: dict = SchemaField(
description="Provides metadata such as pagination info (next_token) or result counts"
)
next_token: str = SchemaField(description="Next token for pagination")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="ed26783e-a62f-11ef-9a21-c77c57dd8a1f",
description="This block retrieves bookmarked tweets from Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetBookmarkedTweetsBlock.Input,
output_schema=TwitterGetBookmarkedTweetsBlock.Output,
test_input={
"max_results": 2,
"pagination_token": None,
"expansions": None,
"media_fields": None,
"place_fields": None,
"poll_fields": None,
"tweet_fields": None,
"user_fields": None,
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", ["1234567890"]),
("text", ["Test tweet"]),
("userId", ["12345"]),
("userName", ["testuser"]),
("data", [{"id": "1234567890", "text": "Test tweet"}]),
],
test_mock={
"get_bookmarked_tweets": lambda *args, **kwargs: (
["1234567890"],
["Test tweet"],
["12345"],
["testuser"],
[{"id": "1234567890", "text": "Test tweet"}],
{},
{},
None,
)
},
)
@staticmethod
def get_bookmarked_tweets(
credentials: TwitterCredentials,
max_results: int | None,
pagination_token: str | None,
expansions: ExpansionFilter | None,
media_fields: TweetMediaFieldsFilter | None,
place_fields: TweetPlaceFieldsFilter | None,
poll_fields: TweetPollFieldsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"max_results": max_results,
"pagination_token": (
None if pagination_token == "" else pagination_token
),
}
params = (
TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(
Response,
client.get_bookmarks(**params),
)
meta = {}
tweet_ids = []
tweet_texts = []
user_ids = []
user_names = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
tweet_ids = [str(tweet.id) for tweet in response.data]
tweet_texts = [tweet.text for tweet in response.data]
if "users" in included:
for user in included["users"]:
user_ids.append(str(user["id"]))
user_names.append(user["username"])
return (
tweet_ids,
tweet_texts,
user_ids,
user_names,
data,
included,
meta,
next_token,
)
raise Exception("No bookmarked tweets found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, texts, user_ids, user_names, data, included, meta, next_token = (
self.get_bookmarked_tweets(
credentials,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields,
)
)
if ids:
yield "id", ids
if texts:
yield "text", texts
if user_ids:
yield "userId", user_ids
if user_names:
yield "userName", user_names
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
if next_token:
yield "next_token", next_token
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterRemoveBookmarkTweetBlock(Block):
"""
Remove a bookmark for a tweet on Twitter
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "bookmark.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to remove bookmark from",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the bookmark was successfully removed"
)
error: str = SchemaField(
description="Error message if the bookmark removal failed"
)
def __init__(self):
super().__init__(
id="e4100684-a62f-11ef-9be9-770cb41a2616",
description="This block removes a bookmark from a tweet on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterRemoveBookmarkTweetBlock.Input,
output_schema=TwitterRemoveBookmarkTweetBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"remove_bookmark_tweet": lambda *args, **kwargs: True},
)
@staticmethod
def remove_bookmark_tweet(
credentials: TwitterCredentials,
tweet_id: str,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.remove_bookmark(tweet_id)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.remove_bookmark_tweet(credentials, input_data.tweet_id)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,154 +0,0 @@
import tweepy
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TwitterHideReplyBlock(Block):
"""
Hides a reply of one of your tweets
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "tweet.moderate.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet reply to hide",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the operation was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="07d58b3e-a630-11ef-a030-93701d1a465e",
description="This block hides a reply to a tweet.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterHideReplyBlock.Input,
output_schema=TwitterHideReplyBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"hide_reply": lambda *args, **kwargs: True},
)
@staticmethod
def hide_reply(
credentials: TwitterCredentials,
tweet_id: str,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.hide_reply(id=tweet_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.hide_reply(
credentials,
input_data.tweet_id,
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterUnhideReplyBlock(Block):
"""
Unhides a reply to a tweet
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "tweet.moderate.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet reply to unhide",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the operation was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="fcf9e4e4-a62f-11ef-9d85-57d3d06b616a",
description="This block unhides a reply to a tweet.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterUnhideReplyBlock.Input,
output_schema=TwitterUnhideReplyBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"unhide_reply": lambda *args, **kwargs: True},
)
@staticmethod
def unhide_reply(
credentials: TwitterCredentials,
tweet_id: str,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.unhide_reply(id=tweet_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.unhide_reply(
credentials,
input_data.tweet_id,
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,576 +0,0 @@
from typing import cast
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter._builders import (
TweetExpansionsBuilder,
UserExpansionsBuilder,
)
from backend.blocks.twitter._serializer import (
IncludesSerializer,
ResponseDataSerializer,
)
from backend.blocks.twitter._types import (
ExpansionFilter,
TweetExpansionInputs,
TweetFieldsFilter,
TweetMediaFieldsFilter,
TweetPlaceFieldsFilter,
TweetPollFieldsFilter,
TweetUserFieldsFilter,
UserExpansionInputs,
UserExpansionsFilter,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TwitterLikeTweetBlock(Block):
"""
Likes a tweet
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "like.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to like",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the operation was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="4d0b4c5c-a630-11ef-8e08-1b14c507b347",
description="This block likes a tweet.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterLikeTweetBlock.Input,
output_schema=TwitterLikeTweetBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"like_tweet": lambda *args, **kwargs: True},
)
@staticmethod
def like_tweet(
credentials: TwitterCredentials,
tweet_id: str,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.like(tweet_id=tweet_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.like_tweet(
credentials,
input_data.tweet_id,
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetLikingUsersBlock(Block):
"""
Gets information about users who liked a one of your tweet
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "like.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to get liking users for",
placeholder="Enter tweet ID",
)
max_results: int | None = SchemaField(
description="Maximum number of results to return (1-100)",
placeholder="Enter max results",
default=10,
advanced=True,
)
pagination_token: str | None = SchemaField(
description="Token for getting next/previous page of results",
placeholder="Enter pagination token",
default="",
advanced=True,
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
id: list[str] = SchemaField(description="All User IDs who liked the tweet")
username: list[str] = SchemaField(
description="All User usernames who liked the tweet"
)
next_token: str = SchemaField(description="Next token for pagination")
# Complete Outputs for advanced use
data: list[dict] = SchemaField(description="Complete Tweet data")
included: dict = SchemaField(
description="Additional data that you have requested (Optional) via Expansions field"
)
meta: dict = SchemaField(
description="Provides metadata such as pagination info (next_token) or result counts"
)
# error
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="34275000-a630-11ef-b01e-5f00d9077c08",
description="This block gets information about users who liked a tweet.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetLikingUsersBlock.Input,
output_schema=TwitterGetLikingUsersBlock.Output,
test_input={
"tweet_id": "1234567890",
"max_results": 1,
"pagination_token": None,
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"tweet_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", ["1234567890"]),
("username", ["testuser"]),
("data", [{"id": "1234567890", "username": "testuser"}]),
],
test_mock={
"get_liking_users": lambda *args, **kwargs: (
["1234567890"],
["testuser"],
[{"id": "1234567890", "username": "testuser"}],
{},
{},
None,
)
},
)
@staticmethod
def get_liking_users(
credentials: TwitterCredentials,
tweet_id: str,
max_results: int | None,
pagination_token: str | None,
expansions: UserExpansionsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": tweet_id,
"max_results": max_results,
"pagination_token": (
None if pagination_token == "" else pagination_token
),
"user_auth": False,
}
params = (
UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_liking_users(**params))
if not response.data and not response.meta:
raise Exception("No liking users found")
meta = {}
user_ids = []
usernames = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
user_ids = [str(user.id) for user in response.data]
usernames = [user.username for user in response.data]
return user_ids, usernames, data, included, meta, next_token
raise Exception("No liking users found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, usernames, data, included, meta, next_token = self.get_liking_users(
credentials,
input_data.tweet_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields,
)
if ids:
yield "id", ids
if usernames:
yield "username", usernames
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetLikedTweetsBlock(Block):
"""
Gets information about tweets liked by you
"""
class Input(TweetExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "like.read", "offline.access"]
)
user_id: str = SchemaField(
description="ID of the user to get liked tweets for",
placeholder="Enter user ID",
)
max_results: int | None = SchemaField(
description="Maximum number of results to return (5-100)",
placeholder="100",
default=10,
advanced=True,
)
pagination_token: str | None = SchemaField(
description="Token for getting next/previous page of results",
placeholder="Enter pagination token",
default="",
advanced=True,
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
ids: list[str] = SchemaField(description="All Tweet IDs")
texts: list[str] = SchemaField(description="All Tweet texts")
userIds: list[str] = SchemaField(
description="List of user ids that authored the tweets"
)
userNames: list[str] = SchemaField(
description="List of user names that authored the tweets"
)
next_token: str = SchemaField(description="Next token for pagination")
# Complete Outputs for advanced use
data: list[dict] = SchemaField(description="Complete Tweet data")
included: dict = SchemaField(
description="Additional data that you have requested (Optional) via Expansions field"
)
meta: dict = SchemaField(
description="Provides metadata such as pagination info (next_token) or result counts"
)
# error
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="292e7c78-a630-11ef-9f40-df5dffaca106",
description="This block gets information about tweets liked by a user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetLikedTweetsBlock.Input,
output_schema=TwitterGetLikedTweetsBlock.Output,
test_input={
"user_id": "1234567890",
"max_results": 2,
"pagination_token": None,
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"media_fields": None,
"place_fields": None,
"poll_fields": None,
"tweet_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["12345", "67890"]),
("texts", ["Tweet 1", "Tweet 2"]),
("userIds", ["67890", "67891"]),
("userNames", ["testuser1", "testuser2"]),
(
"data",
[
{"id": "12345", "text": "Tweet 1"},
{"id": "67890", "text": "Tweet 2"},
],
),
],
test_mock={
"get_liked_tweets": lambda *args, **kwargs: (
["12345", "67890"],
["Tweet 1", "Tweet 2"],
["67890", "67891"],
["testuser1", "testuser2"],
[
{"id": "12345", "text": "Tweet 1"},
{"id": "67890", "text": "Tweet 2"},
],
{},
{},
None,
)
},
)
@staticmethod
def get_liked_tweets(
credentials: TwitterCredentials,
user_id: str,
max_results: int | None,
pagination_token: str | None,
expansions: ExpansionFilter | None,
media_fields: TweetMediaFieldsFilter | None,
place_fields: TweetPlaceFieldsFilter | None,
poll_fields: TweetPollFieldsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": user_id,
"max_results": max_results,
"pagination_token": (
None if pagination_token == "" else pagination_token
),
"user_auth": False,
}
params = (
TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_liked_tweets(**params))
if not response.data and not response.meta:
raise Exception("No liked tweets found")
meta = {}
tweet_ids = []
tweet_texts = []
user_ids = []
user_names = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
tweet_ids = [str(tweet.id) for tweet in response.data]
tweet_texts = [tweet.text for tweet in response.data]
if "users" in response.includes:
user_ids = [str(user["id"]) for user in response.includes["users"]]
user_names = [
user["username"] for user in response.includes["users"]
]
return (
tweet_ids,
tweet_texts,
user_ids,
user_names,
data,
included,
meta,
next_token,
)
raise Exception("No liked tweets found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, texts, user_ids, user_names, data, included, meta, next_token = (
self.get_liked_tweets(
credentials,
input_data.user_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields,
)
)
if ids:
yield "ids", ids
if texts:
yield "texts", texts
if user_ids:
yield "userIds", user_ids
if user_names:
yield "userNames", user_names
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterUnlikeTweetBlock(Block):
"""
Unlikes a tweet that was previously liked
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "like.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to unlike",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the operation was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="1ed5eab8-a630-11ef-8e21-cbbbc80cbb85",
description="This block unlikes a tweet.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterUnlikeTweetBlock.Input,
output_schema=TwitterUnlikeTweetBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"unlike_tweet": lambda *args, **kwargs: True},
)
@staticmethod
def unlike_tweet(
credentials: TwitterCredentials,
tweet_id: str,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.unlike(tweet_id=tweet_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.unlike_tweet(
credentials,
input_data.tweet_id,
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,545 +0,0 @@
from datetime import datetime
from typing import List, Literal, Optional, Union, cast
import tweepy
from pydantic import BaseModel
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter._builders import (
TweetDurationBuilder,
TweetExpansionsBuilder,
TweetPostBuilder,
TweetSearchBuilder,
)
from backend.blocks.twitter._serializer import (
IncludesSerializer,
ResponseDataSerializer,
)
from backend.blocks.twitter._types import (
ExpansionFilter,
TweetExpansionInputs,
TweetFieldsFilter,
TweetMediaFieldsFilter,
TweetPlaceFieldsFilter,
TweetPollFieldsFilter,
TweetReplySettingsFilter,
TweetTimeWindowInputs,
TweetUserFieldsFilter,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class Media(BaseModel):
discriminator: Literal["media"]
media_ids: Optional[List[str]] = None
media_tagged_user_ids: Optional[List[str]] = None
class DeepLink(BaseModel):
discriminator: Literal["deep_link"]
direct_message_deep_link: Optional[str] = None
class Poll(BaseModel):
discriminator: Literal["poll"]
poll_options: Optional[List[str]] = None
poll_duration_minutes: Optional[int] = None
class Place(BaseModel):
discriminator: Literal["place"]
place_id: Optional[str] = None
class Quote(BaseModel):
discriminator: Literal["quote"]
quote_tweet_id: Optional[str] = None
class TwitterPostTweetBlock(Block):
"""
Create a tweet on Twitter with the option to include one additional element such as a media, quote, or deep link.
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "tweet.write", "users.read", "offline.access"]
)
tweet_text: str | None = SchemaField(
description="Text of the tweet to post",
placeholder="Enter your tweet",
default=None,
advanced=False,
)
for_super_followers_only: bool = SchemaField(
description="Tweet exclusively for Super Followers",
placeholder="Enter for super followers only",
advanced=True,
default=False,
)
attachment: Union[Media, DeepLink, Poll, Place, Quote] | None = SchemaField(
discriminator="discriminator",
description="Additional tweet data (media, deep link, poll, place or quote)",
advanced=True,
)
exclude_reply_user_ids: Optional[List[str]] = SchemaField(
description="User IDs to exclude from reply Tweet thread. [ex - 6253282]",
placeholder="Enter user IDs to exclude",
advanced=True,
default=None,
)
in_reply_to_tweet_id: Optional[str] = SchemaField(
description="Tweet ID being replied to. Please note that in_reply_to_tweet_id needs to be in the request if exclude_reply_user_ids is present",
default=None,
placeholder="Enter in reply to tweet ID",
advanced=True,
)
reply_settings: TweetReplySettingsFilter = SchemaField(
description="Who can reply to the Tweet (mentionedUsers or following)",
placeholder="Enter reply settings",
advanced=True,
default=TweetReplySettingsFilter(All_Users=True),
)
class Output(BlockSchema):
tweet_id: str = SchemaField(description="ID of the created tweet")
tweet_url: str = SchemaField(description="URL to the tweet")
error: str = SchemaField(
description="Error message if the tweet posting failed"
)
def __init__(self):
super().__init__(
id="7bb0048a-a630-11ef-aeb8-abc0dadb9b12",
description="This block posts a tweet on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterPostTweetBlock.Input,
output_schema=TwitterPostTweetBlock.Output,
test_input={
"tweet_text": "This is a test tweet.",
"credentials": TEST_CREDENTIALS_INPUT,
"attachment": {
"discriminator": "deep_link",
"direct_message_deep_link": "https://twitter.com/messages/compose",
},
"for_super_followers_only": False,
"exclude_reply_user_ids": [],
"in_reply_to_tweet_id": "",
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("tweet_id", "1234567890"),
("tweet_url", "https://twitter.com/user/status/1234567890"),
],
test_mock={
"post_tweet": lambda *args, **kwargs: (
"1234567890",
"https://twitter.com/user/status/1234567890",
)
},
)
def post_tweet(
self,
credentials: TwitterCredentials,
input_txt: str | None,
attachment: Union[Media, DeepLink, Poll, Place, Quote] | None,
for_super_followers_only: bool,
exclude_reply_user_ids: Optional[List[str]],
in_reply_to_tweet_id: Optional[str],
reply_settings: TweetReplySettingsFilter,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = (
TweetPostBuilder()
.add_text(input_txt)
.add_super_followers(for_super_followers_only)
.add_reply_settings(
exclude_reply_user_ids or [],
in_reply_to_tweet_id or "",
reply_settings,
)
)
if isinstance(attachment, Media):
params.add_media(
attachment.media_ids or [], attachment.media_tagged_user_ids or []
)
elif isinstance(attachment, DeepLink):
params.add_deep_link(attachment.direct_message_deep_link or "")
elif isinstance(attachment, Poll):
params.add_poll_options(attachment.poll_options or [])
params.add_poll_duration(attachment.poll_duration_minutes or 0)
elif isinstance(attachment, Place):
params.add_place(attachment.place_id or "")
elif isinstance(attachment, Quote):
params.add_quote(attachment.quote_tweet_id or "")
tweet = cast(Response, client.create_tweet(**params.build()))
if not tweet.data:
raise Exception("Failed to create tweet")
tweet_id = tweet.data["id"]
tweet_url = f"https://twitter.com/user/status/{tweet_id}"
return str(tweet_id), tweet_url
except tweepy.TweepyException:
raise
except Exception:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
tweet_id, tweet_url = self.post_tweet(
credentials,
input_data.tweet_text,
input_data.attachment,
input_data.for_super_followers_only,
input_data.exclude_reply_user_ids,
input_data.in_reply_to_tweet_id,
input_data.reply_settings,
)
yield "tweet_id", tweet_id
yield "tweet_url", tweet_url
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterDeleteTweetBlock(Block):
"""
Deletes a tweet on Twitter using twitter Id
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "tweet.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to delete",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the tweet was successfully deleted"
)
error: str = SchemaField(
description="Error message if the tweet deletion failed"
)
def __init__(self):
super().__init__(
id="761babf0-a630-11ef-a03d-abceb082f58f",
description="This block deletes a tweet on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterDeleteTweetBlock.Input,
output_schema=TwitterDeleteTweetBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"delete_tweet": lambda *args, **kwargs: True},
)
@staticmethod
def delete_tweet(credentials: TwitterCredentials, tweet_id: str):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.delete_tweet(id=tweet_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
except Exception:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.delete_tweet(
credentials,
input_data.tweet_id,
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterSearchRecentTweetsBlock(Block):
"""
Searches all public Tweets in Twitter history
"""
class Input(TweetExpansionInputs, TweetTimeWindowInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
query: str = SchemaField(
description="Search query (up to 1024 characters)",
placeholder="Enter search query",
)
max_results: int = SchemaField(
description="Maximum number of results per page (10-500)",
placeholder="Enter max results",
default=10,
advanced=True,
)
pagination: str | None = SchemaField(
description="Token for pagination",
default="",
placeholder="Enter pagination token",
advanced=True,
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
tweet_ids: list[str] = SchemaField(description="All Tweet IDs")
tweet_texts: list[str] = SchemaField(description="All Tweet texts")
next_token: str = SchemaField(description="Next token for pagination")
# Complete Outputs for advanced use
data: list[dict] = SchemaField(description="Complete Tweet data")
included: dict = SchemaField(
description="Additional data that you have requested (Optional) via Expansions field"
)
meta: dict = SchemaField(
description="Provides metadata such as pagination info (next_token) or result counts"
)
# error
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="53e5cf8e-a630-11ef-ba85-df6d666fa5d5",
description="This block searches all public Tweets in Twitter history.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterSearchRecentTweetsBlock.Input,
output_schema=TwitterSearchRecentTweetsBlock.Output,
test_input={
"query": "from:twitterapi #twitterapi",
"credentials": TEST_CREDENTIALS_INPUT,
"max_results": 2,
"start_time": "2024-12-14T18:30:00.000Z",
"end_time": "2024-12-17T18:30:00.000Z",
"since_id": None,
"until_id": None,
"sort_order": None,
"pagination": None,
"expansions": None,
"media_fields": None,
"place_fields": None,
"poll_fields": None,
"tweet_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("tweet_ids", ["1373001119480344583", "1372627771717869568"]),
(
"tweet_texts",
[
"Looking to get started with the Twitter API but new to APIs in general?",
"Thanks to everyone who joined and made today a great session!",
],
),
(
"data",
[
{
"id": "1373001119480344583",
"text": "Looking to get started with the Twitter API but new to APIs in general?",
},
{
"id": "1372627771717869568",
"text": "Thanks to everyone who joined and made today a great session!",
},
],
),
],
test_mock={
"search_tweets": lambda *args, **kwargs: (
["1373001119480344583", "1372627771717869568"],
[
"Looking to get started with the Twitter API but new to APIs in general?",
"Thanks to everyone who joined and made today a great session!",
],
[
{
"id": "1373001119480344583",
"text": "Looking to get started with the Twitter API but new to APIs in general?",
},
{
"id": "1372627771717869568",
"text": "Thanks to everyone who joined and made today a great session!",
},
],
{},
{},
None,
)
},
)
@staticmethod
def search_tweets(
credentials: TwitterCredentials,
query: str,
max_results: int,
start_time: datetime | None,
end_time: datetime | None,
since_id: str | None,
until_id: str | None,
sort_order: str | None,
pagination: str | None,
expansions: ExpansionFilter | None,
media_fields: TweetMediaFieldsFilter | None,
place_fields: TweetPlaceFieldsFilter | None,
poll_fields: TweetPollFieldsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
# Building common params
params = (
TweetSearchBuilder()
.add_query(query)
.add_pagination(max_results, pagination)
.build()
)
# Adding expansions to params If required by the user
params = (
TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
# Adding time window to params If required by the user
params = (
TweetDurationBuilder(params)
.add_start_time(start_time)
.add_end_time(end_time)
.add_since_id(since_id)
.add_until_id(until_id)
.add_sort_order(sort_order)
.build()
)
response = cast(Response, client.search_recent_tweets(**params))
if not response.data and not response.meta:
raise Exception("No tweets found")
meta = {}
tweet_ids = []
tweet_texts = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
tweet_ids = [str(tweet.id) for tweet in response.data]
tweet_texts = [tweet.text for tweet in response.data]
return tweet_ids, tweet_texts, data, included, meta, next_token
raise Exception("No tweets found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, texts, data, included, meta, next_token = self.search_tweets(
credentials,
input_data.query,
input_data.max_results,
input_data.start_time,
input_data.end_time,
input_data.since_id,
input_data.until_id,
input_data.sort_order,
input_data.pagination,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields,
)
if ids:
yield "tweet_ids", ids
if texts:
yield "tweet_texts", texts
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,222 +0,0 @@
from typing import cast
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter._builders import TweetExpansionsBuilder
from backend.blocks.twitter._serializer import (
IncludesSerializer,
ResponseDataSerializer,
)
from backend.blocks.twitter._types import (
ExpansionFilter,
TweetExcludesFilter,
TweetExpansionInputs,
TweetFieldsFilter,
TweetMediaFieldsFilter,
TweetPlaceFieldsFilter,
TweetPollFieldsFilter,
TweetUserFieldsFilter,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TwitterGetQuoteTweetsBlock(Block):
"""
Gets quote tweets for a specified tweet ID
"""
class Input(TweetExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to get quotes for",
placeholder="Enter tweet ID",
)
max_results: int | None = SchemaField(
description="Number of results to return (max 100)",
default=10,
advanced=True,
)
exclude: TweetExcludesFilter | None = SchemaField(
description="Types of tweets to exclude", advanced=True, default=None
)
pagination_token: str | None = SchemaField(
description="Token for pagination",
advanced=True,
default="",
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
ids: list = SchemaField(description="All Tweet IDs ")
texts: list = SchemaField(description="All Tweet texts")
next_token: str = SchemaField(description="Next token for pagination")
# Complete Outputs for advanced use
data: list[dict] = SchemaField(description="Complete Tweet data")
included: dict = SchemaField(
description="Additional data that you have requested (Optional) via Expansions field"
)
meta: dict = SchemaField(
description="Provides metadata such as pagination info (next_token) or result counts"
)
# error
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="9fbdd208-a630-11ef-9b97-ab7a3a695ca3",
description="This block gets quote tweets for a specific tweet.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetQuoteTweetsBlock.Input,
output_schema=TwitterGetQuoteTweetsBlock.Output,
test_input={
"tweet_id": "1234567890",
"max_results": 2,
"pagination_token": None,
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["12345", "67890"]),
("texts", ["Tweet 1", "Tweet 2"]),
(
"data",
[
{"id": "12345", "text": "Tweet 1"},
{"id": "67890", "text": "Tweet 2"},
],
),
],
test_mock={
"get_quote_tweets": lambda *args, **kwargs: (
["12345", "67890"],
["Tweet 1", "Tweet 2"],
[
{"id": "12345", "text": "Tweet 1"},
{"id": "67890", "text": "Tweet 2"},
],
{},
{},
None,
)
},
)
@staticmethod
def get_quote_tweets(
credentials: TwitterCredentials,
tweet_id: str,
max_results: int | None,
exclude: TweetExcludesFilter | None,
pagination_token: str | None,
expansions: ExpansionFilter | None,
media_fields: TweetMediaFieldsFilter | None,
place_fields: TweetPlaceFieldsFilter | None,
poll_fields: TweetPollFieldsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": tweet_id,
"max_results": max_results,
"pagination_token": (
None if pagination_token == "" else pagination_token
),
"exclude": None if exclude == TweetExcludesFilter() else exclude,
"user_auth": False,
}
params = (
TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_quote_tweets(**params))
meta = {}
tweet_ids = []
tweet_texts = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
tweet_ids = [str(tweet.id) for tweet in response.data]
tweet_texts = [tweet.text for tweet in response.data]
return tweet_ids, tweet_texts, data, included, meta, next_token
raise Exception("No quote tweets found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, texts, data, included, meta, next_token = self.get_quote_tweets(
credentials,
input_data.tweet_id,
input_data.max_results,
input_data.exclude,
input_data.pagination_token,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields,
)
if ids:
yield "ids", ids
if texts:
yield "texts", texts
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,363 +0,0 @@
from typing import cast
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter._builders import UserExpansionsBuilder
from backend.blocks.twitter._serializer import (
IncludesSerializer,
ResponseDataSerializer,
)
from backend.blocks.twitter._types import (
TweetFieldsFilter,
TweetUserFieldsFilter,
UserExpansionInputs,
UserExpansionsFilter,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TwitterRetweetBlock(Block):
"""
Retweets a tweet on Twitter
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "tweet.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to retweet",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the retweet was successful")
error: str = SchemaField(description="Error message if the retweet failed")
def __init__(self):
super().__init__(
id="bd7b8d3a-a630-11ef-be96-6f4aa4c3c4f4",
description="This block retweets a tweet on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterRetweetBlock.Input,
output_schema=TwitterRetweetBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"retweet": lambda *args, **kwargs: True},
)
@staticmethod
def retweet(
credentials: TwitterCredentials,
tweet_id: str,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.retweet(
tweet_id=tweet_id,
user_auth=False,
)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.retweet(
credentials,
input_data.tweet_id,
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterRemoveRetweetBlock(Block):
"""
Removes a retweet on Twitter
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "tweet.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to remove retweet",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the retweet was successfully removed"
)
error: str = SchemaField(description="Error message if the removal failed")
def __init__(self):
super().__init__(
id="b6e663f0-a630-11ef-a7f0-8b9b0c542ff8",
description="This block removes a retweet on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterRemoveRetweetBlock.Input,
output_schema=TwitterRemoveRetweetBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"remove_retweet": lambda *args, **kwargs: True},
)
@staticmethod
def remove_retweet(
credentials: TwitterCredentials,
tweet_id: str,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.unretweet(
source_tweet_id=tweet_id,
user_auth=False,
)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.remove_retweet(
credentials,
input_data.tweet_id,
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetRetweetersBlock(Block):
"""
Gets information about who has retweeted a tweet
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to get retweeters for",
placeholder="Enter tweet ID",
)
max_results: int | None = SchemaField(
description="Maximum number of results per page (1-100)",
default=10,
placeholder="Enter max results",
advanced=True,
)
pagination_token: str | None = SchemaField(
description="Token for pagination",
placeholder="Enter pagination token",
default="",
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
ids: list = SchemaField(description="List of user ids who retweeted")
names: list = SchemaField(description="List of user names who retweeted")
usernames: list = SchemaField(
description="List of user usernames who retweeted"
)
next_token: str = SchemaField(description="Token for next page of results")
# Complete Outputs for advanced use
data: list[dict] = SchemaField(description="Complete Tweet data")
included: dict = SchemaField(
description="Additional data that you have requested (Optional) via Expansions field"
)
meta: dict = SchemaField(
description="Provides metadata such as pagination info (next_token) or result counts"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="ad7aa6fa-a630-11ef-a6b0-e7ca640aa030",
description="This block gets information about who has retweeted a tweet.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetRetweetersBlock.Input,
output_schema=TwitterGetRetweetersBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
"max_results": 1,
"pagination_token": "",
"expansions": None,
"media_fields": None,
"place_fields": None,
"poll_fields": None,
"tweet_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["12345"]),
("names", ["Test User"]),
("usernames", ["testuser"]),
(
"data",
[{"id": "12345", "name": "Test User", "username": "testuser"}],
),
],
test_mock={
"get_retweeters": lambda *args, **kwargs: (
[{"id": "12345", "name": "Test User", "username": "testuser"}],
{},
{},
["12345"],
["Test User"],
["testuser"],
None,
)
},
)
@staticmethod
def get_retweeters(
credentials: TwitterCredentials,
tweet_id: str,
max_results: int | None,
pagination_token: str | None,
expansions: UserExpansionsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": tweet_id,
"max_results": max_results,
"pagination_token": (
None if pagination_token == "" else pagination_token
),
"user_auth": False,
}
params = (
UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_retweeters(**params))
meta = {}
ids = []
names = []
usernames = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
ids = [str(user.id) for user in response.data]
names = [user.name for user in response.data]
usernames = [user.username for user in response.data]
return data, included, meta, ids, names, usernames, next_token
raise Exception("No retweeters found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
data, included, meta, ids, names, usernames, next_token = (
self.get_retweeters(
credentials,
input_data.tweet_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields,
)
)
if ids:
yield "ids", ids
if names:
yield "names", names
if usernames:
yield "usernames", usernames
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,757 +0,0 @@
from datetime import datetime
from typing import cast
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter._builders import (
TweetDurationBuilder,
TweetExpansionsBuilder,
)
from backend.blocks.twitter._serializer import (
IncludesSerializer,
ResponseDataSerializer,
)
from backend.blocks.twitter._types import (
ExpansionFilter,
TweetExpansionInputs,
TweetFieldsFilter,
TweetMediaFieldsFilter,
TweetPlaceFieldsFilter,
TweetPollFieldsFilter,
TweetTimeWindowInputs,
TweetUserFieldsFilter,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TwitterGetUserMentionsBlock(Block):
"""
Returns Tweets where a single user is mentioned, just put that user id
"""
class Input(TweetExpansionInputs, TweetTimeWindowInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
user_id: str = SchemaField(
description="Unique identifier of the user for whom to return Tweets mentioning the user",
placeholder="Enter user ID",
)
max_results: int | None = SchemaField(
description="Number of tweets to retrieve (5-100)",
default=10,
advanced=True,
)
pagination_token: str | None = SchemaField(
description="Token for pagination", default="", advanced=True
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
ids: list[str] = SchemaField(description="List of Tweet IDs")
texts: list[str] = SchemaField(description="All Tweet texts")
userIds: list[str] = SchemaField(
description="List of user ids that mentioned the user"
)
userNames: list[str] = SchemaField(
description="List of user names that mentioned the user"
)
next_token: str = SchemaField(description="Next token for pagination")
# Complete Outputs for advanced use
data: list[dict] = SchemaField(description="Complete Tweet data")
included: dict = SchemaField(
description="Additional data that you have requested (Optional) via Expansions field"
)
meta: dict = SchemaField(
description="Provides metadata such as pagination info (next_token) or result counts"
)
# error
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="e01c890c-a630-11ef-9e20-37da24888bd0",
description="This block retrieves Tweets mentioning a specific user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetUserMentionsBlock.Input,
output_schema=TwitterGetUserMentionsBlock.Output,
test_input={
"user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT,
"max_results": 2,
"start_time": "2024-12-14T18:30:00.000Z",
"end_time": "2024-12-17T18:30:00.000Z",
"since_id": "",
"until_id": "",
"sort_order": None,
"pagination_token": None,
"expansions": None,
"media_fields": None,
"place_fields": None,
"poll_fields": None,
"tweet_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["1373001119480344583", "1372627771717869568"]),
("texts", ["Test mention 1", "Test mention 2"]),
("userIds", ["67890", "67891"]),
("userNames", ["testuser1", "testuser2"]),
(
"data",
[
{"id": "1373001119480344583", "text": "Test mention 1"},
{"id": "1372627771717869568", "text": "Test mention 2"},
],
),
],
test_mock={
"get_mentions": lambda *args, **kwargs: (
["1373001119480344583", "1372627771717869568"],
["Test mention 1", "Test mention 2"],
["67890", "67891"],
["testuser1", "testuser2"],
[
{"id": "1373001119480344583", "text": "Test mention 1"},
{"id": "1372627771717869568", "text": "Test mention 2"},
],
{},
{},
None,
)
},
)
@staticmethod
def get_mentions(
credentials: TwitterCredentials,
user_id: str,
max_results: int | None,
start_time: datetime | None,
end_time: datetime | None,
since_id: str | None,
until_id: str | None,
sort_order: str | None,
pagination: str | None,
expansions: ExpansionFilter | None,
media_fields: TweetMediaFieldsFilter | None,
place_fields: TweetPlaceFieldsFilter | None,
poll_fields: TweetPollFieldsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": user_id,
"max_results": max_results,
"pagination_token": None if pagination == "" else pagination,
"user_auth": False,
}
# Adding expansions to params If required by the user
params = (
TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
# Adding time window to params If required by the user
params = (
TweetDurationBuilder(params)
.add_start_time(start_time)
.add_end_time(end_time)
.add_since_id(since_id)
.add_until_id(until_id)
.add_sort_order(sort_order)
.build()
)
response = cast(
Response,
client.get_users_mentions(**params),
)
if not response.data and not response.meta:
raise Exception("No tweets found")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
meta = response.meta or {}
next_token = meta.get("next_token", "")
tweet_ids = []
tweet_texts = []
user_ids = []
user_names = []
if response.data:
tweet_ids = [str(tweet.id) for tweet in response.data]
tweet_texts = [tweet.text for tweet in response.data]
if "users" in included:
user_ids = [str(user["id"]) for user in included["users"]]
user_names = [user["username"] for user in included["users"]]
return (
tweet_ids,
tweet_texts,
user_ids,
user_names,
data,
included,
meta,
next_token,
)
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, texts, user_ids, user_names, data, included, meta, next_token = (
self.get_mentions(
credentials,
input_data.user_id,
input_data.max_results,
input_data.start_time,
input_data.end_time,
input_data.since_id,
input_data.until_id,
input_data.sort_order,
input_data.pagination_token,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields,
)
)
if ids:
yield "ids", ids
if texts:
yield "texts", texts
if user_ids:
yield "userIds", user_ids
if user_names:
yield "userNames", user_names
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetHomeTimelineBlock(Block):
"""
Returns a collection of the most recent Tweets and Retweets posted by you and users you follow
"""
class Input(TweetExpansionInputs, TweetTimeWindowInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
max_results: int | None = SchemaField(
description="Number of tweets to retrieve (5-100)",
default=10,
advanced=True,
)
pagination_token: str | None = SchemaField(
description="Token for pagination", default="", advanced=True
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
ids: list[str] = SchemaField(description="List of Tweet IDs")
texts: list[str] = SchemaField(description="All Tweet texts")
userIds: list[str] = SchemaField(
description="List of user ids that authored the tweets"
)
userNames: list[str] = SchemaField(
description="List of user names that authored the tweets"
)
next_token: str = SchemaField(description="Next token for pagination")
# Complete Outputs for advanced use
data: list[dict] = SchemaField(description="Complete Tweet data")
included: dict = SchemaField(
description="Additional data that you have requested (Optional) via Expansions field"
)
meta: dict = SchemaField(
description="Provides metadata such as pagination info (next_token) or result counts"
)
# error
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="d222a070-a630-11ef-a18a-3f52f76c6962",
description="This block retrieves the authenticated user's home timeline.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetHomeTimelineBlock.Input,
output_schema=TwitterGetHomeTimelineBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"max_results": 2,
"start_time": "2024-12-14T18:30:00.000Z",
"end_time": "2024-12-17T18:30:00.000Z",
"since_id": None,
"until_id": None,
"sort_order": None,
"pagination_token": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["1373001119480344583", "1372627771717869568"]),
("texts", ["Test tweet 1", "Test tweet 2"]),
("userIds", ["67890", "67891"]),
("userNames", ["testuser1", "testuser2"]),
(
"data",
[
{"id": "1373001119480344583", "text": "Test tweet 1"},
{"id": "1372627771717869568", "text": "Test tweet 2"},
],
),
],
test_mock={
"get_timeline": lambda *args, **kwargs: (
["1373001119480344583", "1372627771717869568"],
["Test tweet 1", "Test tweet 2"],
["67890", "67891"],
["testuser1", "testuser2"],
[
{"id": "1373001119480344583", "text": "Test tweet 1"},
{"id": "1372627771717869568", "text": "Test tweet 2"},
],
{},
{},
None,
)
},
)
@staticmethod
def get_timeline(
credentials: TwitterCredentials,
max_results: int | None,
start_time: datetime | None,
end_time: datetime | None,
since_id: str | None,
until_id: str | None,
sort_order: str | None,
pagination: str | None,
expansions: ExpansionFilter | None,
media_fields: TweetMediaFieldsFilter | None,
place_fields: TweetPlaceFieldsFilter | None,
poll_fields: TweetPollFieldsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"max_results": max_results,
"pagination_token": None if pagination == "" else pagination,
"user_auth": False,
}
# Adding expansions to params If required by the user
params = (
TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
# Adding time window to params If required by the user
params = (
TweetDurationBuilder(params)
.add_start_time(start_time)
.add_end_time(end_time)
.add_since_id(since_id)
.add_until_id(until_id)
.add_sort_order(sort_order)
.build()
)
response = cast(
Response,
client.get_home_timeline(**params),
)
if not response.data and not response.meta:
raise Exception("No tweets found")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
meta = response.meta or {}
next_token = meta.get("next_token", "")
tweet_ids = []
tweet_texts = []
user_ids = []
user_names = []
if response.data:
tweet_ids = [str(tweet.id) for tweet in response.data]
tweet_texts = [tweet.text for tweet in response.data]
if "users" in included:
user_ids = [str(user["id"]) for user in included["users"]]
user_names = [user["username"] for user in included["users"]]
return (
tweet_ids,
tweet_texts,
user_ids,
user_names,
data,
included,
meta,
next_token,
)
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, texts, user_ids, user_names, data, included, meta, next_token = (
self.get_timeline(
credentials,
input_data.max_results,
input_data.start_time,
input_data.end_time,
input_data.since_id,
input_data.until_id,
input_data.sort_order,
input_data.pagination_token,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields,
)
)
if ids:
yield "ids", ids
if texts:
yield "texts", texts
if user_ids:
yield "userIds", user_ids
if user_names:
yield "userNames", user_names
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetUserTweetsBlock(Block):
"""
Returns Tweets composed by a single user, specified by the requested user ID
"""
class Input(TweetExpansionInputs, TweetTimeWindowInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
user_id: str = SchemaField(
description="Unique identifier of the Twitter account (user ID) for whom to return results",
placeholder="Enter user ID",
)
max_results: int | None = SchemaField(
description="Number of tweets to retrieve (5-100)",
default=10,
advanced=True,
)
pagination_token: str | None = SchemaField(
description="Token for pagination", default="", advanced=True
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
ids: list[str] = SchemaField(description="List of Tweet IDs")
texts: list[str] = SchemaField(description="All Tweet texts")
userIds: list[str] = SchemaField(
description="List of user ids that authored the tweets"
)
userNames: list[str] = SchemaField(
description="List of user names that authored the tweets"
)
next_token: str = SchemaField(description="Next token for pagination")
# Complete Outputs for advanced use
data: list[dict] = SchemaField(description="Complete Tweet data")
included: dict = SchemaField(
description="Additional data that you have requested (Optional) via Expansions field"
)
meta: dict = SchemaField(
description="Provides metadata such as pagination info (next_token) or result counts"
)
# error
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="c44c3ef2-a630-11ef-9ff7-eb7b5ea3a5cb",
description="This block retrieves Tweets composed by a single user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetUserTweetsBlock.Input,
output_schema=TwitterGetUserTweetsBlock.Output,
test_input={
"user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT,
"max_results": 2,
"start_time": "2024-12-14T18:30:00.000Z",
"end_time": "2024-12-17T18:30:00.000Z",
"since_id": None,
"until_id": None,
"sort_order": None,
"pagination_token": None,
"expansions": None,
"media_fields": None,
"place_fields": None,
"poll_fields": None,
"tweet_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["1373001119480344583", "1372627771717869568"]),
("texts", ["Test tweet 1", "Test tweet 2"]),
("userIds", ["67890", "67891"]),
("userNames", ["testuser1", "testuser2"]),
(
"data",
[
{"id": "1373001119480344583", "text": "Test tweet 1"},
{"id": "1372627771717869568", "text": "Test tweet 2"},
],
),
],
test_mock={
"get_user_tweets": lambda *args, **kwargs: (
["1373001119480344583", "1372627771717869568"],
["Test tweet 1", "Test tweet 2"],
["67890", "67891"],
["testuser1", "testuser2"],
[
{"id": "1373001119480344583", "text": "Test tweet 1"},
{"id": "1372627771717869568", "text": "Test tweet 2"},
],
{},
{},
None,
)
},
)
@staticmethod
def get_user_tweets(
credentials: TwitterCredentials,
user_id: str,
max_results: int | None,
start_time: datetime | None,
end_time: datetime | None,
since_id: str | None,
until_id: str | None,
sort_order: str | None,
pagination: str | None,
expansions: ExpansionFilter | None,
media_fields: TweetMediaFieldsFilter | None,
place_fields: TweetPlaceFieldsFilter | None,
poll_fields: TweetPollFieldsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": user_id,
"max_results": max_results,
"pagination_token": None if pagination == "" else pagination,
"user_auth": False,
}
# Adding expansions to params If required by the user
params = (
TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
# Adding time window to params If required by the user
params = (
TweetDurationBuilder(params)
.add_start_time(start_time)
.add_end_time(end_time)
.add_since_id(since_id)
.add_until_id(until_id)
.add_sort_order(sort_order)
.build()
)
response = cast(
Response,
client.get_users_tweets(**params),
)
if not response.data and not response.meta:
raise Exception("No tweets found")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
meta = response.meta or {}
next_token = meta.get("next_token", "")
tweet_ids = []
tweet_texts = []
user_ids = []
user_names = []
if response.data:
tweet_ids = [str(tweet.id) for tweet in response.data]
tweet_texts = [tweet.text for tweet in response.data]
if "users" in included:
user_ids = [str(user["id"]) for user in included["users"]]
user_names = [user["username"] for user in included["users"]]
return (
tweet_ids,
tweet_texts,
user_ids,
user_names,
data,
included,
meta,
next_token,
)
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, texts, user_ids, user_names, data, included, meta, next_token = (
self.get_user_tweets(
credentials,
input_data.user_id,
input_data.max_results,
input_data.start_time,
input_data.end_time,
input_data.since_id,
input_data.until_id,
input_data.sort_order,
input_data.pagination_token,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields,
)
)
if ids:
yield "ids", ids
if texts:
yield "texts", texts
if user_ids:
yield "userIds", user_ids
if user_names:
yield "userNames", user_names
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,361 +0,0 @@
from typing import cast
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter._builders import TweetExpansionsBuilder
from backend.blocks.twitter._serializer import (
IncludesSerializer,
ResponseDataSerializer,
)
from backend.blocks.twitter._types import (
ExpansionFilter,
TweetExpansionInputs,
TweetFieldsFilter,
TweetMediaFieldsFilter,
TweetPlaceFieldsFilter,
TweetPollFieldsFilter,
TweetUserFieldsFilter,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TwitterGetTweetBlock(Block):
"""
Returns information about a single Tweet specified by the requested ID
"""
class Input(TweetExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="Unique identifier of the Tweet to request (ex: 1460323737035677698)",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
id: str = SchemaField(description="Tweet ID")
text: str = SchemaField(description="Tweet text")
userId: str = SchemaField(description="ID of the tweet author")
userName: str = SchemaField(description="Username of the tweet author")
# Complete Outputs for advanced use
data: dict = SchemaField(description="Tweet data")
included: dict = SchemaField(
description="Additional data that you have requested (Optional) via Expansions field"
)
meta: dict = SchemaField(description="Metadata about the tweet")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="f5155c3a-a630-11ef-9cc1-a309988b4d92",
description="This block retrieves information about a specific Tweet.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetTweetBlock.Input,
output_schema=TwitterGetTweetBlock.Output,
test_input={
"tweet_id": "1460323737035677698",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"media_fields": None,
"place_fields": None,
"poll_fields": None,
"tweet_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "1460323737035677698"),
("text", "Test tweet content"),
("userId", "12345"),
("userName", "testuser"),
("data", {"id": "1460323737035677698", "text": "Test tweet content"}),
("included", {"users": [{"id": "12345", "username": "testuser"}]}),
("meta", {"result_count": 1}),
],
test_mock={
"get_tweet": lambda *args, **kwargs: (
{"id": "1460323737035677698", "text": "Test tweet content"},
{"users": [{"id": "12345", "username": "testuser"}]},
{"result_count": 1},
"12345",
"testuser",
)
},
)
@staticmethod
def get_tweet(
credentials: TwitterCredentials,
tweet_id: str,
expansions: ExpansionFilter | None,
media_fields: TweetMediaFieldsFilter | None,
place_fields: TweetPlaceFieldsFilter | None,
poll_fields: TweetPollFieldsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {"id": tweet_id, "user_auth": False}
# Adding expansions to params If required by the user
params = (
TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_tweet(**params))
meta = {}
user_id = ""
user_name = ""
if response.meta:
meta = response.meta
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_dict(response.data)
if included and "users" in included:
user_id = str(included["users"][0]["id"])
user_name = included["users"][0]["username"]
if response.data:
return data, included, meta, user_id, user_name
raise Exception("Tweet not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
tweet_data, included, meta, user_id, user_name = self.get_tweet(
credentials,
input_data.tweet_id,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields,
)
yield "id", str(tweet_data["id"])
yield "text", tweet_data["text"]
if user_id:
yield "userId", user_id
if user_name:
yield "userName", user_name
yield "data", tweet_data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetTweetsBlock(Block):
"""
Returns information about multiple Tweets specified by the requested IDs
"""
class Input(TweetExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
tweet_ids: list[str] = SchemaField(
description="List of Tweet IDs to request (up to 100)",
placeholder="Enter tweet IDs",
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
ids: list[str] = SchemaField(description="All Tweet IDs")
texts: list[str] = SchemaField(description="All Tweet texts")
userIds: list[str] = SchemaField(
description="List of user ids that authored the tweets"
)
userNames: list[str] = SchemaField(
description="List of user names that authored the tweets"
)
# Complete Outputs for advanced use
data: list[dict] = SchemaField(description="Complete Tweet data")
included: dict = SchemaField(
description="Additional data that you have requested (Optional) via Expansions field"
)
meta: dict = SchemaField(description="Metadata about the tweets")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="e7cc5420-a630-11ef-bfaf-13bdd8096a51",
description="This block retrieves information about multiple Tweets.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetTweetsBlock.Input,
output_schema=TwitterGetTweetsBlock.Output,
test_input={
"tweet_ids": ["1460323737035677698"],
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"media_fields": None,
"place_fields": None,
"poll_fields": None,
"tweet_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["1460323737035677698"]),
("texts", ["Test tweet content"]),
("userIds", ["67890"]),
("userNames", ["testuser1"]),
("data", [{"id": "1460323737035677698", "text": "Test tweet content"}]),
("included", {"users": [{"id": "67890", "username": "testuser1"}]}),
("meta", {"result_count": 1}),
],
test_mock={
"get_tweets": lambda *args, **kwargs: (
["1460323737035677698"], # ids
["Test tweet content"], # texts
["67890"], # user_ids
["testuser1"], # user_names
[
{"id": "1460323737035677698", "text": "Test tweet content"}
], # data
{"users": [{"id": "67890", "username": "testuser1"}]}, # included
{"result_count": 1}, # meta
)
},
)
@staticmethod
def get_tweets(
credentials: TwitterCredentials,
tweet_ids: list[str],
expansions: ExpansionFilter | None,
media_fields: TweetMediaFieldsFilter | None,
place_fields: TweetPlaceFieldsFilter | None,
poll_fields: TweetPollFieldsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {"ids": tweet_ids, "user_auth": False}
# Adding expansions to params If required by the user
params = (
TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_tweets(**params))
if not response.data and not response.meta:
raise Exception("No tweets found")
tweet_ids = []
tweet_texts = []
user_ids = []
user_names = []
meta = {}
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
tweet_ids = [str(tweet.id) for tweet in response.data]
tweet_texts = [tweet.text for tweet in response.data]
if included and "users" in included:
for user in included["users"]:
user_ids.append(str(user["id"]))
user_names.append(user["username"])
if response.meta:
meta = response.meta
return tweet_ids, tweet_texts, user_ids, user_names, data, included, meta
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, texts, user_ids, user_names, data, included, meta = self.get_tweets(
credentials,
input_data.tweet_ids,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields,
)
if ids:
yield "ids", ids
if texts:
yield "texts", texts
if user_ids:
yield "userIds", user_ids
if user_names:
yield "userNames", user_names
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,305 +0,0 @@
from typing import cast
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter._builders import UserExpansionsBuilder
from backend.blocks.twitter._serializer import IncludesSerializer
from backend.blocks.twitter._types import (
TweetFieldsFilter,
TweetUserFieldsFilter,
UserExpansionInputs,
UserExpansionsFilter,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TwitterUnblockUserBlock(Block):
"""
Unblock a specific user on Twitter. The request succeeds with no action when the user sends a request to a user they're not blocking or have already unblocked.
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["block.write", "users.read", "offline.access"]
)
target_user_id: str = SchemaField(
description="The user ID of the user that you would like to unblock",
placeholder="Enter target user ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the unblock was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="0f1b6570-a631-11ef-a3ea-230cbe9650dd",
description="This block unblocks a specific user on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterUnblockUserBlock.Input,
output_schema=TwitterUnblockUserBlock.Output,
test_input={
"target_user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"unblock_user": lambda *args, **kwargs: True},
)
@staticmethod
def unblock_user(credentials: TwitterCredentials, target_user_id: str):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.unblock(target_user_id=target_user_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.unblock_user(credentials, input_data.target_user_id)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetBlockedUsersBlock(Block):
"""
Get a list of users who are blocked by the authenticating user
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "offline.access", "block.read"]
)
max_results: int | None = SchemaField(
description="Maximum number of results to return (1-1000, default 100)",
placeholder="Enter max results",
default=10,
advanced=True,
)
pagination_token: str | None = SchemaField(
description="Token for retrieving next/previous page of results",
placeholder="Enter pagination token",
default="",
advanced=True,
)
class Output(BlockSchema):
user_ids: list[str] = SchemaField(description="List of blocked user IDs")
usernames_: list[str] = SchemaField(description="List of blocked usernames")
included: dict = SchemaField(
description="Additional data requested via expansions"
)
meta: dict = SchemaField(description="Metadata including pagination info")
next_token: str = SchemaField(description="Next token for pagination")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="05f409e8-a631-11ef-ae89-93de863ee30d",
description="This block retrieves a list of users blocked by the authenticating user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetBlockedUsersBlock.Input,
output_schema=TwitterGetBlockedUsersBlock.Output,
test_input={
"max_results": 10,
"pagination_token": "",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"tweet_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("user_ids", ["12345", "67890"]),
("usernames_", ["testuser1", "testuser2"]),
],
test_mock={
"get_blocked_users": lambda *args, **kwargs: (
{}, # included
{}, # meta
["12345", "67890"], # user_ids
["testuser1", "testuser2"], # usernames
None, # next_token
)
},
)
@staticmethod
def get_blocked_users(
credentials: TwitterCredentials,
max_results: int | None,
pagination_token: str | None,
expansions: UserExpansionsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"max_results": max_results,
"pagination_token": (
None if pagination_token == "" else pagination_token
),
"user_auth": False,
}
params = (
UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_blocked(**params))
meta = {}
user_ids = []
usernames = []
next_token = None
included = IncludesSerializer.serialize(response.includes)
if response.data:
for user in response.data:
user_ids.append(str(user.id))
usernames.append(user.username)
if response.meta:
meta = response.meta
if "next_token" in meta:
next_token = meta["next_token"]
if user_ids and usernames:
return included, meta, user_ids, usernames, next_token
else:
raise tweepy.TweepyException("No blocked users found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
included, meta, user_ids, usernames, next_token = self.get_blocked_users(
credentials,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields,
)
if user_ids:
yield "user_ids", user_ids
if usernames:
yield "usernames_", usernames
if included:
yield "included", included
if meta:
yield "meta", meta
if next_token:
yield "next_token", next_token
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterBlockUserBlock(Block):
"""
Block a specific user on Twitter
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["block.write", "users.read", "offline.access"]
)
target_user_id: str = SchemaField(
description="The user ID of the user that you would like to block",
placeholder="Enter target user ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the block was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="fc258b94-a630-11ef-abc3-df050b75b816",
description="This block blocks a specific user on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterBlockUserBlock.Input,
output_schema=TwitterBlockUserBlock.Output,
test_input={
"target_user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"block_user": lambda *args, **kwargs: True},
)
@staticmethod
def block_user(credentials: TwitterCredentials, target_user_id: str):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.block(target_user_id=target_user_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.block_user(credentials, input_data.target_user_id)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,510 +0,0 @@
from typing import cast
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter._builders import UserExpansionsBuilder
from backend.blocks.twitter._serializer import (
IncludesSerializer,
ResponseDataSerializer,
)
from backend.blocks.twitter._types import (
TweetFieldsFilter,
TweetUserFieldsFilter,
UserExpansionInputs,
UserExpansionsFilter,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TwitterUnfollowUserBlock(Block):
"""
Allows a user to unfollow another user specified by target user ID.
The request succeeds with no action when the authenticated user sends a request to a user they're not following or have already unfollowed.
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "users.write", "follows.write", "offline.access"]
)
target_user_id: str = SchemaField(
description="The user ID of the user that you would like to unfollow",
placeholder="Enter target user ID",
)
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the unfollow action was successful"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="37e386a4-a631-11ef-b7bd-b78204b35fa4",
description="This block unfollows a specified Twitter user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterUnfollowUserBlock.Input,
output_schema=TwitterUnfollowUserBlock.Output,
test_input={
"target_user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"unfollow_user": lambda *args, **kwargs: True},
)
@staticmethod
def unfollow_user(credentials: TwitterCredentials, target_user_id: str):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.unfollow_user(target_user_id=target_user_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.unfollow_user(credentials, input_data.target_user_id)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterFollowUserBlock(Block):
"""
Allows a user to follow another user specified by target user ID. If the target user does not have public Tweets,
this endpoint will send a follow request. The request succeeds with no action when the authenticated user sends a
request to a user they're already following, or if they're sending a follower request to a user that does not have
public Tweets.
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "users.write", "follows.write", "offline.access"]
)
target_user_id: str = SchemaField(
description="The user ID of the user that you would like to follow",
placeholder="Enter target user ID",
)
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the follow action was successful"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="1aae6a5e-a631-11ef-a090-435900c6d429",
description="This block follows a specified Twitter user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterFollowUserBlock.Input,
output_schema=TwitterFollowUserBlock.Output,
test_input={
"target_user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"follow_user": lambda *args, **kwargs: True},
)
@staticmethod
def follow_user(credentials: TwitterCredentials, target_user_id: str):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.follow_user(target_user_id=target_user_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.follow_user(credentials, input_data.target_user_id)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetFollowersBlock(Block):
"""
Retrieves a list of followers for a specified Twitter user ID
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "offline.access", "follows.read"]
)
target_user_id: str = SchemaField(
description="The user ID whose followers you would like to retrieve",
placeholder="Enter target user ID",
)
max_results: int | None = SchemaField(
description="Maximum number of results to return (1-1000, default 100)",
placeholder="Enter max results",
default=10,
advanced=True,
)
pagination_token: str | None = SchemaField(
description="Token for retrieving next/previous page of results",
placeholder="Enter pagination token",
default="",
advanced=True,
)
class Output(BlockSchema):
ids: list[str] = SchemaField(description="List of follower user IDs")
usernames: list[str] = SchemaField(description="List of follower usernames")
next_token: str = SchemaField(description="Next token for pagination")
data: list[dict] = SchemaField(description="Complete user data for followers")
includes: dict = SchemaField(
description="Additional data requested via expansions"
)
meta: dict = SchemaField(description="Metadata including pagination info")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="30f66410-a631-11ef-8fe7-d7f888b4f43c",
description="This block retrieves followers of a specified Twitter user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetFollowersBlock.Input,
output_schema=TwitterGetFollowersBlock.Output,
test_input={
"target_user_id": "12345",
"max_results": 1,
"pagination_token": "",
"expansions": None,
"tweet_fields": None,
"user_fields": None,
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["1234567890"]),
("usernames", ["testuser"]),
("data", [{"id": "1234567890", "username": "testuser"}]),
],
test_mock={
"get_followers": lambda *args, **kwargs: (
["1234567890"],
["testuser"],
[{"id": "1234567890", "username": "testuser"}],
{},
{},
None,
)
},
)
@staticmethod
def get_followers(
credentials: TwitterCredentials,
target_user_id: str,
max_results: int | None,
pagination_token: str | None,
expansions: UserExpansionsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": target_user_id,
"max_results": max_results,
"pagination_token": (
None if pagination_token == "" else pagination_token
),
"user_auth": False,
}
params = (
UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_users_followers(**params))
meta = {}
follower_ids = []
follower_usernames = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
follower_ids = [str(user.id) for user in response.data]
follower_usernames = [user.username for user in response.data]
return (
follower_ids,
follower_usernames,
data,
included,
meta,
next_token,
)
raise Exception("Followers not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, usernames, data, includes, meta, next_token = self.get_followers(
credentials,
input_data.target_user_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields,
)
if ids:
yield "ids", ids
if usernames:
yield "usernames", usernames
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if includes:
yield "includes", includes
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetFollowingBlock(Block):
"""
Retrieves a list of users that a specified Twitter user ID is following
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "offline.access", "follows.read"]
)
target_user_id: str = SchemaField(
description="The user ID whose following you would like to retrieve",
placeholder="Enter target user ID",
)
max_results: int | None = SchemaField(
description="Maximum number of results to return (1-1000, default 100)",
placeholder="Enter max results",
default=10,
advanced=True,
)
pagination_token: str | None = SchemaField(
description="Token for retrieving next/previous page of results",
placeholder="Enter pagination token",
default="",
advanced=True,
)
class Output(BlockSchema):
ids: list[str] = SchemaField(description="List of following user IDs")
usernames: list[str] = SchemaField(description="List of following usernames")
next_token: str = SchemaField(description="Next token for pagination")
data: list[dict] = SchemaField(description="Complete user data for following")
includes: dict = SchemaField(
description="Additional data requested via expansions"
)
meta: dict = SchemaField(description="Metadata including pagination info")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="264a399c-a631-11ef-a97d-bfde4ca91173",
description="This block retrieves the users that a specified Twitter user is following.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetFollowingBlock.Input,
output_schema=TwitterGetFollowingBlock.Output,
test_input={
"target_user_id": "12345",
"max_results": 1,
"pagination_token": None,
"expansions": None,
"tweet_fields": None,
"user_fields": None,
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["1234567890"]),
("usernames", ["testuser"]),
("data", [{"id": "1234567890", "username": "testuser"}]),
],
test_mock={
"get_following": lambda *args, **kwargs: (
["1234567890"],
["testuser"],
[{"id": "1234567890", "username": "testuser"}],
{},
{},
None,
)
},
)
@staticmethod
def get_following(
credentials: TwitterCredentials,
target_user_id: str,
max_results: int | None,
pagination_token: str | None,
expansions: UserExpansionsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": target_user_id,
"max_results": max_results,
"pagination_token": (
None if pagination_token == "" else pagination_token
),
"user_auth": False,
}
params = (
UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_users_following(**params))
meta = {}
following_ids = []
following_usernames = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
following_ids = [str(user.id) for user in response.data]
following_usernames = [user.username for user in response.data]
return (
following_ids,
following_usernames,
data,
included,
meta,
next_token,
)
raise Exception("Following not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, usernames, data, includes, meta, next_token = self.get_following(
credentials,
input_data.target_user_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields,
)
if ids:
yield "ids", ids
if usernames:
yield "usernames", usernames
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if includes:
yield "includes", includes
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,328 +0,0 @@
from typing import cast
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter._builders import UserExpansionsBuilder
from backend.blocks.twitter._serializer import (
IncludesSerializer,
ResponseDataSerializer,
)
from backend.blocks.twitter._types import (
TweetFieldsFilter,
TweetUserFieldsFilter,
UserExpansionInputs,
UserExpansionsFilter,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TwitterUnmuteUserBlock(Block):
"""
Allows a user to unmute another user specified by target user ID.
The request succeeds with no action when the user sends a request to a user they're not muting or have already unmuted.
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "users.write", "offline.access"]
)
target_user_id: str = SchemaField(
description="The user ID of the user that you would like to unmute",
placeholder="Enter target user ID",
)
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the unmute action was successful"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="40458504-a631-11ef-940b-eff92be55422",
description="This block unmutes a specified Twitter user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterUnmuteUserBlock.Input,
output_schema=TwitterUnmuteUserBlock.Output,
test_input={
"target_user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"unmute_user": lambda *args, **kwargs: True},
)
@staticmethod
def unmute_user(credentials: TwitterCredentials, target_user_id: str):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.unmute(target_user_id=target_user_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.unmute_user(credentials, input_data.target_user_id)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetMutedUsersBlock(Block):
"""
Returns a list of users who are muted by the authenticating user
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "offline.access"]
)
max_results: int | None = SchemaField(
description="The maximum number of results to be returned per page (1-1000). Default is 100.",
placeholder="Enter max results",
default=10,
advanced=True,
)
pagination_token: str | None = SchemaField(
description="Token to request next/previous page of results",
placeholder="Enter pagination token",
default="",
advanced=True,
)
class Output(BlockSchema):
ids: list[str] = SchemaField(description="List of muted user IDs")
usernames: list[str] = SchemaField(description="List of muted usernames")
next_token: str = SchemaField(description="Next token for pagination")
data: list[dict] = SchemaField(description="Complete user data for muted users")
includes: dict = SchemaField(
description="Additional data requested via expansions"
)
meta: dict = SchemaField(description="Metadata including pagination info")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="475024da-a631-11ef-9ccd-f724b8b03cda",
description="This block gets a list of users muted by the authenticating user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetMutedUsersBlock.Input,
output_schema=TwitterGetMutedUsersBlock.Output,
test_input={
"max_results": 2,
"pagination_token": "",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"tweet_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["12345", "67890"]),
("usernames", ["testuser1", "testuser2"]),
(
"data",
[
{"id": "12345", "username": "testuser1"},
{"id": "67890", "username": "testuser2"},
],
),
],
test_mock={
"get_muted_users": lambda *args, **kwargs: (
["12345", "67890"],
["testuser1", "testuser2"],
[
{"id": "12345", "username": "testuser1"},
{"id": "67890", "username": "testuser2"},
],
{},
{},
None,
)
},
)
@staticmethod
def get_muted_users(
credentials: TwitterCredentials,
max_results: int | None,
pagination_token: str | None,
expansions: UserExpansionsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"max_results": max_results,
"pagination_token": (
None if pagination_token == "" else pagination_token
),
"user_auth": False,
}
params = (
UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_muted(**params))
meta = {}
user_ids = []
usernames = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
user_ids = [str(item.id) for item in response.data]
usernames = [item.username for item in response.data]
return user_ids, usernames, data, included, meta, next_token
raise Exception("Muted users not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, usernames, data, includes, meta, next_token = self.get_muted_users(
credentials,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields,
)
if ids:
yield "ids", ids
if usernames:
yield "usernames", usernames
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if includes:
yield "includes", includes
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterMuteUserBlock(Block):
"""
Allows a user to mute another user specified by target user ID
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "users.write", "offline.access"]
)
target_user_id: str = SchemaField(
description="The user ID of the user that you would like to mute",
placeholder="Enter target user ID",
)
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the mute action was successful"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="4d1919d0-a631-11ef-90ab-3b73af9ce8f1",
description="This block mutes a specified Twitter user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterMuteUserBlock.Input,
output_schema=TwitterMuteUserBlock.Output,
test_input={
"target_user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"mute_user": lambda *args, **kwargs: True},
)
@staticmethod
def mute_user(credentials: TwitterCredentials, target_user_id: str):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.mute(target_user_id=target_user_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.mute_user(credentials, input_data.target_user_id)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -1,383 +0,0 @@
from typing import Literal, Union, cast
import tweepy
from pydantic import BaseModel
from tweepy.client import Response
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
from backend.blocks.twitter._builders import UserExpansionsBuilder
from backend.blocks.twitter._serializer import (
IncludesSerializer,
ResponseDataSerializer,
)
from backend.blocks.twitter._types import (
TweetFieldsFilter,
TweetUserFieldsFilter,
UserExpansionInputs,
UserExpansionsFilter,
)
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class UserId(BaseModel):
discriminator: Literal["user_id"]
user_id: str = SchemaField(description="The ID of the user to lookup", default="")
class Username(BaseModel):
discriminator: Literal["username"]
username: str = SchemaField(
description="The Twitter username (handle) of the user", default=""
)
class TwitterGetUserBlock(Block):
"""
Gets information about a single Twitter user specified by ID or username
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "offline.access"]
)
identifier: Union[UserId, Username] = SchemaField(
discriminator="discriminator",
description="Choose whether to identify the user by their unique Twitter ID or by their username",
advanced=False,
)
class Output(BlockSchema):
# Common outputs
id: str = SchemaField(description="User ID")
username_: str = SchemaField(description="User username")
name_: str = SchemaField(description="User name")
# Complete outputs
data: dict = SchemaField(description="Complete user data")
included: dict = SchemaField(
description="Additional data requested via expansions"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="5446db8e-a631-11ef-812a-cf315d373ee9",
description="This block retrieves information about a specified Twitter user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetUserBlock.Input,
output_schema=TwitterGetUserBlock.Output,
test_input={
"identifier": {"discriminator": "username", "username": "twitter"},
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"tweet_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "783214"),
("username_", "twitter"),
("name_", "Twitter"),
(
"data",
{
"user": {
"id": "783214",
"username": "twitter",
"name": "Twitter",
}
},
),
],
test_mock={
"get_user": lambda *args, **kwargs: (
{
"user": {
"id": "783214",
"username": "twitter",
"name": "Twitter",
}
},
{},
"twitter",
"783214",
"Twitter",
)
},
)
@staticmethod
def get_user(
credentials: TwitterCredentials,
identifier: Union[UserId, Username],
expansions: UserExpansionsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": identifier.user_id if isinstance(identifier, UserId) else None,
"username": (
identifier.username if isinstance(identifier, Username) else None
),
"user_auth": False,
}
params = (
UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_user(**params))
username = ""
id = ""
name = ""
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_dict(response.data)
if response.data:
username = response.data.username
id = str(response.data.id)
name = response.data.name
if username and id:
return data, included, username, id, name
else:
raise tweepy.TweepyException("User not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
data, included, username, id, name = self.get_user(
credentials,
input_data.identifier,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields,
)
if id:
yield "id", id
if username:
yield "username_", username
if name:
yield "name_", name
if data:
yield "data", data
if included:
yield "included", included
except Exception as e:
yield "error", handle_tweepy_exception(e)
class UserIdList(BaseModel):
discriminator: Literal["user_id_list"]
user_ids: list[str] = SchemaField(
description="List of user IDs to lookup (max 100)",
placeholder="Enter user IDs",
default=[],
advanced=False,
)
class UsernameList(BaseModel):
discriminator: Literal["username_list"]
usernames: list[str] = SchemaField(
description="List of Twitter usernames/handles to lookup (max 100)",
placeholder="Enter usernames",
default=[],
advanced=False,
)
class TwitterGetUsersBlock(Block):
"""
Gets information about multiple Twitter users specified by IDs or usernames
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "offline.access"]
)
identifier: Union[UserIdList, UsernameList] = SchemaField(
discriminator="discriminator",
description="Choose whether to identify users by their unique Twitter IDs or by their usernames",
advanced=False,
)
class Output(BlockSchema):
# Common outputs
ids: list[str] = SchemaField(description="User IDs")
usernames_: list[str] = SchemaField(description="User usernames")
names_: list[str] = SchemaField(description="User names")
# Complete outputs
data: list[dict] = SchemaField(description="Complete users data")
included: dict = SchemaField(
description="Additional data requested via expansions"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="5abc857c-a631-11ef-8cfc-f7b79354f7a1",
description="This block retrieves information about multiple Twitter users.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetUsersBlock.Input,
output_schema=TwitterGetUsersBlock.Output,
test_input={
"identifier": {
"discriminator": "username_list",
"usernames": ["twitter", "twitterdev"],
},
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": None,
"tweet_fields": None,
"user_fields": None,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["783214", "2244994945"]),
("usernames_", ["twitter", "twitterdev"]),
("names_", ["Twitter", "Twitter Dev"]),
(
"data",
[
{"id": "783214", "username": "twitter", "name": "Twitter"},
{
"id": "2244994945",
"username": "twitterdev",
"name": "Twitter Dev",
},
],
),
],
test_mock={
"get_users": lambda *args, **kwargs: (
[
{"id": "783214", "username": "twitter", "name": "Twitter"},
{
"id": "2244994945",
"username": "twitterdev",
"name": "Twitter Dev",
},
],
{},
["twitter", "twitterdev"],
["783214", "2244994945"],
["Twitter", "Twitter Dev"],
)
},
)
@staticmethod
def get_users(
credentials: TwitterCredentials,
identifier: Union[UserIdList, UsernameList],
expansions: UserExpansionsFilter | None,
tweet_fields: TweetFieldsFilter | None,
user_fields: TweetUserFieldsFilter | None,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"ids": (
",".join(identifier.user_ids)
if isinstance(identifier, UserIdList)
else None
),
"usernames": (
",".join(identifier.usernames)
if isinstance(identifier, UsernameList)
else None
),
"user_auth": False,
}
params = (
UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build()
)
response = cast(Response, client.get_users(**params))
usernames = []
ids = []
names = []
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
for user in response.data:
usernames.append(user.username)
ids.append(str(user.id))
names.append(user.name)
if usernames and ids:
return data, included, usernames, ids, names
else:
raise tweepy.TweepyException("Users not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
data, included, usernames, ids, names = self.get_users(
credentials,
input_data.identifier,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields,
)
if ids:
yield "ids", ids
if usernames:
yield "usernames_", usernames
if names:
yield "names_", names
if data:
yield "data", data
if included:
yield "included", included
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -22,10 +22,10 @@ from backend.util import json
from backend.util.settings import Config
from .model import (
CREDENTIALS_FIELD_NAME,
ContributorDetails,
Credentials,
CredentialsMetaInput,
is_credentials_field_name,
)
app_config = Config()
@@ -42,7 +42,6 @@ class BlockType(Enum):
OUTPUT = "Output"
NOTE = "Note"
WEBHOOK = "Webhook"
WEBHOOK_MANUAL = "Webhook (manual)"
AGENT = "Agent"
@@ -58,19 +57,15 @@ class BlockCategory(Enum):
COMMUNICATION = "Block that interacts with communication platforms."
DEVELOPER_TOOLS = "Developer tools such as GitHub blocks."
DATA = "Block that interacts with structured data."
HARDWARE = "Block that interacts with hardware."
AGENT = "Block that interacts with other agents."
CRM = "Block that interacts with CRM services."
SAFETY = (
"Block that provides AI safety mechanisms such as detecting harmful content"
)
def dict(self) -> dict[str, str]:
return {"category": self.name, "description": self.value}
class BlockSchema(BaseModel):
cached_jsonschema: ClassVar[dict[str, Any]]
cached_jsonschema: ClassVar[dict[str, Any]] = {}
@classmethod
def jsonschema(cls) -> dict[str, Any]:
@@ -95,11 +90,15 @@ class BlockSchema(BaseModel):
}
elif isinstance(obj, list):
return [ref_to_dict(item) for item in obj]
return obj
cls.cached_jsonschema = cast(dict[str, Any], ref_to_dict(model))
# Set default properties values
for field in cls.cached_jsonschema.get("properties", {}).values():
if isinstance(field, dict) and "advanced" not in field:
field["advanced"] = True
return cls.cached_jsonschema
@classmethod
@@ -141,38 +140,13 @@ class BlockSchema(BaseModel):
@classmethod
def __pydantic_init_subclass__(cls, **kwargs):
"""Validates the schema definition. Rules:
- Fields with annotation `CredentialsMetaInput` MUST be
named `credentials` or `*_credentials`
- Fields named `credentials` or `*_credentials` MUST be
of type `CredentialsMetaInput`
- Only one `CredentialsMetaInput` field may be present.
- This field MUST be called `credentials`.
- A field that is called `credentials` MUST be a `CredentialsMetaInput`.
"""
super().__pydantic_init_subclass__(**kwargs)
# Reset cached JSON schema to prevent inheriting it from parent class
cls.cached_jsonschema = {}
credentials_fields = cls.get_credentials_fields()
for field_name in cls.get_fields():
if is_credentials_field_name(field_name):
if field_name not in credentials_fields:
raise TypeError(
f"Credentials field '{field_name}' on {cls.__qualname__} "
f"is not of type {CredentialsMetaInput.__name__}"
)
credentials_fields[field_name].validate_credentials_field_schema(cls)
elif field_name in credentials_fields:
raise KeyError(
f"Credentials field '{field_name}' on {cls.__qualname__} "
"has invalid name: must be 'credentials' or *_credentials"
)
@classmethod
def get_credentials_fields(cls) -> dict[str, type[CredentialsMetaInput]]:
return {
field_name: info.annotation
credentials_fields = [
field_name
for field_name, info in cls.model_fields.items()
if (
inspect.isclass(info.annotation)
@@ -181,7 +155,27 @@ class BlockSchema(BaseModel):
CredentialsMetaInput,
)
)
}
]
if len(credentials_fields) > 1:
raise ValueError(
f"{cls.__qualname__} can only have one CredentialsMetaInput field"
)
elif (
len(credentials_fields) == 1
and credentials_fields[0] != CREDENTIALS_FIELD_NAME
):
raise ValueError(
f"CredentialsMetaInput field on {cls.__qualname__} "
"must be named 'credentials'"
)
elif (
len(credentials_fields) == 0
and CREDENTIALS_FIELD_NAME in cls.model_fields.keys()
):
raise TypeError(
f"Field 'credentials' on {cls.__qualname__} "
f"must be of type {CredentialsMetaInput.__name__}"
)
BlockSchemaInputType = TypeVar("BlockSchemaInputType", bound=BlockSchema)
@@ -193,12 +187,7 @@ class EmptySchema(BlockSchema):
# --8<-- [start:BlockWebhookConfig]
class BlockManualWebhookConfig(BaseModel):
"""
Configuration model for webhook-triggered blocks on which
the user has to manually set up the webhook at the provider.
"""
class BlockWebhookConfig(BaseModel):
provider: str
"""The service provider that the webhook connects to"""
@@ -209,27 +198,6 @@ class BlockManualWebhookConfig(BaseModel):
Only for use in the corresponding `WebhooksManager`.
"""
event_filter_input: str = ""
"""
Name of the block's event filter input.
Leave empty if the corresponding webhook doesn't have distinct event/payload types.
"""
event_format: str = "{event}"
"""
Template string for the event(s) that a block instance subscribes to.
Applied individually to each event selected in the event filter input.
Example: `"pull_request.{event}"` -> `"pull_request.opened"`
"""
class BlockWebhookConfig(BlockManualWebhookConfig):
"""
Configuration model for webhook-triggered blocks for which
the webhook can be automatically set up through the provider's API.
"""
resource_format: str
"""
Template string for the resource that a block instance subscribes to.
@@ -239,6 +207,17 @@ class BlockWebhookConfig(BlockManualWebhookConfig):
Only for use in the corresponding `WebhooksManager`.
"""
event_filter_input: str
"""Name of the block's event filter input."""
event_format: str = "{event}"
"""
Template string for the event(s) that a block instance subscribes to.
Applied individually to each event selected in the event filter input.
Example: `"pull_request.{event}"` -> `"pull_request.opened"`
"""
# --8<-- [end:BlockWebhookConfig]
@@ -254,11 +233,11 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]):
test_input: BlockInput | list[BlockInput] | None = None,
test_output: BlockData | list[BlockData] | None = None,
test_mock: dict[str, Any] | None = None,
test_credentials: Optional[Credentials | dict[str, Credentials]] = None,
test_credentials: Optional[Credentials] = None,
disabled: bool = False,
static_output: bool = False,
block_type: BlockType = BlockType.STANDARD,
webhook_config: Optional[BlockWebhookConfig | BlockManualWebhookConfig] = None,
webhook_config: Optional[BlockWebhookConfig] = None,
):
"""
Initialize the block with the given schema.
@@ -289,44 +268,27 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]):
self.contributors = contributors or set()
self.disabled = disabled
self.static_output = static_output
self.block_type = block_type
self.block_type = block_type if not webhook_config else BlockType.WEBHOOK
self.webhook_config = webhook_config
self.execution_stats = {}
if self.webhook_config:
if isinstance(self.webhook_config, BlockWebhookConfig):
# Enforce presence of credentials field on auto-setup webhook blocks
if not (cred_fields := self.input_schema.get_credentials_fields()):
raise TypeError(
"credentials field is required on auto-setup webhook blocks"
)
# Disallow multiple credentials inputs on webhook blocks
elif len(cred_fields) > 1:
raise ValueError(
"Multiple credentials inputs not supported on webhook blocks"
)
self.block_type = BlockType.WEBHOOK
else:
self.block_type = BlockType.WEBHOOK_MANUAL
# Enforce shape of webhook event filter, if present
if self.webhook_config.event_filter_input:
event_filter_field = self.input_schema.model_fields[
self.webhook_config.event_filter_input
]
if not (
isinstance(event_filter_field.annotation, type)
and issubclass(event_filter_field.annotation, BaseModel)
and all(
field.annotation is bool
for field in event_filter_field.annotation.model_fields.values()
)
):
raise NotImplementedError(
f"{self.name} has an invalid webhook event selector: "
"field must be a BaseModel and all its fields must be boolean"
)
# Enforce shape of webhook event filter
event_filter_field = self.input_schema.model_fields[
self.webhook_config.event_filter_input
]
if not (
isinstance(event_filter_field.annotation, type)
and issubclass(event_filter_field.annotation, BaseModel)
and all(
field.annotation is bool
for field in event_filter_field.annotation.model_fields.values()
)
):
raise NotImplementedError(
f"{self.name} has an invalid webhook event selector: "
"field must be a BaseModel and all its fields must be boolean"
)
# Enforce presence of 'payload' input
if "payload" not in self.input_schema.model_fields:

Some files were not shown because too many files have changed in this diff Show More