Merge branch 'dev' into swiftyos/open-2278-implement-agent-preset-functionality

This commit is contained in:
Swifty
2025-01-30 15:07:18 +01:00
committed by GitHub
67 changed files with 4710 additions and 664 deletions

18
.deepsource.toml Normal file
View File

@@ -0,0 +1,18 @@
version = 1
test_patterns = ["**/*.spec.ts","**/*_test.py","**/*_tests.py","**/test_*.py"]
exclude_patterns = ["classic/**"]
[[analyzers]]
name = "javascript"
[analyzers.meta]
plugins = ["react"]
environment = ["nodejs"]
[[analyzers]]
name = "python"
[analyzers.meta]
runtime_version = "3.x.x"

View File

@@ -37,6 +37,25 @@ jobs:
run: |
yarn lint
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "21"
- name: Install dependencies
run: |
yarn install --frozen-lockfile
- name: Run tsc check
run: |
yarn type-check
test:
runs-on: ubuntu-latest
strategy:

View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
[[package]]
name = "aiohappyeyeballs"
@@ -6,7 +6,6 @@ version = "2.4.0"
description = "Happy Eyeballs for asyncio"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "aiohappyeyeballs-2.4.0-py3-none-any.whl", hash = "sha256:7ce92076e249169a13c2f49320d1967425eaf1f407522d707d59cac7628d62bd"},
{file = "aiohappyeyeballs-2.4.0.tar.gz", hash = "sha256:55a1714f084e63d49639800f95716da97a1f173d46a16dfcfda0016abb93b6b2"},
@@ -18,7 +17,6 @@ version = "3.10.5"
description = "Async http client/server framework (asyncio)"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:18a01eba2574fb9edd5f6e5fb25f66e6ce061da5dab5db75e13fe1558142e0a3"},
{file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:94fac7c6e77ccb1ca91e9eb4cb0ac0270b9fb9b289738654120ba8cebb1189c6"},
@@ -131,7 +129,6 @@ version = "1.3.1"
description = "aiosignal: a list of registered asynchronous callbacks"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
@@ -146,7 +143,6 @@ version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
@@ -158,7 +154,6 @@ version = "4.4.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
{file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
@@ -181,12 +176,10 @@ version = "4.0.3"
description = "Timeout context manager for asyncio programs"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
files = [
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
]
markers = {main = "python_version < \"3.11\"", dev = "python_full_version < \"3.11.3\""}
[[package]]
name = "attrs"
@@ -194,7 +187,6 @@ version = "24.2.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"},
{file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"},
@@ -214,7 +206,6 @@ version = "5.5.0"
description = "Extensible memoizing collections and decorators"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"},
{file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"},
@@ -226,7 +217,6 @@ version = "2024.8.30"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
@@ -238,7 +228,6 @@ version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
groups = ["main"]
files = [
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
@@ -338,7 +327,6 @@ version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["main"]
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
@@ -350,7 +338,6 @@ version = "1.2.14"
description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
groups = ["main"]
files = [
{file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"},
{file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"},
@@ -368,7 +355,6 @@ version = "2.1.0"
description = "A library to handle automated deprecations"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"},
{file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"},
@@ -383,8 +369,6 @@ version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version < \"3.11\""
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
@@ -399,7 +383,6 @@ version = "1.2.2"
description = "Dictionary with auto-expiring values for caching purposes"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "expiringdict-1.2.2-py3-none-any.whl", hash = "sha256:09a5d20bc361163e6432a874edd3179676e935eb81b925eccef48d409a8a45e8"},
{file = "expiringdict-1.2.2.tar.gz", hash = "sha256:300fb92a7e98f15b05cf9a856c1415b3bc4f2e132be07daa326da6414c23ee09"},
@@ -414,7 +397,6 @@ version = "1.4.1"
description = "A list-like structure which implements collections.abc.MutableSequence"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"},
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"},
@@ -501,7 +483,6 @@ version = "2.19.2"
description = "Google API client core library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_api_core-2.19.2-py3-none-any.whl", hash = "sha256:53ec0258f2837dd53bbd3d3df50f5359281b3cc13f800c941dd15a9b5a415af4"},
{file = "google_api_core-2.19.2.tar.gz", hash = "sha256:ca07de7e8aa1c98a8bfca9321890ad2340ef7f2eb136e558cee68f24b94b0a8f"},
@@ -533,7 +514,6 @@ version = "2.34.0"
description = "Google Authentication Library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_auth-2.34.0-py2.py3-none-any.whl", hash = "sha256:72fd4733b80b6d777dcde515628a9eb4a577339437012874ea286bca7261ee65"},
{file = "google_auth-2.34.0.tar.gz", hash = "sha256:8eb87396435c19b20d32abd2f984e31c191a15284af72eb922f10e5bde9c04cc"},
@@ -557,7 +537,6 @@ version = "1.4.5"
description = "Google Cloud Appengine Logging API client library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_cloud_appengine_logging-1.4.5-py2.py3-none-any.whl", hash = "sha256:344e0244404049b42164e4d6dc718ca2c81b393d066956e7cb85fd9407ed9c48"},
{file = "google_cloud_appengine_logging-1.4.5.tar.gz", hash = "sha256:de7d766e5d67b19fc5833974b505b32d2a5bbdfb283fd941e320e7cfdae4cb83"},
@@ -575,7 +554,6 @@ version = "0.3.0"
description = "Google Cloud Audit Protos"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_cloud_audit_log-0.3.0-py2.py3-none-any.whl", hash = "sha256:8340793120a1d5aa143605def8704ecdcead15106f754ef1381ae3bab533722f"},
{file = "google_cloud_audit_log-0.3.0.tar.gz", hash = "sha256:901428b257020d8c1d1133e0fa004164a555e5a395c7ca3cdbb8486513df3a65"},
@@ -591,7 +569,6 @@ version = "2.4.1"
description = "Google Cloud API client core library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google-cloud-core-2.4.1.tar.gz", hash = "sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073"},
{file = "google_cloud_core-2.4.1-py2.py3-none-any.whl", hash = "sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61"},
@@ -610,7 +587,6 @@ version = "3.11.3"
description = "Stackdriver Logging API client library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_cloud_logging-3.11.3-py2.py3-none-any.whl", hash = "sha256:b8ec23f2998f76a58f8492db26a0f4151dd500425c3f08448586b85972f3c494"},
{file = "google_cloud_logging-3.11.3.tar.gz", hash = "sha256:0a73cd94118875387d4535371d9e9426861edef8e44fba1261e86782d5b8d54f"},
@@ -636,7 +612,6 @@ version = "1.65.0"
description = "Common protobufs used in Google APIs"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "googleapis_common_protos-1.65.0-py2.py3-none-any.whl", hash = "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63"},
{file = "googleapis_common_protos-1.65.0.tar.gz", hash = "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0"},
@@ -655,7 +630,6 @@ version = "2.11.1"
description = "Python Client Library for Supabase Auth"
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "gotrue-2.11.1-py3-none-any.whl", hash = "sha256:1b2d915bdc65fd0ad608532759ce9c72fa2e910145c1e6901f2188519e7bcd2d"},
{file = "gotrue-2.11.1.tar.gz", hash = "sha256:5594ceee60bd873e5f4fdd028b08dece3906f6013b6ed08e7786b71c0092fed0"},
@@ -671,7 +645,6 @@ version = "0.13.1"
description = "IAM API client library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "grpc-google-iam-v1-0.13.1.tar.gz", hash = "sha256:3ff4b2fd9d990965e410965253c0da6f66205d5a8291c4c31c6ebecca18a9001"},
{file = "grpc_google_iam_v1-0.13.1-py2.py3-none-any.whl", hash = "sha256:c3e86151a981811f30d5e7330f271cee53e73bb87755e88cc3b6f0c7b5fe374e"},
@@ -688,7 +661,6 @@ version = "1.66.1"
description = "HTTP/2-based RPC framework"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "grpcio-1.66.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:4877ba180591acdf127afe21ec1c7ff8a5ecf0fe2600f0d3c50e8c4a1cbc6492"},
{file = "grpcio-1.66.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:3750c5a00bd644c75f4507f77a804d0189d97a107eb1481945a0cf3af3e7a5ac"},
@@ -747,7 +719,6 @@ version = "1.66.1"
description = "Status proto mapping for gRPC"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "grpcio_status-1.66.1-py3-none-any.whl", hash = "sha256:cf9ed0b4a83adbe9297211c95cb5488b0cd065707e812145b842c85c4782ff02"},
{file = "grpcio_status-1.66.1.tar.gz", hash = "sha256:b3f7d34ccc46d83fea5261eea3786174459f763c31f6e34f1d24eba6d515d024"},
@@ -764,7 +735,6 @@ version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
@@ -776,7 +746,6 @@ version = "4.1.0"
description = "HTTP/2 State-Machine based protocol implementation"
optional = false
python-versions = ">=3.6.1"
groups = ["main"]
files = [
{file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"},
{file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"},
@@ -792,7 +761,6 @@ version = "4.0.0"
description = "Pure-Python HPACK header compression"
optional = false
python-versions = ">=3.6.1"
groups = ["main"]
files = [
{file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"},
{file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"},
@@ -804,7 +772,6 @@ version = "1.0.5"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
{file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
@@ -826,7 +793,6 @@ version = "0.27.2"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
{file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
@@ -853,7 +819,6 @@ version = "6.0.1"
description = "HTTP/2 framing layer for Python"
optional = false
python-versions = ">=3.6.1"
groups = ["main"]
files = [
{file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"},
{file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"},
@@ -865,7 +830,6 @@ version = "3.8"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"},
{file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"},
@@ -877,7 +841,6 @@ version = "8.4.0"
description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"},
{file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"},
@@ -897,7 +860,6 @@ version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
@@ -909,7 +871,6 @@ version = "6.1.0"
description = "multidict implementation"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"},
{file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"},
@@ -1014,7 +975,6 @@ version = "1.27.0"
description = "OpenTelemetry Python API"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "opentelemetry_api-1.27.0-py3-none-any.whl", hash = "sha256:953d5871815e7c30c81b56d910c707588000fff7a3ca1c73e6531911d53065e7"},
{file = "opentelemetry_api-1.27.0.tar.gz", hash = "sha256:ed673583eaa5f81b5ce5e86ef7cdaf622f88ef65f0b9aab40b843dcae5bef342"},
@@ -1030,7 +990,6 @@ version = "24.1"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
@@ -1042,7 +1001,6 @@ version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
@@ -1058,7 +1016,6 @@ version = "0.19.1"
description = "PostgREST client for Python. This library provides an ORM interface to PostgREST."
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "postgrest-0.19.1-py3-none-any.whl", hash = "sha256:a8e7be4e1abc69fd8eee5a49d7dc3a76dfbffbd778beed0b2bd7accb3f4f3a2a"},
{file = "postgrest-0.19.1.tar.gz", hash = "sha256:d8fa88953cced4f45efa0f412056c364f64ece8a35b5b35f458a7e58c133fbca"},
@@ -1076,7 +1033,6 @@ version = "1.24.0"
description = "Beautiful, Pythonic protocol buffers."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "proto-plus-1.24.0.tar.gz", hash = "sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445"},
{file = "proto_plus-1.24.0-py3-none-any.whl", hash = "sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12"},
@@ -1094,7 +1050,6 @@ version = "5.28.0"
description = ""
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "protobuf-5.28.0-cp310-abi3-win32.whl", hash = "sha256:66c3edeedb774a3508ae70d87b3a19786445fe9a068dd3585e0cefa8a77b83d0"},
{file = "protobuf-5.28.0-cp310-abi3-win_amd64.whl", hash = "sha256:6d7cc9e60f976cf3e873acb9a40fed04afb5d224608ed5c1a105db4a3f09c5b6"},
@@ -1115,7 +1070,6 @@ version = "0.6.1"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"},
{file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"},
@@ -1127,7 +1081,6 @@ version = "0.4.1"
description = "A collection of ASN.1-based protocols modules"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"},
{file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"},
@@ -1142,7 +1095,6 @@ version = "2.10.5"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53"},
{file = "pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff"},
@@ -1163,7 +1115,6 @@ version = "2.27.2"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"},
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"},
@@ -1276,7 +1227,6 @@ version = "2.7.1"
description = "Settings management using Pydantic"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd"},
{file = "pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93"},
@@ -1297,7 +1247,6 @@ version = "2.10.1"
description = "JSON Web Token implementation in Python"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"},
{file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"},
@@ -1315,7 +1264,6 @@ version = "8.3.3"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
{file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
@@ -1338,7 +1286,6 @@ version = "0.25.2"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075"},
{file = "pytest_asyncio-0.25.2.tar.gz", hash = "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f"},
@@ -1357,7 +1304,6 @@ version = "3.14.0"
description = "Thin-wrapper around the mock package for easier use with pytest"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"},
{file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"},
@@ -1375,7 +1321,6 @@ version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
groups = ["main"]
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
@@ -1390,7 +1335,6 @@ version = "1.0.1"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
@@ -1405,7 +1349,6 @@ version = "2.0.2"
description = ""
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "realtime-2.0.2-py3-none-any.whl", hash = "sha256:2634c915bc38807f2013f21e8bcc4d2f79870dfd81460ddb9393883d0489928a"},
{file = "realtime-2.0.2.tar.gz", hash = "sha256:519da9325b3b8102139d51785013d592f6b2403d81fa21d838a0b0234723ed7d"},
@@ -1423,7 +1366,6 @@ version = "5.2.1"
description = "Python client for Redis database and key-value store"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"},
{file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"},
@@ -1442,7 +1384,6 @@ version = "2.32.3"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
@@ -1464,7 +1405,6 @@ version = "4.9"
description = "Pure-Python RSA implementation"
optional = false
python-versions = ">=3.6,<4"
groups = ["main"]
files = [
{file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"},
{file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"},
@@ -1475,30 +1415,29 @@ pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
version = "0.9.2"
version = "0.9.3"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347"},
{file = "ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00"},
{file = "ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247"},
{file = "ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e"},
{file = "ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe"},
{file = "ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb"},
{file = "ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a"},
{file = "ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145"},
{file = "ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5"},
{file = "ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6"},
{file = "ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0"},
{file = "ruff-0.9.3-py3-none-linux_armv6l.whl", hash = "sha256:7f39b879064c7d9670197d91124a75d118d00b0990586549949aae80cdc16624"},
{file = "ruff-0.9.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a187171e7c09efa4b4cc30ee5d0d55a8d6c5311b3e1b74ac5cb96cc89bafc43c"},
{file = "ruff-0.9.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c59ab92f8e92d6725b7ded9d4a31be3ef42688a115c6d3da9457a5bda140e2b4"},
{file = "ruff-0.9.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc153c25e715be41bb228bc651c1e9b1a88d5c6e5ed0194fa0dfea02b026439"},
{file = "ruff-0.9.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:646909a1e25e0dc28fbc529eab8eb7bb583079628e8cbe738192853dbbe43af5"},
{file = "ruff-0.9.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a5a46e09355695fbdbb30ed9889d6cf1c61b77b700a9fafc21b41f097bfbba4"},
{file = "ruff-0.9.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c4bb09d2bbb394e3730d0918c00276e79b2de70ec2a5231cd4ebb51a57df9ba1"},
{file = "ruff-0.9.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96a87ec31dc1044d8c2da2ebbed1c456d9b561e7d087734336518181b26b3aa5"},
{file = "ruff-0.9.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb7554aca6f842645022fe2d301c264e6925baa708b392867b7a62645304df4"},
{file = "ruff-0.9.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabc332b7075a914ecea912cd1f3d4370489c8018f2c945a30bcc934e3bc06a6"},
{file = "ruff-0.9.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:33866c3cc2a575cbd546f2cd02bdd466fed65118e4365ee538a3deffd6fcb730"},
{file = "ruff-0.9.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:006e5de2621304c8810bcd2ee101587712fa93b4f955ed0985907a36c427e0c2"},
{file = "ruff-0.9.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ba6eea4459dbd6b1be4e6bfc766079fb9b8dd2e5a35aff6baee4d9b1514ea519"},
{file = "ruff-0.9.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:90230a6b8055ad47d3325e9ee8f8a9ae7e273078a66401ac66df68943ced029b"},
{file = "ruff-0.9.3-py3-none-win32.whl", hash = "sha256:eabe5eb2c19a42f4808c03b82bd313fc84d4e395133fb3fc1b1516170a31213c"},
{file = "ruff-0.9.3-py3-none-win_amd64.whl", hash = "sha256:040ceb7f20791dfa0e78b4230ee9dce23da3b64dd5848e40e3bf3ab76468dcf4"},
{file = "ruff-0.9.3-py3-none-win_arm64.whl", hash = "sha256:800d773f6d4d33b0a3c60e2c6ae8f4c202ea2de056365acfa519aa48acf28e0b"},
{file = "ruff-0.9.3.tar.gz", hash = "sha256:8293f89985a090ebc3ed1064df31f3b4b56320cdfcec8b60d3295bddb955c22a"},
]
[[package]]
@@ -1507,7 +1446,6 @@ version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
groups = ["main"]
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
@@ -1519,7 +1457,6 @@ version = "1.3.1"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
@@ -1531,7 +1468,6 @@ version = "0.11.0"
description = "Supabase Storage client for Python."
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "storage3-0.11.0-py3-none-any.whl", hash = "sha256:de2d8f9c9103ca91a9a9d0d69d80b07a3ab6f647b93e023e6a1a97d3607b9728"},
{file = "storage3-0.11.0.tar.gz", hash = "sha256:243583f2180686c0f0a19e6117d8a9796fd60c0ca72ec567d62b75a5af0d57a1"},
@@ -1547,7 +1483,6 @@ version = "0.4.15"
description = "An Enum that inherits from str."
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659"},
{file = "StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff"},
@@ -1564,7 +1499,6 @@ version = "2.11.0"
description = "Supabase client for Python."
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "supabase-2.11.0-py3-none-any.whl", hash = "sha256:67a0da498895f4cd6554935e2854b4c41f87b297b78fb9c9414902a382041406"},
{file = "supabase-2.11.0.tar.gz", hash = "sha256:2a906f7909fd9a50f944cd9332ce66c684e2d37c0864284d34c5815e6c63cc01"},
@@ -1584,7 +1518,6 @@ version = "0.9.2"
description = "Library for Supabase Functions"
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "supafunc-0.9.2-py3-none-any.whl", hash = "sha256:be5ee9f53842c4b0ba5f4abfb5bddf9f9e37e69e755ec0526852bb15af9d2ff5"},
{file = "supafunc-0.9.2.tar.gz", hash = "sha256:f5164114a3e65e7e552539f3f1050aa3d4970885abdd7405555c17fd216e2da1"},
@@ -1600,8 +1533,6 @@ version = "2.1.0"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version < \"3.11\""
files = [
{file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"},
{file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"},
@@ -1613,7 +1544,6 @@ version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
@@ -1625,7 +1555,6 @@ version = "2.2.2"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
@@ -1643,7 +1572,6 @@ version = "12.0"
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"},
{file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"},
@@ -1725,7 +1653,6 @@ version = "1.16.0"
description = "Module for decorators, wrappers and monkey patching."
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"},
{file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"},
@@ -1805,7 +1732,6 @@ version = "1.11.1"
description = "Yet another URL library"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "yarl-1.11.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:400cd42185f92de559d29eeb529e71d80dfbd2f45c36844914a4a34297ca6f00"},
{file = "yarl-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8258c86f47e080a258993eed877d579c71da7bda26af86ce6c2d2d072c11320d"},
@@ -1911,7 +1837,6 @@ version = "3.20.1"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064"},
{file = "zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"},
@@ -1926,6 +1851,6 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
lock-version = "2.0"
python-versions = ">=3.10,<4.0"
content-hash = "53a31ce3d94999d9267f2a229c53a9d97d96c9413843bfdcb7ef0c0c21723e49"
content-hash = "62c4ef3f1ae73546a66783a2dc0672e664ce415a7b0514a7b92fc9fe1a23239e"

View File

@@ -21,7 +21,7 @@ supabase = "^2.11.0"
[tool.poetry.group.dev.dependencies]
redis = "^5.2.1"
ruff = "^0.9.2"
ruff = "^0.9.3"
[build-system]
requires = ["poetry-core"]

View File

@@ -82,6 +82,14 @@ TWITTER_CLIENT_SECRET=
LINEAR_CLIENT_ID=
LINEAR_CLIENT_SECRET=
# To obtain Todoist API credentials:
# 1. Create a Todoist account at todoist.com
# 2. Visit the Developer Console: https://developer.todoist.com/appconsole.html
# 3. Click "Create new app"
# 4. Once created, copy your Client ID and Client Secret below
TODOIST_CLIENT_ID=
TODOIST_CLIENT_SECRET=
## ===== OPTIONAL API KEYS ===== ##
# LLM

View File

@@ -252,3 +252,31 @@ class TextSplitBlock(Block):
if input_data.strip:
texts = [text.strip() for text in texts]
yield "texts", texts
class TextReplaceBlock(Block):
class Input(BlockSchema):
text: str = SchemaField(description="The text to replace.")
old: str = SchemaField(description="The old text to replace.")
new: str = SchemaField(description="The new text to replace with.")
class Output(BlockSchema):
output: str = SchemaField(description="The text with the replaced text.")
def __init__(self):
super().__init__(
id="7e7c87ab-3469-4bcc-9abe-67705091b713",
description="This block is used to replace a text with a new text.",
categories={BlockCategory.TEXT},
input_schema=TextReplaceBlock.Input,
output_schema=TextReplaceBlock.Output,
test_input=[
{"text": "Hello, World!", "old": "Hello", "new": "Hi"},
],
test_output=[
("output", "Hi, World!"),
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "output", input_data.text.replace(input_data.old, input_data.new)

View File

@@ -0,0 +1,61 @@
from typing import Literal
from pydantic import SecretStr
from backend.data.model import (
CredentialsField,
CredentialsMetaInput,
OAuth2Credentials,
ProviderName,
)
from backend.integrations.oauth.todoist import TodoistOAuthHandler
from backend.util.settings import Secrets
secrets = Secrets()
TODOIST_OAUTH_IS_CONFIGURED = bool(
secrets.todoist_client_id and secrets.todoist_client_secret
)
TodoistCredentials = OAuth2Credentials
TodoistCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.TODOIST], Literal["oauth2"]
]
def TodoistCredentialsField(scopes: list[str]) -> TodoistCredentialsInput:
"""
Creates a Todoist credentials input on a block.
Params:
scopes: The authorization scopes needed for the block to work.
"""
return CredentialsField(
required_scopes=set(TodoistOAuthHandler.DEFAULT_SCOPES + scopes),
description="The Todoist integration requires OAuth2 authentication.",
)
TEST_CREDENTIALS = OAuth2Credentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="todoist",
access_token=SecretStr("mock-todoist-access-token"),
refresh_token=None,
access_token_expires_at=None,
scopes=[
"task:add",
"data:read",
"data:read_write",
"data:delete",
"project:delete",
],
title="Mock Todoist OAuth2 Credentials",
username="mock-todoist-username",
refresh_token_expires_at=None,
)
TEST_CREDENTIALS_INPUT = {
"provider": TEST_CREDENTIALS.provider,
"id": TEST_CREDENTIALS.id,
"type": TEST_CREDENTIALS.type,
"title": TEST_CREDENTIALS.title,
}

View File

@@ -0,0 +1,24 @@
from enum import Enum
class Colors(Enum):
berry_red = "berry_red"
red = "red"
orange = "orange"
yellow = "yellow"
olive_green = "olive_green"
lime_green = "lime_green"
green = "green"
mint_green = "mint_green"
teal = "teal"
sky_blue = "sky_blue"
light_blue = "light_blue"
blue = "blue"
grape = "grape"
violet = "violet"
lavender = "lavender"
magenta = "magenta"
salmon = "salmon"
charcoal = "charcoal"
grey = "grey"
taupe = "taupe"

View File

@@ -0,0 +1,439 @@
from typing import Literal, Union
from pydantic import BaseModel
from todoist_api_python.api import TodoistAPI
from typing_extensions import Optional
from backend.blocks.todoist._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TodoistCredentials,
TodoistCredentialsField,
TodoistCredentialsInput,
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TaskId(BaseModel):
discriminator: Literal["task"]
task_id: str
class ProjectId(BaseModel):
discriminator: Literal["project"]
project_id: str
class TodoistCreateCommentBlock(Block):
"""Creates a new comment on a Todoist task or project"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
content: str = SchemaField(description="Comment content")
id_type: Union[TaskId, ProjectId] = SchemaField(
discriminator="discriminator",
description="Specify either task_id or project_id to comment on",
default=TaskId(discriminator="task", task_id=""),
advanced=False,
)
attachment: Optional[dict] = SchemaField(
description="Optional file attachment", default=None
)
class Output(BlockSchema):
id: str = SchemaField(description="ID of created comment")
content: str = SchemaField(description="Comment content")
posted_at: str = SchemaField(description="Comment timestamp")
task_id: Optional[str] = SchemaField(
description="Associated task ID", default=None
)
project_id: Optional[str] = SchemaField(
description="Associated project ID", default=None
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="1bba7e54-2310-4a31-8e6f-54d5f9ab7459",
description="Creates a new comment on a Todoist task or project",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistCreateCommentBlock.Input,
output_schema=TodoistCreateCommentBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"content": "Test comment",
"id_type": {"discriminator": "task", "task_id": "2995104339"},
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "2992679862"),
("content", "Test comment"),
("posted_at", "2016-09-22T07:00:00.000000Z"),
("task_id", "2995104339"),
("project_id", None),
],
test_mock={
"create_comment": lambda content, credentials, task_id=None, project_id=None, attachment=None: {
"id": "2992679862",
"content": "Test comment",
"posted_at": "2016-09-22T07:00:00.000000Z",
"task_id": "2995104339",
"project_id": None,
}
},
)
@staticmethod
def create_comment(
credentials: TodoistCredentials,
content: str,
task_id: Optional[str] = None,
project_id: Optional[str] = None,
attachment: Optional[dict] = None,
):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
comment = api.add_comment(
content=content,
task_id=task_id,
project_id=project_id,
attachment=attachment,
)
return comment.__dict__
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
task_id = None
project_id = None
if isinstance(input_data.id_type, TaskId):
task_id = input_data.id_type.task_id
else:
project_id = input_data.id_type.project_id
comment_data = self.create_comment(
credentials,
input_data.content,
task_id=task_id,
project_id=project_id,
attachment=input_data.attachment,
)
if comment_data:
yield "id", comment_data["id"]
yield "content", comment_data["content"]
yield "posted_at", comment_data["posted_at"]
yield "task_id", comment_data["task_id"]
yield "project_id", comment_data["project_id"]
except Exception as e:
yield "error", str(e)
class TodoistGetCommentsBlock(Block):
"""Get all comments for a Todoist task or project"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
id_type: Union[TaskId, ProjectId] = SchemaField(
discriminator="discriminator",
description="Specify either task_id or project_id to get comments for",
default=TaskId(discriminator="task", task_id=""),
advanced=False,
)
class Output(BlockSchema):
comments: list = SchemaField(description="List of comments")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="9972d8ae-ddf2-11ef-a9b8-32d3674e8b7e",
description="Get all comments for a Todoist task or project",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistGetCommentsBlock.Input,
output_schema=TodoistGetCommentsBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"id_type": {"discriminator": "task", "task_id": "2995104339"},
},
test_credentials=TEST_CREDENTIALS,
test_output=[
(
"comments",
[
{
"id": "2992679862",
"content": "Test comment",
"posted_at": "2016-09-22T07:00:00.000000Z",
"task_id": "2995104339",
"project_id": None,
"attachment": None,
}
],
)
],
test_mock={
"get_comments": lambda credentials, task_id=None, project_id=None: [
{
"id": "2992679862",
"content": "Test comment",
"posted_at": "2016-09-22T07:00:00.000000Z",
"task_id": "2995104339",
"project_id": None,
"attachment": None,
}
]
},
)
@staticmethod
def get_comments(
credentials: TodoistCredentials,
task_id: Optional[str] = None,
project_id: Optional[str] = None,
):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
comments = api.get_comments(task_id=task_id, project_id=project_id)
return [comment.__dict__ for comment in comments]
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
task_id = None
project_id = None
if isinstance(input_data.id_type, TaskId):
task_id = input_data.id_type.task_id
else:
project_id = input_data.id_type.project_id
comments = self.get_comments(
credentials, task_id=task_id, project_id=project_id
)
yield "comments", comments
except Exception as e:
yield "error", str(e)
class TodoistGetCommentBlock(Block):
"""Get a single comment from Todoist using comment ID"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
comment_id: str = SchemaField(description="Comment ID to retrieve")
class Output(BlockSchema):
content: str = SchemaField(description="Comment content")
id: str = SchemaField(description="Comment ID")
posted_at: str = SchemaField(description="Comment timestamp")
project_id: Optional[str] = SchemaField(
description="Associated project ID", default=None
)
task_id: Optional[str] = SchemaField(
description="Associated task ID", default=None
)
attachment: Optional[dict] = SchemaField(
description="Optional file attachment", default=None
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="a809d264-ddf2-11ef-9764-32d3674e8b7e",
description="Get a single comment from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistGetCommentBlock.Input,
output_schema=TodoistGetCommentBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"comment_id": "2992679862",
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("content", "Test comment"),
("id", "2992679862"),
("posted_at", "2016-09-22T07:00:00.000000Z"),
("project_id", None),
("task_id", "2995104339"),
("attachment", None),
],
test_mock={
"get_comment": lambda credentials, comment_id: {
"content": "Test comment",
"id": "2992679862",
"posted_at": "2016-09-22T07:00:00.000000Z",
"project_id": None,
"task_id": "2995104339",
"attachment": None,
}
},
)
@staticmethod
def get_comment(credentials: TodoistCredentials, comment_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
comment = api.get_comment(comment_id=comment_id)
return comment.__dict__
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
comment_data = self.get_comment(
credentials, comment_id=input_data.comment_id
)
if comment_data:
yield "content", comment_data["content"]
yield "id", comment_data["id"]
yield "posted_at", comment_data["posted_at"]
yield "project_id", comment_data["project_id"]
yield "task_id", comment_data["task_id"]
yield "attachment", comment_data["attachment"]
except Exception as e:
yield "error", str(e)
class TodoistUpdateCommentBlock(Block):
"""Updates a Todoist comment"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
comment_id: str = SchemaField(description="Comment ID to update")
content: str = SchemaField(description="New content for the comment")
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="b773c520-ddf2-11ef-9f34-32d3674e8b7e",
description="Updates a Todoist comment",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistUpdateCommentBlock.Input,
output_schema=TodoistUpdateCommentBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"comment_id": "2992679862",
"content": "Need one bottle of milk",
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"update_comment": lambda credentials, comment_id, content: True},
)
@staticmethod
def update_comment(credentials: TodoistCredentials, comment_id: str, content: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
api.update_comment(comment_id=comment_id, content=content)
return True
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.update_comment(
credentials,
comment_id=input_data.comment_id,
content=input_data.content,
)
yield "success", success
except Exception as e:
yield "error", str(e)
class TodoistDeleteCommentBlock(Block):
"""Deletes a Todoist comment"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
comment_id: str = SchemaField(description="Comment ID to delete")
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="bda4c020-ddf2-11ef-b114-32d3674e8b7e",
description="Deletes a Todoist comment",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistDeleteCommentBlock.Input,
output_schema=TodoistDeleteCommentBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"comment_id": "2992679862",
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"delete_comment": lambda credentials, comment_id: True},
)
@staticmethod
def delete_comment(credentials: TodoistCredentials, comment_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
success = api.delete_comment(comment_id=comment_id)
return success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.delete_comment(credentials, comment_id=input_data.comment_id)
yield "success", success
except Exception as e:
yield "error", str(e)

View File

@@ -0,0 +1,557 @@
from todoist_api_python.api import TodoistAPI
from typing_extensions import Optional
from backend.blocks.todoist._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TodoistCredentials,
TodoistCredentialsField,
TodoistCredentialsInput,
)
from backend.blocks.todoist._types import Colors
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TodoistCreateLabelBlock(Block):
"""Creates a new label in Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
name: str = SchemaField(description="Name of the label")
order: Optional[int] = SchemaField(description="Label order", default=None)
color: Optional[Colors] = SchemaField(
description="The color of the label icon", default=Colors.charcoal
)
is_favorite: bool = SchemaField(
description="Whether the label is a favorite", default=False
)
class Output(BlockSchema):
id: str = SchemaField(description="ID of the created label")
name: str = SchemaField(description="Name of the label")
color: str = SchemaField(description="Color of the label")
order: int = SchemaField(description="Label order")
is_favorite: bool = SchemaField(description="Favorite status")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="7288a968-de14-11ef-8997-32d3674e8b7e",
description="Creates a new label in Todoist, It will not work if same name already exists",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistCreateLabelBlock.Input,
output_schema=TodoistCreateLabelBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"name": "Test Label",
"color": Colors.charcoal.value,
"order": 1,
"is_favorite": False,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "2156154810"),
("name", "Test Label"),
("color", "charcoal"),
("order", 1),
("is_favorite", False),
],
test_mock={
"create_label": lambda *args, **kwargs: {
"id": "2156154810",
"name": "Test Label",
"color": "charcoal",
"order": 1,
"is_favorite": False,
}
},
)
@staticmethod
def create_label(credentials: TodoistCredentials, name: str, **kwargs):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
label = api.add_label(name=name, **kwargs)
return label.__dict__
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
label_args = {
"order": input_data.order,
"color": (
input_data.color.value if input_data.color is not None else None
),
"is_favorite": input_data.is_favorite,
}
label_data = self.create_label(
credentials,
input_data.name,
**{k: v for k, v in label_args.items() if v is not None},
)
if label_data:
yield "id", label_data["id"]
yield "name", label_data["name"]
yield "color", label_data["color"]
yield "order", label_data["order"]
yield "is_favorite", label_data["is_favorite"]
except Exception as e:
yield "error", str(e)
class TodoistListLabelsBlock(Block):
"""Gets all personal labels from Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
class Output(BlockSchema):
labels: list = SchemaField(description="List of complete label data")
label_ids: list = SchemaField(description="List of label IDs")
label_names: list = SchemaField(description="List of label names")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="776dd750-de14-11ef-b927-32d3674e8b7e",
description="Gets all personal labels from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistListLabelsBlock.Input,
output_schema=TodoistListLabelsBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT},
test_credentials=TEST_CREDENTIALS,
test_output=[
(
"labels",
[
{
"id": "2156154810",
"name": "Test Label",
"color": "charcoal",
"order": 1,
"is_favorite": False,
}
],
),
("label_ids", ["2156154810"]),
("label_names", ["Test Label"]),
],
test_mock={
"get_labels": lambda *args, **kwargs: [
{
"id": "2156154810",
"name": "Test Label",
"color": "charcoal",
"order": 1,
"is_favorite": False,
}
]
},
)
@staticmethod
def get_labels(credentials: TodoistCredentials):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
labels = api.get_labels()
return [label.__dict__ for label in labels]
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
labels = self.get_labels(credentials)
yield "labels", labels
yield "label_ids", [label["id"] for label in labels]
yield "label_names", [label["name"] for label in labels]
except Exception as e:
yield "error", str(e)
class TodoistGetLabelBlock(Block):
"""Gets a personal label from Todoist by ID"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
label_id: str = SchemaField(description="ID of the label to retrieve")
class Output(BlockSchema):
id: str = SchemaField(description="ID of the label")
name: str = SchemaField(description="Name of the label")
color: str = SchemaField(description="Color of the label")
order: int = SchemaField(description="Label order")
is_favorite: bool = SchemaField(description="Favorite status")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="7f236514-de14-11ef-bd7a-32d3674e8b7e",
description="Gets a personal label from Todoist by ID",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistGetLabelBlock.Input,
output_schema=TodoistGetLabelBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"label_id": "2156154810",
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "2156154810"),
("name", "Test Label"),
("color", "charcoal"),
("order", 1),
("is_favorite", False),
],
test_mock={
"get_label": lambda *args, **kwargs: {
"id": "2156154810",
"name": "Test Label",
"color": "charcoal",
"order": 1,
"is_favorite": False,
}
},
)
@staticmethod
def get_label(credentials: TodoistCredentials, label_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
label = api.get_label(label_id=label_id)
return label.__dict__
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
label_data = self.get_label(credentials, input_data.label_id)
if label_data:
yield "id", label_data["id"]
yield "name", label_data["name"]
yield "color", label_data["color"]
yield "order", label_data["order"]
yield "is_favorite", label_data["is_favorite"]
except Exception as e:
yield "error", str(e)
class TodoistUpdateLabelBlock(Block):
"""Updates a personal label in Todoist using ID"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
label_id: str = SchemaField(description="ID of the label to update")
name: Optional[str] = SchemaField(
description="New name of the label", default=None
)
order: Optional[int] = SchemaField(description="Label order", default=None)
color: Optional[Colors] = SchemaField(
description="The color of the label icon", default=None
)
is_favorite: bool = SchemaField(
description="Whether the label is a favorite (true/false)", default=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="8755614c-de14-11ef-9b56-32d3674e8b7e",
description="Updates a personal label in Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistUpdateLabelBlock.Input,
output_schema=TodoistUpdateLabelBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"label_id": "2156154810",
"name": "Updated Label",
"color": Colors.charcoal.value,
"order": 2,
"is_favorite": True,
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"update_label": lambda *args, **kwargs: True},
)
@staticmethod
def update_label(credentials: TodoistCredentials, label_id: str, **kwargs):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
api.update_label(label_id=label_id, **kwargs)
return True
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
label_args = {}
if input_data.name is not None:
label_args["name"] = input_data.name
if input_data.order is not None:
label_args["order"] = input_data.order
if input_data.color is not None:
label_args["color"] = input_data.color.value
if input_data.is_favorite is not None:
label_args["is_favorite"] = input_data.is_favorite
success = self.update_label(
credentials,
input_data.label_id,
**{k: v for k, v in label_args.items() if v is not None},
)
yield "success", success
except Exception as e:
yield "error", str(e)
class TodoistDeleteLabelBlock(Block):
"""Deletes a personal label in Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
label_id: str = SchemaField(description="ID of the label to delete")
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="901b8f86-de14-11ef-98b8-32d3674e8b7e",
description="Deletes a personal label in Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistDeleteLabelBlock.Input,
output_schema=TodoistDeleteLabelBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"label_id": "2156154810",
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"delete_label": lambda *args, **kwargs: True},
)
@staticmethod
def delete_label(credentials: TodoistCredentials, label_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
success = api.delete_label(label_id=label_id)
return success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.delete_label(credentials, input_data.label_id)
yield "success", success
except Exception as e:
yield "error", str(e)
class TodoistGetSharedLabelsBlock(Block):
"""Gets all shared labels from Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
class Output(BlockSchema):
labels: list = SchemaField(description="List of shared label names")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="55fba510-de15-11ef-aed2-32d3674e8b7e",
description="Gets all shared labels from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistGetSharedLabelsBlock.Input,
output_schema=TodoistGetSharedLabelsBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT},
test_credentials=TEST_CREDENTIALS,
test_output=[("labels", ["Label1", "Label2", "Label3"])],
test_mock={
"get_shared_labels": lambda *args, **kwargs: [
"Label1",
"Label2",
"Label3",
]
},
)
@staticmethod
def get_shared_labels(credentials: TodoistCredentials):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
labels = api.get_shared_labels()
return labels
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
labels = self.get_shared_labels(credentials)
yield "labels", labels
except Exception as e:
yield "error", str(e)
class TodoistRenameSharedLabelsBlock(Block):
"""Renames all instances of a shared label"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
name: str = SchemaField(description="The name of the existing label to rename")
new_name: str = SchemaField(description="The new name for the label")
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the rename was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="9d63ad9a-de14-11ef-ab3f-32d3674e8b7e",
description="Renames all instances of a shared label",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistRenameSharedLabelsBlock.Input,
output_schema=TodoistRenameSharedLabelsBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"name": "OldLabel",
"new_name": "NewLabel",
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"rename_shared_labels": lambda *args, **kwargs: True},
)
@staticmethod
def rename_shared_labels(credentials: TodoistCredentials, name: str, new_name: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
success = api.rename_shared_label(name=name, new_name=new_name)
return success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.rename_shared_labels(
credentials, input_data.name, input_data.new_name
)
yield "success", success
except Exception as e:
yield "error", str(e)
class TodoistRemoveSharedLabelsBlock(Block):
"""Removes all instances of a shared label"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
name: str = SchemaField(description="The name of the label to remove")
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the removal was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="a6c5cbde-de14-11ef-8863-32d3674e8b7e",
description="Removes all instances of a shared label",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistRemoveSharedLabelsBlock.Input,
output_schema=TodoistRemoveSharedLabelsBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT, "name": "LabelToRemove"},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"remove_shared_label": lambda *args, **kwargs: True},
)
@staticmethod
def remove_shared_label(credentials: TodoistCredentials, name: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
success = api.remove_shared_label(name=name)
return success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.remove_shared_label(credentials, input_data.name)
yield "success", success
except Exception as e:
yield "error", str(e)

View File

@@ -0,0 +1,566 @@
from todoist_api_python.api import TodoistAPI
from typing_extensions import Optional
from backend.blocks.todoist._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TodoistCredentials,
TodoistCredentialsField,
TodoistCredentialsInput,
)
from backend.blocks.todoist._types import Colors
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TodoistListProjectsBlock(Block):
"""Gets all projects for a Todoist user"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
class Output(BlockSchema):
names_list: list[str] = SchemaField(description="List of project names")
ids_list: list[str] = SchemaField(description="List of project IDs")
url_list: list[str] = SchemaField(description="List of project URLs")
complete_data: list[dict] = SchemaField(
description="Complete project data including all fields"
)
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="5f3e1d5b-6bc5-40e3-97ee-1318b3f38813",
description="Gets all projects and their details from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistListProjectsBlock.Input,
output_schema=TodoistListProjectsBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("names_list", ["Inbox"]),
("ids_list", ["220474322"]),
("url_list", ["https://todoist.com/showProject?id=220474322"]),
(
"complete_data",
[
{
"id": "220474322",
"name": "Inbox",
"url": "https://todoist.com/showProject?id=220474322",
}
],
),
],
test_mock={
"get_project_lists": lambda *args, **kwargs: (
["Inbox"],
["220474322"],
["https://todoist.com/showProject?id=220474322"],
[
{
"id": "220474322",
"name": "Inbox",
"url": "https://todoist.com/showProject?id=220474322",
}
],
None,
)
},
)
@staticmethod
def get_project_lists(credentials: TodoistCredentials):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
projects = api.get_projects()
names = []
ids = []
urls = []
complete_data = []
for project in projects:
names.append(project.name)
ids.append(project.id)
urls.append(project.url)
complete_data.append(project.__dict__)
return names, ids, urls, complete_data, None
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
names, ids, urls, data, error = self.get_project_lists(credentials)
if names:
yield "names_list", names
if ids:
yield "ids_list", ids
if urls:
yield "url_list", urls
if data:
yield "complete_data", data
except Exception as e:
yield "error", str(e)
class TodoistCreateProjectBlock(Block):
"""Creates a new project in Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
name: str = SchemaField(description="Name of the project", advanced=False)
parent_id: Optional[str] = SchemaField(
description="Parent project ID", default=None, advanced=True
)
color: Optional[Colors] = SchemaField(
description="Color of the project icon",
default=Colors.charcoal,
advanced=True,
)
is_favorite: bool = SchemaField(
description="Whether the project is a favorite",
default=False,
advanced=True,
)
view_style: Optional[str] = SchemaField(
description="Display style (list or board)", default=None, advanced=True
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the creation was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="ade60136-de14-11ef-b5e5-32d3674e8b7e",
description="Creates a new project in Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistCreateProjectBlock.Input,
output_schema=TodoistCreateProjectBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT, "name": "Test Project"},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"create_project": lambda *args, **kwargs: (True)},
)
@staticmethod
def create_project(
credentials: TodoistCredentials,
name: str,
parent_id: Optional[str],
color: Optional[Colors],
is_favorite: bool,
view_style: Optional[str],
):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
params = {"name": name, "is_favorite": is_favorite}
if parent_id is not None:
params["parent_id"] = parent_id
if color is not None:
params["color"] = color.value
if view_style is not None:
params["view_style"] = view_style
api.add_project(**params)
return True
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.create_project(
credentials=credentials,
name=input_data.name,
parent_id=input_data.parent_id,
color=input_data.color,
is_favorite=input_data.is_favorite,
view_style=input_data.view_style,
)
yield "success", success
except Exception as e:
yield "error", str(e)
class TodoistGetProjectBlock(Block):
"""Gets details for a specific Todoist project"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
project_id: str = SchemaField(
description="ID of the project to get details for", advanced=False
)
class Output(BlockSchema):
project_id: str = SchemaField(description="ID of project")
project_name: str = SchemaField(description="Name of project")
project_url: str = SchemaField(description="URL of project")
complete_data: dict = SchemaField(
description="Complete project data including all fields"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="b435b5ea-de14-11ef-8b51-32d3674e8b7e",
description="Gets details for a specific Todoist project",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistGetProjectBlock.Input,
output_schema=TodoistGetProjectBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"project_id": "2203306141",
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("project_id", "2203306141"),
("project_name", "Shopping List"),
("project_url", "https://todoist.com/showProject?id=2203306141"),
(
"complete_data",
{
"id": "2203306141",
"name": "Shopping List",
"url": "https://todoist.com/showProject?id=2203306141",
},
),
],
test_mock={
"get_project": lambda *args, **kwargs: (
"2203306141",
"Shopping List",
"https://todoist.com/showProject?id=2203306141",
{
"id": "2203306141",
"name": "Shopping List",
"url": "https://todoist.com/showProject?id=2203306141",
},
)
},
)
@staticmethod
def get_project(credentials: TodoistCredentials, project_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
project = api.get_project(project_id=project_id)
return project.id, project.name, project.url, project.__dict__
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
project_id, project_name, project_url, data = self.get_project(
credentials=credentials, project_id=input_data.project_id
)
if project_id:
yield "project_id", project_id
if project_name:
yield "project_name", project_name
if project_url:
yield "project_url", project_url
if data:
yield "complete_data", data
except Exception as e:
yield "error", str(e)
class TodoistUpdateProjectBlock(Block):
"""Updates an existing project in Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
project_id: str = SchemaField(
description="ID of project to update", advanced=False
)
name: Optional[str] = SchemaField(
description="New name for the project", default=None, advanced=False
)
color: Optional[Colors] = SchemaField(
description="New color for the project icon", default=None, advanced=True
)
is_favorite: Optional[bool] = SchemaField(
description="Whether the project should be a favorite",
default=None,
advanced=True,
)
view_style: Optional[str] = SchemaField(
description="Display style (list or board)", default=None, advanced=True
)
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="ba41a20a-de14-11ef-91d7-32d3674e8b7e",
description="Updates an existing project in Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistUpdateProjectBlock.Input,
output_schema=TodoistUpdateProjectBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"project_id": "2203306141",
"name": "Things To Buy",
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"update_project": lambda *args, **kwargs: (True)},
)
@staticmethod
def update_project(
credentials: TodoistCredentials,
project_id: str,
name: Optional[str],
color: Optional[Colors],
is_favorite: Optional[bool],
view_style: Optional[str],
):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
params = {}
if name is not None:
params["name"] = name
if color is not None:
params["color"] = color.value
if is_favorite is not None:
params["is_favorite"] = is_favorite
if view_style is not None:
params["view_style"] = view_style
api.update_project(project_id=project_id, **params)
return True
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.update_project(
credentials=credentials,
project_id=input_data.project_id,
name=input_data.name,
color=input_data.color,
is_favorite=input_data.is_favorite,
view_style=input_data.view_style,
)
yield "success", success
except Exception as e:
yield "error", str(e)
class TodoistDeleteProjectBlock(Block):
"""Deletes a project and all of its sections and tasks"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
project_id: str = SchemaField(
description="ID of project to delete", advanced=False
)
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="c2893acc-de14-11ef-a113-32d3674e8b7e",
description="Deletes a Todoist project and all its contents",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistDeleteProjectBlock.Input,
output_schema=TodoistDeleteProjectBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"project_id": "2203306141",
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"delete_project": lambda *args, **kwargs: (True)},
)
@staticmethod
def delete_project(credentials: TodoistCredentials, project_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
success = api.delete_project(project_id=project_id)
return success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.delete_project(
credentials=credentials, project_id=input_data.project_id
)
yield "success", success
except Exception as e:
yield "error", str(e)
class TodoistListCollaboratorsBlock(Block):
"""Gets all collaborators for a Todoist project"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
project_id: str = SchemaField(
description="ID of the project to get collaborators for", advanced=False
)
class Output(BlockSchema):
collaborator_ids: list[str] = SchemaField(
description="List of collaborator IDs"
)
collaborator_names: list[str] = SchemaField(
description="List of collaborator names"
)
collaborator_emails: list[str] = SchemaField(
description="List of collaborator email addresses"
)
complete_data: list[dict] = SchemaField(
description="Complete collaborator data including all fields"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="c99c804e-de14-11ef-9f47-32d3674e8b7e",
description="Gets all collaborators for a specific Todoist project",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistListCollaboratorsBlock.Input,
output_schema=TodoistListCollaboratorsBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"project_id": "2203306141",
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("collaborator_ids", ["2671362", "2671366"]),
("collaborator_names", ["Alice", "Bob"]),
("collaborator_emails", ["alice@example.com", "bob@example.com"]),
(
"complete_data",
[
{
"id": "2671362",
"name": "Alice",
"email": "alice@example.com",
},
{"id": "2671366", "name": "Bob", "email": "bob@example.com"},
],
),
],
test_mock={
"get_collaborators": lambda *args, **kwargs: (
["2671362", "2671366"],
["Alice", "Bob"],
["alice@example.com", "bob@example.com"],
[
{
"id": "2671362",
"name": "Alice",
"email": "alice@example.com",
},
{"id": "2671366", "name": "Bob", "email": "bob@example.com"},
],
)
},
)
@staticmethod
def get_collaborators(credentials: TodoistCredentials, project_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
collaborators = api.get_collaborators(project_id=project_id)
ids = []
names = []
emails = []
complete_data = []
for collaborator in collaborators:
ids.append(collaborator.id)
names.append(collaborator.name)
emails.append(collaborator.email)
complete_data.append(collaborator.__dict__)
return ids, names, emails, complete_data
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, names, emails, data = self.get_collaborators(
credentials=credentials, project_id=input_data.project_id
)
if ids:
yield "collaborator_ids", ids
if names:
yield "collaborator_names", names
if emails:
yield "collaborator_emails", emails
if data:
yield "complete_data", data
except Exception as e:
yield "error", str(e)

View File

@@ -0,0 +1,306 @@
from todoist_api_python.api import TodoistAPI
from typing_extensions import Optional
from backend.blocks.todoist._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TodoistCredentials,
TodoistCredentialsField,
TodoistCredentialsInput,
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TodoistListSectionsBlock(Block):
"""Gets all sections for a Todoist project"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
project_id: Optional[str] = SchemaField(
description="Optional project ID to filter sections"
)
class Output(BlockSchema):
names_list: list[str] = SchemaField(description="List of section names")
ids_list: list[str] = SchemaField(description="List of section IDs")
complete_data: list[dict] = SchemaField(
description="Complete section data including all fields"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="d6a116d8-de14-11ef-a94c-32d3674e8b7e",
description="Gets all sections and their details from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistListSectionsBlock.Input,
output_schema=TodoistListSectionsBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"project_id": "2203306141",
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("names_list", ["Groceries"]),
("ids_list", ["7025"]),
(
"complete_data",
[
{
"id": "7025",
"project_id": "2203306141",
"order": 1,
"name": "Groceries",
}
],
),
],
test_mock={
"get_section_lists": lambda *args, **kwargs: (
["Groceries"],
["7025"],
[
{
"id": "7025",
"project_id": "2203306141",
"order": 1,
"name": "Groceries",
}
],
)
},
)
@staticmethod
def get_section_lists(
credentials: TodoistCredentials, project_id: Optional[str] = None
):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
sections = api.get_sections(project_id=project_id)
names = []
ids = []
complete_data = []
for section in sections:
names.append(section.name)
ids.append(section.id)
complete_data.append(section.__dict__)
return names, ids, complete_data
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
names, ids, data = self.get_section_lists(
credentials, input_data.project_id
)
if names:
yield "names_list", names
if ids:
yield "ids_list", ids
if data:
yield "complete_data", data
except Exception as e:
yield "error", str(e)
# Error in official todoist SDK. Will add this block using sync_api
# class TodoistCreateSectionBlock(Block):
# """Creates a new section in a Todoist project"""
# class Input(BlockSchema):
# credentials: TodoistCredentialsInput = TodoistCredentialsField([])
# name: str = SchemaField(description="Section name")
# project_id: str = SchemaField(description="Project ID this section should belong to")
# order: Optional[int] = SchemaField(description="Optional order among other sections", default=None)
# class Output(BlockSchema):
# success: bool = SchemaField(description="Whether section was successfully created")
# error: str = SchemaField(description="Error message if the request failed")
# def __init__(self):
# super().__init__(
# id="e3025cfc-de14-11ef-b9f2-32d3674e8b7e",
# description="Creates a new section in a Todoist project",
# categories={BlockCategory.PRODUCTIVITY},
# input_schema=TodoistCreateSectionBlock.Input,
# output_schema=TodoistCreateSectionBlock.Output,
# test_input={
# "credentials": TEST_CREDENTIALS_INPUT,
# "name": "Groceries",
# "project_id": "2203306141"
# },
# test_credentials=TEST_CREDENTIALS,
# test_output=[
# ("success", True)
# ],
# test_mock={
# "create_section": lambda *args, **kwargs: (
# {"id": "7025", "project_id": "2203306141", "order": 1, "name": "Groceries"},
# )
# },
# )
# @staticmethod
# def create_section(credentials: TodoistCredentials, name: str, project_id: str, order: Optional[int] = None):
# try:
# api = TodoistAPI(credentials.access_token.get_secret_value())
# section = api.add_section(name=name, project_id=project_id, order=order)
# return section.__dict__
# except Exception as e:
# raise e
# def run(
# self,
# input_data: Input,
# *,
# credentials: TodoistCredentials,
# **kwargs,
# ) -> BlockOutput:
# try:
# section_data = self.create_section(
# credentials,
# input_data.name,
# input_data.project_id,
# input_data.order
# )
# if section_data:
# yield "success", True
# except Exception as e:
# yield "error", str(e)
class TodoistGetSectionBlock(Block):
"""Gets a single section from Todoist by ID"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
section_id: str = SchemaField(description="ID of section to fetch")
class Output(BlockSchema):
id: str = SchemaField(description="ID of section")
project_id: str = SchemaField(description="Project ID the section belongs to")
order: int = SchemaField(description="Order of the section")
name: str = SchemaField(description="Name of the section")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="ea5580e2-de14-11ef-a5d3-32d3674e8b7e",
description="Gets a single section by ID from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistGetSectionBlock.Input,
output_schema=TodoistGetSectionBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT, "section_id": "7025"},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "7025"),
("project_id", "2203306141"),
("order", 1),
("name", "Groceries"),
],
test_mock={
"get_section": lambda *args, **kwargs: {
"id": "7025",
"project_id": "2203306141",
"order": 1,
"name": "Groceries",
}
},
)
@staticmethod
def get_section(credentials: TodoistCredentials, section_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
section = api.get_section(section_id=section_id)
return section.__dict__
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
section_data = self.get_section(credentials, input_data.section_id)
if section_data:
yield "id", section_data["id"]
yield "project_id", section_data["project_id"]
yield "order", section_data["order"]
yield "name", section_data["name"]
except Exception as e:
yield "error", str(e)
class TodoistDeleteSectionBlock(Block):
"""Deletes a section and all its tasks from Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
section_id: str = SchemaField(description="ID of section to delete")
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether section was successfully deleted"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="f0e52eee-de14-11ef-9b12-32d3674e8b7e",
description="Deletes a section and all its tasks from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistDeleteSectionBlock.Input,
output_schema=TodoistDeleteSectionBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT, "section_id": "7025"},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"delete_section": lambda *args, **kwargs: (True)},
)
@staticmethod
def delete_section(credentials: TodoistCredentials, section_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
success = api.delete_section(section_id=section_id)
return success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.delete_section(credentials, input_data.section_id)
yield "success", success
except Exception as e:
yield "error", str(e)

View File

@@ -0,0 +1,660 @@
from datetime import datetime
from todoist_api_python.api import TodoistAPI
from todoist_api_python.models import Task
from typing_extensions import Optional
from backend.blocks.todoist._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TodoistCredentials,
TodoistCredentialsField,
TodoistCredentialsInput,
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TodoistCreateTaskBlock(Block):
"""Creates a new task in a Todoist project"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
content: str = SchemaField(description="Task content", advanced=False)
description: Optional[str] = SchemaField(
description="Task description", default=None, advanced=False
)
project_id: Optional[str] = SchemaField(
description="Project ID this task should belong to",
default=None,
advanced=False,
)
section_id: Optional[str] = SchemaField(
description="Section ID this task should belong to",
default=None,
advanced=False,
)
parent_id: Optional[str] = SchemaField(
description="Parent task ID", default=None, advanced=True
)
order: Optional[int] = SchemaField(
description="Optional order among other tasks,[Non-zero integer value used by clients to sort tasks under the same parent]",
default=None,
advanced=True,
)
labels: Optional[list[str]] = SchemaField(
description="Task labels", default=None, advanced=True
)
priority: Optional[int] = SchemaField(
description="Task priority from 1 (normal) to 4 (urgent)",
default=None,
advanced=True,
)
due_date: Optional[datetime] = SchemaField(
description="Due date in YYYY-MM-DD format", advanced=True, default=None
)
deadline_date: Optional[datetime] = SchemaField(
description="Specific date in YYYY-MM-DD format relative to user's timezone",
default=None,
advanced=True,
)
assignee_id: Optional[str] = SchemaField(
description="Responsible user ID", default=None, advanced=True
)
duration_unit: Optional[str] = SchemaField(
description="Task duration unit (minute/day)", default=None, advanced=True
)
duration: Optional[int] = SchemaField(
description="Task duration amount, You need to selecct the duration unit first",
depends_on=["duration_unit"],
default=None,
advanced=True,
)
class Output(BlockSchema):
id: str = SchemaField(description="Task ID")
url: str = SchemaField(description="Task URL")
complete_data: dict = SchemaField(
description="Complete task data as dictionary"
)
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="fde4f458-de14-11ef-bf0c-32d3674e8b7e",
description="Creates a new task in a Todoist project",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistCreateTaskBlock.Input,
output_schema=TodoistCreateTaskBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"content": "Buy groceries",
"project_id": "2203306141",
"priority": 4,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "2995104339"),
("url", "https://todoist.com/showTask?id=2995104339"),
(
"complete_data",
{
"id": "2995104339",
"project_id": "2203306141",
"url": "https://todoist.com/showTask?id=2995104339",
},
),
],
test_mock={
"create_task": lambda *args, **kwargs: (
"2995104339",
"https://todoist.com/showTask?id=2995104339",
{
"id": "2995104339",
"project_id": "2203306141",
"url": "https://todoist.com/showTask?id=2995104339",
},
)
},
)
@staticmethod
def create_task(credentials: TodoistCredentials, content: str, **kwargs):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
task = api.add_task(content=content, **kwargs)
task_dict = Task.to_dict(task)
return task.id, task.url, task_dict
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
due_date = (
input_data.due_date.strftime("%Y-%m-%d")
if input_data.due_date
else None
)
deadline_date = (
input_data.deadline_date.strftime("%Y-%m-%d")
if input_data.deadline_date
else None
)
task_args = {
"description": input_data.description,
"project_id": input_data.project_id,
"section_id": input_data.section_id,
"parent_id": input_data.parent_id,
"order": input_data.order,
"labels": input_data.labels,
"priority": input_data.priority,
"due_date": due_date,
"deadline_date": deadline_date,
"assignee_id": input_data.assignee_id,
"duration": input_data.duration,
"duration_unit": input_data.duration_unit,
}
id, url, complete_data = self.create_task(
credentials,
input_data.content,
**{k: v for k, v in task_args.items() if v is not None},
)
yield "id", id
yield "url", url
yield "complete_data", complete_data
except Exception as e:
yield "error", str(e)
class TodoistGetTasksBlock(Block):
"""Get active tasks from Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
project_id: Optional[str] = SchemaField(
description="Filter tasks by project ID", default=None, advanced=False
)
section_id: Optional[str] = SchemaField(
description="Filter tasks by section ID", default=None, advanced=True
)
label: Optional[str] = SchemaField(
description="Filter tasks by label name", default=None, advanced=True
)
filter: Optional[str] = SchemaField(
description="Filter by any supported filter, You can see How to use filters or create one of your one here - https://todoist.com/help/articles/introduction-to-filters-V98wIH",
default=None,
advanced=True,
)
lang: Optional[str] = SchemaField(
description="IETF language tag for filter language", default=None
)
ids: Optional[list[str]] = SchemaField(
description="List of task IDs to retrieve", default=None, advanced=False
)
class Output(BlockSchema):
ids: list[str] = SchemaField(description="Task IDs")
urls: list[str] = SchemaField(description="Task URLs")
complete_data: list[dict] = SchemaField(
description="Complete task data as dictionary"
)
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="0b706e86-de15-11ef-a113-32d3674e8b7e",
description="Get active tasks from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistGetTasksBlock.Input,
output_schema=TodoistGetTasksBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"project_id": "2203306141",
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["2995104339"]),
("urls", ["https://todoist.com/showTask?id=2995104339"]),
(
"complete_data",
[
{
"id": "2995104339",
"project_id": "2203306141",
"url": "https://todoist.com/showTask?id=2995104339",
"is_completed": False,
}
],
),
],
test_mock={
"get_tasks": lambda *args, **kwargs: [
{
"id": "2995104339",
"project_id": "2203306141",
"url": "https://todoist.com/showTask?id=2995104339",
"is_completed": False,
}
]
},
)
@staticmethod
def get_tasks(credentials: TodoistCredentials, **kwargs):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
tasks = api.get_tasks(**kwargs)
return [Task.to_dict(task) for task in tasks]
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
task_filters = {
"project_id": input_data.project_id,
"section_id": input_data.section_id,
"label": input_data.label,
"filter": input_data.filter,
"lang": input_data.lang,
"ids": input_data.ids,
}
tasks = self.get_tasks(
credentials, **{k: v for k, v in task_filters.items() if v is not None}
)
yield "ids", [task["id"] for task in tasks]
yield "urls", [task["url"] for task in tasks]
yield "complete_data", tasks
except Exception as e:
yield "error", str(e)
class TodoistGetTaskBlock(Block):
"""Get an active task from Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
task_id: str = SchemaField(description="Task ID to retrieve")
class Output(BlockSchema):
project_id: str = SchemaField(description="Project ID containing the task")
url: str = SchemaField(description="Task URL")
complete_data: dict = SchemaField(
description="Complete task data as dictionary"
)
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="16d7dc8c-de15-11ef-8ace-32d3674e8b7e",
description="Get an active task from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistGetTaskBlock.Input,
output_schema=TodoistGetTaskBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT, "task_id": "2995104339"},
test_credentials=TEST_CREDENTIALS,
test_output=[
("project_id", "2203306141"),
("url", "https://todoist.com/showTask?id=2995104339"),
(
"complete_data",
{
"id": "2995104339",
"project_id": "2203306141",
"url": "https://todoist.com/showTask?id=2995104339",
},
),
],
test_mock={
"get_task": lambda *args, **kwargs: {
"project_id": "2203306141",
"id": "2995104339",
"url": "https://todoist.com/showTask?id=2995104339",
}
},
)
@staticmethod
def get_task(credentials: TodoistCredentials, task_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
task = api.get_task(task_id=task_id)
return Task.to_dict(task)
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
task_data = self.get_task(credentials, input_data.task_id)
if task_data:
yield "project_id", task_data["project_id"]
yield "url", task_data["url"]
yield "complete_data", task_data
except Exception as e:
yield "error", str(e)
class TodoistUpdateTaskBlock(Block):
"""Updates an existing task in Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
task_id: str = SchemaField(description="Task ID to update")
content: str = SchemaField(description="Task content", advanced=False)
description: Optional[str] = SchemaField(
description="Task description", default=None, advanced=False
)
project_id: Optional[str] = SchemaField(
description="Project ID this task should belong to",
default=None,
advanced=False,
)
section_id: Optional[str] = SchemaField(
description="Section ID this task should belong to",
default=None,
advanced=False,
)
parent_id: Optional[str] = SchemaField(
description="Parent task ID", default=None, advanced=True
)
order: Optional[int] = SchemaField(
description="Optional order among other tasks,[Non-zero integer value used by clients to sort tasks under the same parent]",
default=None,
advanced=True,
)
labels: Optional[list[str]] = SchemaField(
description="Task labels", default=None, advanced=True
)
priority: Optional[int] = SchemaField(
description="Task priority from 1 (normal) to 4 (urgent)",
default=None,
advanced=True,
)
due_date: Optional[datetime] = SchemaField(
description="Due date in YYYY-MM-DD format", advanced=True, default=None
)
deadline_date: Optional[datetime] = SchemaField(
description="Specific date in YYYY-MM-DD format relative to user's timezone",
default=None,
advanced=True,
)
assignee_id: Optional[str] = SchemaField(
description="Responsible user ID", default=None, advanced=True
)
duration_unit: Optional[str] = SchemaField(
description="Task duration unit (minute/day)", default=None, advanced=True
)
duration: Optional[int] = SchemaField(
description="Task duration amount, You need to selecct the duration unit first",
depends_on=["duration_unit"],
default=None,
advanced=True,
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the update was successful")
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="1eee6d32-de15-11ef-a2ff-32d3674e8b7e",
description="Updates an existing task in Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistUpdateTaskBlock.Input,
output_schema=TodoistUpdateTaskBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"task_id": "2995104339",
"content": "Buy Coffee",
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"update_task": lambda *args, **kwargs: True},
)
@staticmethod
def update_task(credentials: TodoistCredentials, task_id: str, **kwargs):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
is_success = api.update_task(task_id=task_id, **kwargs)
return is_success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
due_date = (
input_data.due_date.strftime("%Y-%m-%d")
if input_data.due_date
else None
)
deadline_date = (
input_data.deadline_date.strftime("%Y-%m-%d")
if input_data.deadline_date
else None
)
task_updates = {}
if input_data.content is not None:
task_updates["content"] = input_data.content
if input_data.description is not None:
task_updates["description"] = input_data.description
if input_data.project_id is not None:
task_updates["project_id"] = input_data.project_id
if input_data.section_id is not None:
task_updates["section_id"] = input_data.section_id
if input_data.parent_id is not None:
task_updates["parent_id"] = input_data.parent_id
if input_data.order is not None:
task_updates["order"] = input_data.order
if input_data.labels is not None:
task_updates["labels"] = input_data.labels
if input_data.priority is not None:
task_updates["priority"] = input_data.priority
if due_date is not None:
task_updates["due_date"] = due_date
if deadline_date is not None:
task_updates["deadline_date"] = deadline_date
if input_data.assignee_id is not None:
task_updates["assignee_id"] = input_data.assignee_id
if input_data.duration is not None:
task_updates["duration"] = input_data.duration
if input_data.duration_unit is not None:
task_updates["duration_unit"] = input_data.duration_unit
self.update_task(
credentials,
input_data.task_id,
**{k: v for k, v in task_updates.items() if v is not None},
)
yield "success", True
except Exception as e:
yield "error", str(e)
class TodoistCloseTaskBlock(Block):
"""Closes a task in Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
task_id: str = SchemaField(description="Task ID to close")
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the task was successfully closed"
)
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="29fac798-de15-11ef-b839-32d3674e8b7e",
description="Closes a task in Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistCloseTaskBlock.Input,
output_schema=TodoistCloseTaskBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT, "task_id": "2995104339"},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"close_task": lambda *args, **kwargs: True},
)
@staticmethod
def close_task(credentials: TodoistCredentials, task_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
is_success = api.close_task(task_id=task_id)
return is_success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
is_success = self.close_task(credentials, input_data.task_id)
yield "success", is_success
except Exception as e:
yield "error", str(e)
class TodoistReopenTaskBlock(Block):
"""Reopens a task in Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
task_id: str = SchemaField(description="Task ID to reopen")
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the task was successfully reopened"
)
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="2e6bf6f8-de15-11ef-ae7c-32d3674e8b7e",
description="Reopens a task in Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistReopenTaskBlock.Input,
output_schema=TodoistReopenTaskBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT, "task_id": "2995104339"},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"reopen_task": lambda *args, **kwargs: (True)},
)
@staticmethod
def reopen_task(credentials: TodoistCredentials, task_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
is_success = api.reopen_task(task_id=task_id)
return is_success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
is_success = self.reopen_task(credentials, input_data.task_id)
yield "success", is_success
except Exception as e:
yield "error", str(e)
class TodoistDeleteTaskBlock(Block):
"""Deletes a task in Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
task_id: str = SchemaField(description="Task ID to delete")
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the task was successfully deleted"
)
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="33c29ada-de15-11ef-bcbb-32d3674e8b7e",
description="Deletes a task in Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistDeleteTaskBlock.Input,
output_schema=TodoistDeleteTaskBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT, "task_id": "2995104339"},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"delete_task": lambda *args, **kwargs: (True)},
)
@staticmethod
def delete_task(credentials: TodoistCredentials, task_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
is_success = api.delete_task(task_id=task_id)
return is_success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
is_success = self.delete_task(credentials, input_data.task_id)
yield "success", is_success
except Exception as e:
yield "error", str(e)

View File

@@ -92,7 +92,8 @@ class TwitterPostTweetBlock(Block):
attachment: Union[Media, DeepLink, Poll, Place, Quote] | None = SchemaField(
discriminator="discriminator",
description="Additional tweet data (media, deep link, poll, place or quote)",
advanced=True,
advanced=False,
default=Media(discriminator="media"),
)
exclude_reply_user_ids: Optional[List[str]] = SchemaField(

View File

@@ -132,12 +132,14 @@ class UserCreditBase(ABC):
},
)
transaction_balance = (
transactions[0].get("_sum", {}).get("amount", 0) + snapshot_balance
int(transactions[0].get("_sum", {}).get("amount", 0) + snapshot_balance)
if transactions
else snapshot_balance
)
transaction_time = (
transactions[0].get("_max", {}).get("createdAt", datetime_min)
datetime.fromisoformat(
str(transactions[0].get("_max", {}).get("createdAt", datetime_min))
)
if transactions
else snapshot_time
)
@@ -360,12 +362,21 @@ class UserCredit(UserCreditBase):
)
async def top_up_intent(self, user_id: str, amount: int) -> str:
if amount < 500 or amount % 100 != 0:
raise ValueError(
f"Top up amount must be at least 500 credits and multiple of 100 but is {amount}"
)
if not (user := await get_user_by_id(user_id)):
raise ValueError(f"User not found: {user_id}")
# Create checkout session
# https://docs.stripe.com/checkout/quickstart?client=react
# unit_amount param is always in the smallest currency unit (so cents for usd)
# which is equal to amount of credits
checkout_session = stripe.checkout.Session.create(
customer=await get_stripe_customer_id(user_id),
customer_email=user.email,
line_items=[
{
"price_data": {
@@ -385,6 +396,7 @@ class UserCredit(UserCreditBase):
+ "/marketplace/credits?topup=success",
cancel_url=settings.config.platform_base_url
+ "/marketplace/credits?topup=cancel",
return_url=settings.config.platform_base_url + "/marketplace/credits",
)
await self._add_transaction(
@@ -517,10 +529,10 @@ async def get_stripe_customer_id(user_id: str) -> str:
return customer.id
async def set_auto_top_up(user_id: str, threshold: int, amount: int):
async def set_auto_top_up(user_id: str, config: AutoTopUpConfig):
await User.prisma().update(
where={"id": user_id},
data={"topUpConfig": Json({"threshold": threshold, "amount": amount})},
data={"topUpConfig": Json(config.model_dump())},
)

View File

@@ -1,5 +1,7 @@
from typing import TYPE_CHECKING
from backend.integrations.oauth.todoist import TodoistOAuthHandler
from .github import GitHubOAuthHandler
from .google import GoogleOAuthHandler
from .linear import LinearOAuthHandler
@@ -19,6 +21,7 @@ HANDLERS_BY_NAME: dict["ProviderName", type["BaseOAuthHandler"]] = {
NotionOAuthHandler,
TwitterOAuthHandler,
LinearOAuthHandler,
TodoistOAuthHandler,
]
}
# --8<-- [end:HANDLERS_BY_NAMEExample]

View File

@@ -0,0 +1,81 @@
import urllib.parse
from typing import ClassVar, Optional
import requests
from backend.data.model import OAuth2Credentials, ProviderName
from backend.integrations.oauth.base import BaseOAuthHandler
class TodoistOAuthHandler(BaseOAuthHandler):
PROVIDER_NAME = ProviderName.TODOIST
DEFAULT_SCOPES: ClassVar[list[str]] = [
"task:add",
"data:read",
"data:read_write",
"data:delete",
"project:delete",
]
AUTHORIZE_URL = "https://todoist.com/oauth/authorize"
TOKEN_URL = "https://todoist.com/oauth/access_token"
def __init__(self, client_id: str, client_secret: str, redirect_uri: str):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
def get_login_url(
self, scopes: list[str], state: str, code_challenge: Optional[str]
) -> str:
params = {
"client_id": self.client_id,
"scope": ",".join(self.DEFAULT_SCOPES),
"state": state,
}
return f"{self.AUTHORIZE_URL}?{urllib.parse.urlencode(params)}"
def exchange_code_for_tokens(
self, code: str, scopes: list[str], code_verifier: Optional[str]
) -> OAuth2Credentials:
"""Exchange authorization code for access tokens"""
data = {
"client_id": self.client_id,
"client_secret": self.client_secret,
"code": code,
"redirect_uri": self.redirect_uri,
}
response = requests.post(self.TOKEN_URL, data=data)
response.raise_for_status()
tokens = response.json()
response = requests.post(
"https://api.todoist.com/sync/v9/sync",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
data={"sync_token": "*", "resource_types": '["user"]'},
)
response.raise_for_status()
user_info = response.json()
user_email = user_info["user"].get("email")
return OAuth2Credentials(
provider=self.PROVIDER_NAME,
title=None,
username=user_email,
access_token=tokens["access_token"],
refresh_token=None,
access_token_expires_at=None,
refresh_token_expires_at=None,
scopes=scopes,
)
def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials:
# Todoist does not support token refresh
return credentials
def revoke_tokens(self, credentials: OAuth2Credentials) -> bool:
return False

View File

@@ -33,5 +33,6 @@ class ProviderName(str, Enum):
SLANT3D = "slant3d"
SMTP = "smtp"
TWITTER = "twitter"
TODOIST = "todoist"
UNREAL_SPEECH = "unreal_speech"
# --8<-- [end:ProviderName]

View File

@@ -74,5 +74,4 @@ class Pagination(pydantic.BaseModel):
class RequestTopUp(pydantic.BaseModel):
amount: int
"""Amount of credits to top up."""
credit_amount: int

View File

@@ -152,7 +152,9 @@ async def get_user_credits(
async def request_top_up(
request: RequestTopUp, user_id: Annotated[str, Depends(get_user_id)]
):
checkout_url = await _user_credit_model.top_up_intent(user_id, request.amount)
checkout_url = await _user_credit_model.top_up_intent(
user_id, request.credit_amount
)
return {"checkout_url": checkout_url}
@@ -184,7 +186,9 @@ async def configure_user_auto_top_up(
else:
await _user_credit_model.top_up_credits(user_id, 0)
await set_auto_top_up(user_id, threshold=request.threshold, amount=request.amount)
await set_auto_top_up(
user_id, AutoTopUpConfig(threshold=request.threshold, amount=request.amount)
)
return "Auto top-up settings updated"

View File

@@ -320,6 +320,9 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
linear_client_id: str = Field(default="", description="Linear client ID")
linear_client_secret: str = Field(default="", description="Linear client secret")
todoist_client_id: str = Field(default="", description="Todoist client ID")
todoist_client_secret: str = Field(default="", description="Todoist client secret")
stripe_api_key: str = Field(default="", description="Stripe API Key")
stripe_webhook_secret: str = Field(default="", description="Stripe Webhook Secret")

View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand.
[[package]]
name = "aio-pika"
@@ -173,14 +173,14 @@ files = [
[[package]]
name = "anthropic"
version = "0.40.0"
version = "0.45.2"
description = "The official Python library for the anthropic API"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "anthropic-0.40.0-py3-none-any.whl", hash = "sha256:442028ae8790ff9e3b6f8912043918755af1230d193904ae2ef78cc22995280c"},
{file = "anthropic-0.40.0.tar.gz", hash = "sha256:3efeca6d9e97813f93ed34322c6c7ea2279bf0824cd0aa71b59ce222665e2b87"},
{file = "anthropic-0.45.2-py3-none-any.whl", hash = "sha256:ecd746f7274451dfcb7e1180571ead624c7e1195d1d46cb7c70143d2aedb4d35"},
{file = "anthropic-0.45.2.tar.gz", hash = "sha256:32a18b9ecd12c91b2be4cae6ca2ab46a06937b5aa01b21308d97a6d29794fb5e"},
]
[package.dependencies]
@@ -190,7 +190,7 @@ httpx = ">=0.23.0,<1"
jiter = ">=0.4.0,<1"
pydantic = ">=1.9.0,<3"
sniffio = "*"
typing-extensions = ">=4.7,<5"
typing-extensions = ">=4.10,<5"
[package.extras]
bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"]
@@ -1015,14 +1015,14 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
[[package]]
name = "google-api-python-client"
version = "2.159.0"
version = "2.160.0"
description = "Google API Client Library for Python"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_api_python_client-2.159.0-py2.py3-none-any.whl", hash = "sha256:baef0bb631a60a0bd7c0bf12a5499e3a40cd4388484de7ee55c1950bf820a0cf"},
{file = "google_api_python_client-2.159.0.tar.gz", hash = "sha256:55197f430f25c907394b44fa078545ffef89d33fd4dca501b7db9f0d8e224bd6"},
{file = "google_api_python_client-2.160.0-py2.py3-none-any.whl", hash = "sha256:63d61fb3e4cf3fb31a70a87f45567c22f6dfe87bbfa27252317e3e2c42900db4"},
{file = "google_api_python_client-2.160.0.tar.gz", hash = "sha256:a8ccafaecfa42d15d5b5c3134ced8de08380019717fc9fb1ed510ca58eca3b7e"},
]
[package.dependencies]
@@ -1393,14 +1393,14 @@ test = ["objgraph", "psutil"]
[[package]]
name = "groq"
version = "0.13.1"
version = "0.15.0"
description = "The official Python library for the groq API"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "groq-0.13.1-py3-none-any.whl", hash = "sha256:0c5d1d6df93de55de705fe73729b79baaa0c871f7575d6aa64b2962b56101b3e"},
{file = "groq-0.13.1.tar.gz", hash = "sha256:588fd5bee984f4eb46ec89552778d5698b9e9614435defef868645c19463cbcc"},
{file = "groq-0.15.0-py3-none-any.whl", hash = "sha256:c200558b67fee4b4f2bb89cc166337e3419a68c23280065770f8f8b0729c79ef"},
{file = "groq-0.15.0.tar.gz", hash = "sha256:9ad08ba6156c67d0975595a8515b517f22ff63158e063c55192e161ed3648af1"},
]
[package.dependencies]
@@ -2151,14 +2151,14 @@ files = [
[[package]]
name = "mem0ai"
version = "0.1.44"
version = "0.1.48"
description = "Long-term memory for AI Agents"
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "mem0ai-0.1.44-py3-none-any.whl", hash = "sha256:32260a2cd935035a1b16ce04ad2e4510a5bd97618709466e2d06303e0eb8d9d4"},
{file = "mem0ai-0.1.44.tar.gz", hash = "sha256:93214272915d94f673d370bb8fe7a8bfc21806267e65700b471bec454dcdfa5c"},
{file = "mem0ai-0.1.48-py3-none-any.whl", hash = "sha256:23d1bd591c36da9e1f9f013d6f87a79ef9eb1495ac27b1e380af7f819b07fee0"},
{file = "mem0ai-0.1.48.tar.gz", hash = "sha256:f5cceb768fa2898e59d55d3d472ccb983e3d9ae82ccba1d435545e16853dbeb6"},
]
[package.dependencies]
@@ -2439,14 +2439,14 @@ pydantic = ">=2.9.0,<3.0.0"
[[package]]
name = "openai"
version = "1.60.0"
version = "1.60.2"
description = "The official Python library for the openai API"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "openai-1.60.0-py3-none-any.whl", hash = "sha256:df06c43be8018274980ac363da07d4b417bd835ead1c66e14396f6f15a0d5dda"},
{file = "openai-1.60.0.tar.gz", hash = "sha256:7fa536cd4b644718645b874d2706e36dbbef38b327e42ca0623275da347ee1a9"},
{file = "openai-1.60.2-py3-none-any.whl", hash = "sha256:993bd11b96900b9098179c728026f016b4982ded7ee30dfcf4555eab1171fff9"},
{file = "openai-1.60.2.tar.gz", hash = "sha256:a8f843e10f2855713007f491d96afb2694b11b5e02cb97c7d01a0be60bc5bb51"},
]
[package.dependencies]
@@ -3451,14 +3451,14 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments
[[package]]
name = "pytest-asyncio"
version = "0.25.2"
version = "0.25.3"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075"},
{file = "pytest_asyncio-0.25.2.tar.gz", hash = "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f"},
{file = "pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3"},
{file = "pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a"},
]
[package.dependencies]
@@ -3959,14 +3959,14 @@ files = [
[[package]]
name = "sentry-sdk"
version = "2.19.2"
version = "2.20.0"
description = "Python client for Sentry (https://sentry.io)"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "sentry_sdk-2.19.2-py2.py3-none-any.whl", hash = "sha256:ebdc08228b4d131128e568d696c210d846e5b9d70aa0327dec6b1272d9d40b84"},
{file = "sentry_sdk-2.19.2.tar.gz", hash = "sha256:467df6e126ba242d39952375dd816fbee0f217d119bf454a8ce74cf1e7909e8d"},
{file = "sentry_sdk-2.20.0-py2.py3-none-any.whl", hash = "sha256:c359a1edf950eb5e80cffd7d9111f3dbeef57994cb4415df37d39fda2cf22364"},
{file = "sentry_sdk-2.20.0.tar.gz", hash = "sha256:afa82713a92facf847df3c6f63cec71eb488d826a50965def3d7722aa6f0fdab"},
]
[package.dependencies]
@@ -4011,6 +4011,7 @@ sqlalchemy = ["sqlalchemy (>=1.2)"]
starlette = ["starlette (>=0.19.1)"]
starlite = ["starlite (>=1.48)"]
tornado = ["tornado (>=6)"]
unleash = ["UnleashClient (>=6.0.1)"]
[[package]]
name = "serpent"
@@ -4229,14 +4230,14 @@ test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"]
[[package]]
name = "stripe"
version = "11.4.1"
version = "11.5.0"
description = "Python bindings for the Stripe API"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "stripe-11.4.1-py2.py3-none-any.whl", hash = "sha256:8aa47a241de0355c383c916c4ef7273ab666f096a44ee7081e357db4a36f0cce"},
{file = "stripe-11.4.1.tar.gz", hash = "sha256:7ddd251b622d490fe57d78487855dc9f4d95b1bb113607e81fd377037a133d5a"},
{file = "stripe-11.5.0-py2.py3-none-any.whl", hash = "sha256:3b2cd47ed3002328249bff5cacaee38d5e756c3899ab425d3bd07acdaf32534a"},
{file = "stripe-11.5.0.tar.gz", hash = "sha256:bc3e0358ffc23d5ecfa8aafec1fa4f048ee8107c3237bcb00003e68c8c96fa02"},
]
[package.dependencies]
@@ -4245,14 +4246,14 @@ typing-extensions = {version = ">=4.5.0", markers = "python_version >= \"3.7\""}
[[package]]
name = "supabase"
version = "2.11.0"
version = "2.12.0"
description = "Supabase client for Python."
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "supabase-2.11.0-py3-none-any.whl", hash = "sha256:67a0da498895f4cd6554935e2854b4c41f87b297b78fb9c9414902a382041406"},
{file = "supabase-2.11.0.tar.gz", hash = "sha256:2a906f7909fd9a50f944cd9332ce66c684e2d37c0864284d34c5815e6c63cc01"},
{file = "supabase-2.12.0-py3-none-any.whl", hash = "sha256:f8896f3314179fdf27f8bb8357947493ec32b98dcdac7114208aaf21cd59ce35"},
{file = "supabase-2.12.0.tar.gz", hash = "sha256:284612c3e94ff4ed2f18c985f5eba4d6e17b5e2bf16a9a24290bb83c9c217078"},
]
[package.dependencies]
@@ -4295,6 +4296,21 @@ files = [
doc = ["reno", "sphinx"]
test = ["pytest", "tornado (>=4.5)", "typeguard"]
[[package]]
name = "todoist-api-python"
version = "2.1.7"
description = "Official Python SDK for the Todoist REST API."
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "todoist_api_python-2.1.7-py3-none-any.whl", hash = "sha256:278bfe851b9bd19bde5ff5de09d813d671ef7310ba55e1962131fca5b59bb735"},
{file = "todoist_api_python-2.1.7.tar.gz", hash = "sha256:84934a19ccd83fb61010a8126362a5d7d6486c92454c111307ba55bc74903f5c"},
]
[package.dependencies]
requests = ">=2.32.3,<3.0.0"
[[package]]
name = "tomli"
version = "2.2.1"
@@ -5034,4 +5050,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<3.13"
content-hash = "38a5c750ddca1a6264fd98b7ee74d199c2bbf57d3acc189264bd9f8ec90febc2"
content-hash = "9a40ee57d7440f352b0ac0a138fa26d05cd8eadb0cf7d3a0f995fb3c36a2fa7c"

View File

@@ -10,7 +10,7 @@ packages = [{ include = "backend", format = "sdist" }]
[tool.poetry.dependencies]
python = ">=3.10,<3.13"
aio-pika = "^9.5.4"
anthropic = "^0.40.0"
anthropic = "^0.45.2"
apscheduler = "^3.11.0"
autogpt-libs = { path = "../autogpt_libs", develop = true }
click = "^8.1.7"
@@ -19,14 +19,14 @@ e2b-code-interpreter = "^1.0.1"
fastapi = "^0.115.5"
feedparser = "^6.0.11"
flake8 = "^7.0.0"
google-api-python-client = "^2.154.0"
google-api-python-client = "^2.160.0"
google-auth-oauthlib = "^1.2.1"
groq = "^0.13.1"
groq = "^0.15.0"
jinja2 = "^3.1.4"
jsonref = "^1.1.0"
jsonschema = "^4.22.0"
ollama = "^0.4.1"
openai = "^1.57.4"
openai = "^1.60.2"
praw = "~7.8.1"
prisma = "^0.15.0"
psutil = "^6.1.0"
@@ -34,13 +34,13 @@ pydantic = "^2.9.2"
pydantic-settings = "^2.3.4"
pyro5 = "^5.15"
pytest = "^8.2.1"
pytest-asyncio = "^0.25.0"
pytest-asyncio = "^0.25.3"
python-dotenv = "^1.0.1"
redis = "^5.2.0"
sentry-sdk = "2.19.2"
sentry-sdk = "2.20.0"
strenum = "^0.4.9"
stripe = "^11.3.0"
supabase = "2.11.0"
stripe = "^11.5.0"
supabase = "2.12.0"
tenacity = "^9.0.0"
tweepy = "^4.14.0"
uvicorn = { extras = ["standard"], version = "^0.34.0" }
@@ -56,6 +56,7 @@ psycopg2-binary = "^2.9.10"
google-cloud-storage = "^2.18.2"
launchdarkly-server-sdk = "^9.8.0"
mem0ai = "^0.1.44"
todoist-api-python = "^2.1.7"
moviepy = "^2.1.2"
[tool.poetry.group.dev.dependencies]

View File

@@ -93,13 +93,13 @@ async def test_block_credit_reset(server: SpinTestServer):
# set the calendar to month 2 but use current time from now
user_credit.time_now = lambda: datetime.now(timezone.utc).replace(
month=month2, day=12
month=month2, day=1
)
month2credit = await user_credit.get_credits(DEFAULT_USER_ID)
# Month 1 result should only affect month 1
user_credit.time_now = lambda: datetime.now(timezone.utc).replace(
month=month1, day=12
month=month1, day=1
)
month1credit = await user_credit.get_credits(DEFAULT_USER_ID)
await top_up(100)
@@ -107,7 +107,7 @@ async def test_block_credit_reset(server: SpinTestServer):
# Month 2 balance is unaffected
user_credit.time_now = lambda: datetime.now(timezone.utc).replace(
month=month2, day=12
month=month2, day=1
)
assert await user_credit.get_credits(DEFAULT_USER_ID) == month2credit

View File

@@ -1,18 +1,18 @@
import { withRoleAccess } from "@/lib/withRoleAccess";
import React from "react";
import { getReviewableAgents } from "@/components/admin/marketplace/actions";
import AdminMarketplaceAgentList from "@/components/admin/marketplace/AdminMarketplaceAgentList";
import AdminFeaturedAgentsControl from "@/components/admin/marketplace/AdminFeaturedAgentsControl";
// import { getReviewableAgents } from "@/components/admin/marketplace/actions";
// import AdminMarketplaceAgentList from "@/components/admin/marketplace/AdminMarketplaceAgentList";
// import AdminFeaturedAgentsControl from "@/components/admin/marketplace/AdminFeaturedAgentsControl";
import { Separator } from "@/components/ui/separator";
async function AdminMarketplace() {
const reviewableAgents = await getReviewableAgents();
// const reviewableAgents = await getReviewableAgents();
return (
<>
<AdminMarketplaceAgentList agents={reviewableAgents.items} />
<Separator className="my-4" />
<AdminFeaturedAgentsControl className="mt-4" />
{/* <AdminMarketplaceAgentList agents={reviewableAgents.items} />
<Separator className="my-4" />
<AdminFeaturedAgentsControl className="mt-4" /> */}
</>
);
}

View File

@@ -179,6 +179,7 @@ export default function PrivatePage() {
{
oauth2: "OAuth2 credentials",
api_key: "API key",
user_password: "Username & password",
}[cred.type]
}{" "}
- <code>{cred.id}</code>

View File

@@ -151,11 +151,7 @@ function SearchResults({
<div className="min-h-[500px] max-w-[1440px]">
{showAgents && agentsCount > 0 && (
<div className="mt-[36px]">
<AgentsSection
agents={agents}
sectionTitle="Agents"
className="font-[Large-Poppins] text-[18px] font-semibold leading-[28px]"
/>
<AgentsSection agents={agents} sectionTitle="Agents" />
</div>
)}

View File

@@ -109,78 +109,80 @@ export default function ResetPasswordPage() {
}
return (
<AuthCard>
<AuthHeader>Reset Password</AuthHeader>
{user ? (
<form onSubmit={changePasswordForm.handleSubmit(onChangePassword)}>
<Form {...changePasswordForm}>
<FormField
control={changePasswordForm.control}
name="password"
render={({ field }) => (
<FormItem className="mb-6">
<FormLabel>Password</FormLabel>
<FormControl>
<PasswordInput {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={changePasswordForm.control}
name="confirmPassword"
render={({ field }) => (
<FormItem className="mb-6">
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<PasswordInput {...field} />
</FormControl>
<FormDescription className="text-sm font-normal leading-tight text-slate-500">
Password needs to be at least 6 characters long
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<AuthButton
onClick={() => onChangePassword(changePasswordForm.getValues())}
isLoading={isLoading}
type="submit"
>
Update password
</AuthButton>
<AuthFeedback message={feedback} isError={isError} />
</Form>
</form>
) : (
<form onSubmit={sendEmailForm.handleSubmit(onSendEmail)}>
<Form {...sendEmailForm}>
<FormField
control={sendEmailForm.control}
name="email"
render={({ field }) => (
<FormItem className="mb-6">
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="m@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<AuthButton
onClick={() => onSendEmail(sendEmailForm.getValues())}
isLoading={isLoading}
disabled={disabled}
type="submit"
>
Send reset email
</AuthButton>
<AuthFeedback message={feedback} isError={isError} />
</Form>
</form>
)}
</AuthCard>
<div className="flex min-h-screen items-center justify-center">
<AuthCard>
<AuthHeader>Reset Password</AuthHeader>
{user ? (
<form onSubmit={changePasswordForm.handleSubmit(onChangePassword)}>
<Form {...changePasswordForm}>
<FormField
control={changePasswordForm.control}
name="password"
render={({ field }) => (
<FormItem className="mb-6">
<FormLabel>Password</FormLabel>
<FormControl>
<PasswordInput {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={changePasswordForm.control}
name="confirmPassword"
render={({ field }) => (
<FormItem className="mb-6">
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<PasswordInput {...field} />
</FormControl>
<FormDescription className="text-sm font-normal leading-tight text-slate-500">
Password needs to be at least 6 characters long
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<AuthButton
onClick={() => onChangePassword(changePasswordForm.getValues())}
isLoading={isLoading}
type="submit"
>
Update password
</AuthButton>
<AuthFeedback message={feedback} isError={isError} />
</Form>
</form>
) : (
<form onSubmit={sendEmailForm.handleSubmit(onSendEmail)}>
<Form {...sendEmailForm}>
<FormField
control={sendEmailForm.control}
name="email"
render={({ field }) => (
<FormItem className="mb-6">
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="m@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<AuthButton
onClick={() => onSendEmail(sendEmailForm.getValues())}
isLoading={isLoading}
disabled={disabled}
type="submit"
>
Send reset email
</AuthButton>
<AuthFeedback message={feedback} isError={isError} />
</Form>
</form>
)}
</AuthCard>
</div>
);
}

View File

@@ -250,7 +250,7 @@ const FlowEditor: React.FC<{
if (deletedNodeData) {
history.push({
type: "DELETE_NODE",
payload: { node: deletedNodeData },
payload: { node: deletedNodeData.data },
undo: () => addNodes(deletedNodeData),
redo: () => deleteElements({ nodes: [{ id: nodeID }] }),
});
@@ -660,7 +660,7 @@ const FlowEditor: React.FC<{
onNodeDragStop={onNodeDragEnd}
onNodeDragStart={onNodeDragStart}
deleteKeyCode={["Backspace", "Delete"]}
minZoom={0.2}
minZoom={0.1}
maxZoom={2}
className="dark:bg-slate-900"
>

View File

@@ -9,6 +9,7 @@ import RunnerOutputUI from "./runner-ui/RunnerOutputUI";
import { Node } from "@xyflow/react";
import { filterBlocksByType } from "@/lib/utils";
import { BlockIORootSchema, BlockUIType } from "@/lib/autogpt-server-api/types";
import { CustomNode } from "./CustomNode";
interface HardcodedValues {
name: any;
@@ -27,7 +28,7 @@ export interface InputItem {
interface RunnerUIWrapperProps {
nodes: Node[];
setNodes: React.Dispatch<React.SetStateAction<Node[]>>;
setNodes: React.Dispatch<React.SetStateAction<CustomNode[]>>;
setIsScheduling: React.Dispatch<React.SetStateAction<boolean>>;
isRunning: boolean;
isScheduling: boolean;

View File

@@ -10,7 +10,6 @@ const meta = {
},
tags: ["autodocs"],
argTypes: {
onRunAgent: { action: "run agent clicked" },
name: { control: "text" },
creator: { control: "text" },
shortDescription: { control: "text" },
@@ -28,8 +27,8 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
onRunAgent: () => console.log("Run agent clicked"),
name: "AI Video Generator",
storeListingVersionId: "123",
creator: "Toran Richards",
shortDescription:
"Transform ideas into breathtaking images with this AI-powered Image Generator.",

View File

@@ -15,40 +15,64 @@ type Story = StoryObj<typeof AgentTable>;
const sampleAgents: AgentTableRowProps[] = [
{
id: "agent-1",
id: 43,
agentName: "Super Coder",
description: "An AI agent that writes clean, efficient code",
imageSrc:
imageSrc: [
"https://ddz4ak4pa3d19.cloudfront.net/cache/53/b2/53b2bc7d7900f0e1e60bf64ebf38032d.jpg",
],
dateSubmitted: "2023-05-15",
status: "approved",
runs: 1500,
rating: 4.8,
onEdit: () => console.log("Edit Super Coder"),
agent_id: "43",
agent_version: 1,
sub_heading: "Super Coder",
date_submitted: "2023-05-15",
onEditSubmission: () => console.log("Edit Super Coder"),
onDeleteSubmission: () => console.log("Delete Super Coder"),
selectedAgents: new Set(),
setSelectedAgents: () => {},
},
{
id: "agent-2",
id: 44,
agentName: "Data Analyzer",
description: "Processes and analyzes large datasets with ease",
imageSrc:
imageSrc: [
"https://ddz4ak4pa3d19.cloudfront.net/cache/40/f7/40f7bc97c952f8df0f9c88d29defe8d4.jpg",
],
dateSubmitted: "2023-05-10",
status: "awaiting_review",
runs: 1200,
rating: 4.5,
onEdit: () => console.log("Edit Data Analyzer"),
agent_id: "44",
agent_version: 1,
sub_heading: "Data Analyzer",
date_submitted: "2023-05-10",
onEditSubmission: () => console.log("Edit Data Analyzer"),
onDeleteSubmission: () => console.log("Delete Data Analyzer"),
selectedAgents: new Set(),
setSelectedAgents: () => {},
},
{
id: "agent-3",
id: 45,
agentName: "UI Designer",
description: "Creates beautiful and intuitive user interfaces",
imageSrc:
imageSrc: [
"https://ddz4ak4pa3d19.cloudfront.net/cache/14/9e/149ebb9014aa8c0097e72ed89845af0e.jpg",
],
dateSubmitted: "2023-05-05",
status: "draft",
runs: 800,
rating: 4.2,
onEdit: () => console.log("Edit UI Designer"),
agent_id: "45",
agent_version: 1,
sub_heading: "UI Designer",
date_submitted: "2023-05-05",
onEditSubmission: () => console.log("Edit UI Designer"),
onDeleteSubmission: () => console.log("Delete UI Designer"),
selectedAgents: new Set(),
setSelectedAgents: () => {},
},
];

View File

@@ -16,13 +16,13 @@ export const Default: Story = {
args: {
agentName: "Super Coder",
description: "An AI agent that writes clean, efficient code",
imageSrc:
imageSrc: [
"https://ddz4ak4pa3d19.cloudfront.net/cache/53/b2/53b2bc7d7900f0e1e60bf64ebf38032d.jpg",
],
dateSubmitted: "2023-05-15",
status: "ACTIVE" as StatusType,
runs: 1500,
rating: 4.8,
onEdit: () => console.log("Edit Super Coder"),
},
};

View File

@@ -4,6 +4,7 @@ import * as React from "react";
import Image from "next/image";
import { IconStarFilled, IconMore } from "@/components/ui/icons";
import { Status, StatusType } from "./Status";
import { StoreSubmissionRequest } from "@/lib/autogpt-server-api";
export interface AgentTableCardProps {
agent_id: string;

View File

@@ -11,7 +11,6 @@ const meta = {
tags: ["autodocs"],
argTypes: {
title: { control: "text" },
heading: { control: "text" },
description: { control: "text" },
buttonText: { control: "text" },
onButtonClick: { action: "buttonClicked" },
@@ -24,7 +23,6 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
title: "Want to contribute?",
heading: "We're always looking for more Creators!",
description: "Join our ever-growing community of hackers and tinkerers",
buttonText: "Become a Creator",
onButtonClick: () => console.log("Button clicked"),
@@ -34,7 +32,6 @@ export const Default: Story = {
export const CustomText: Story = {
args: {
title: "Become a Creator Today!",
heading: "Join Our Creator Community",
description: "Share your ideas and build amazing AI agents with us",
buttonText: "Start Creating",
onButtonClick: () => console.log("Custom button clicked"),

View File

@@ -15,7 +15,6 @@ const meta = {
bio: { control: "text" },
agentsUploaded: { control: "number" },
onClick: { action: "clicked" },
avatarSrc: { control: "text" },
},
} satisfies Meta<typeof CreatorCard>;
@@ -24,49 +23,49 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
index: 0,
creatorName: "John Doe",
creatorImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
bio: "AI enthusiast and developer with a passion for creating innovative agents.",
agentsUploaded: 15,
onClick: () => console.log("Default CreatorCard clicked"),
avatarSrc: "https://github.com/shadcn.png",
},
};
export const NewCreator: Story = {
args: {
index: 1,
creatorName: "Jane Smith",
creatorImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
bio: "Excited to start my journey in AI agent development!",
agentsUploaded: 1,
onClick: () => console.log("NewCreator CreatorCard clicked"),
avatarSrc: "https://example.com/avatar2.jpg",
},
};
export const ExperiencedCreator: Story = {
args: {
index: 2,
creatorName: "Alex Johnson",
creatorImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
bio: "Veteran AI researcher with a focus on natural language processing and machine learning.",
agentsUploaded: 50,
onClick: () => console.log("ExperiencedCreator CreatorCard clicked"),
avatarSrc: "https://example.com/avatar3.jpg",
},
};
export const WithInteraction: Story = {
args: {
index: 3,
creatorName: "Sam Brown",
creatorImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
bio: "Exploring the frontiers of AI and its applications in everyday life.",
agentsUploaded: 30,
onClick: () => console.log("WithInteraction CreatorCard clicked"),
avatarSrc: "https://example.com/avatar4.jpg",
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

View File

@@ -21,51 +21,48 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
links: {
website: "https://example.com",
linkedin: "https://linkedin.com/in/johndoe",
github: "https://github.com/johndoe",
other: ["https://twitter.com/johndoe", "https://medium.com/@johndoe"],
},
links: [
"https://example.com",
"https://linkedin.com/in/johndoe",
"https://github.com/johndoe",
"https://twitter.com/johndoe",
"https://medium.com/@johndoe",
],
},
};
export const WebsiteOnly: Story = {
args: {
links: {
website: "https://example.com",
},
links: ["https://example.com"],
},
};
export const SocialLinks: Story = {
args: {
links: {
linkedin: "https://linkedin.com/in/janedoe",
github: "https://github.com/janedoe",
other: ["https://twitter.com/janedoe"],
},
links: [
"https://linkedin.com/in/janedoe",
"https://github.com/janedoe",
"https://twitter.com/janedoe",
],
},
};
export const NoLinks: Story = {
args: {
links: {},
links: [],
},
};
export const MultipleOtherLinks: Story = {
args: {
links: {
website: "https://example.com",
linkedin: "https://linkedin.com/in/creator",
github: "https://github.com/creator",
other: [
"https://twitter.com/creator",
"https://medium.com/@creator",
"https://youtube.com/@creator",
"https://tiktok.com/@creator",
],
},
links: [
"https://example.com",
"https://linkedin.com/in/creator",
"https://github.com/creator",
"https://twitter.com/creator",
"https://medium.com/@creator",
"https://youtube.com/@creator",
"https://tiktok.com/@creator",
],
},
};

View File

@@ -43,6 +43,7 @@ export const Default: Story = {
runs: 50000,
rating: 4.7,
onClick: () => console.log("Card clicked"),
backgroundColor: "bg-white",
},
};
@@ -60,6 +61,7 @@ export const LowRating: Story = {
runs: 10000,
rating: 2.8,
onClick: () => console.log("Card clicked"),
backgroundColor: "bg-white",
},
};
@@ -77,6 +79,7 @@ export const HighRuns: Story = {
runs: 1000000,
rating: 4.9,
onClick: () => console.log("Card clicked"),
backgroundColor: "bg-white",
},
};
@@ -92,6 +95,7 @@ export const NoCreatorImage: Story = {
runs: 75000,
rating: 4.5,
onClick: () => console.log("Card clicked"),
backgroundColor: "bg-white",
},
};
@@ -108,6 +112,7 @@ export const ShortDescription: Story = {
runs: 50000,
rating: 4.2,
onClick: () => console.log("Card clicked"),
backgroundColor: "bg-white",
},
};
@@ -125,6 +130,7 @@ export const WithInteraction: Story = {
runs: 200000,
rating: 4.6,
onClick: () => console.log("Card clicked"),
backgroundColor: "bg-white",
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

View File

@@ -13,7 +13,6 @@ const meta = {
argTypes: {
userName: { control: "text" },
userEmail: { control: "text" },
activeLink: { control: "text" },
avatarSrc: { control: "text" },
menuItemGroups: { control: "object" },
},
@@ -67,7 +66,6 @@ export const Default: Story = {
args: {
userName: "John Doe",
userEmail: "john.doe@example.com",
activeLink: "/marketplace",
avatarSrc: "https://avatars.githubusercontent.com/u/123456789?v=4",
menuItemGroups: defaultMenuItemGroups,
},
@@ -77,7 +75,6 @@ export const NoAvatar: Story = {
args: {
userName: "Jane Smith",
userEmail: "jane.smith@example.com",
activeLink: "/library",
menuItemGroups: defaultMenuItemGroups,
},
};
@@ -86,7 +83,6 @@ export const LongUserName: Story = {
args: {
userName: "Alexander Bartholomew Christopherson III",
userEmail: "alexander@example.com",
activeLink: "/builder",
avatarSrc: "https://avatars.githubusercontent.com/u/987654321?v=4",
menuItemGroups: defaultMenuItemGroups,
},

View File

@@ -1,9 +1,10 @@
import type { Meta, StoryObj } from "@storybook/react";
import Navbar from "./Navbar";
import { Navbar } from "./Navbar";
import { userEvent, within } from "@storybook/test";
import { IconType } from "../ui/icons";
import { ProfileDetails } from "@/lib/autogpt-server-api/types";
import { jest } from "@jest/globals";
// You can't import this here, jest is not available in storybook and will crash it
// import { jest } from "@jest/globals";
// Mock the API responses
const mockProfileData: ProfileDetails = {
@@ -19,14 +20,14 @@ const mockCreditData = {
};
// Mock the API module
jest.mock("@/lib/autogpt-server-api", () => {
return function () {
return {
getStoreProfile: () => Promise.resolve(mockProfileData),
getUserCredit: () => Promise.resolve(mockCreditData),
};
};
});
// jest.mock("@/lib/autogpt-server-api", () => {
// return function () {
// return {
// getStoreProfile: () => Promise.resolve(mockProfileData),
// getUserCredit: () => Promise.resolve(mockCreditData),
// };
// };
// });
const meta = {
title: "AGPT UI/Navbar",
@@ -36,12 +37,12 @@ const meta = {
},
tags: ["autodocs"],
argTypes: {
isLoggedIn: { control: "boolean" },
avatarSrc: { control: "text" },
// isLoggedIn: { control: "boolean" },
// avatarSrc: { control: "text" },
links: { control: "object" },
activeLink: { control: "text" },
// activeLink: { control: "text" },
menuItemGroups: { control: "object" },
params: { control: { type: "object", defaultValue: { lang: "en" } } },
// params: { control: { type: "object", defaultValue: { lang: "en" } } },
},
} satisfies Meta<typeof Navbar>;
@@ -90,11 +91,11 @@ const defaultLinks = [
export const Default: Story = {
args: {
params: { lang: "en" },
isLoggedIn: true,
// params: { lang: "en" },
// isLoggedIn: true,
links: defaultLinks,
activeLink: "/marketplace",
avatarSrc: mockProfileData.avatar_url,
// activeLink: "/marketplace",
// avatarSrc: mockProfileData.avatar_url,
menuItemGroups: defaultMenuItemGroups,
},
};
@@ -102,21 +103,21 @@ export const Default: Story = {
export const WithActiveLink: Story = {
args: {
...Default.args,
activeLink: "/library",
// activeLink: "/library",
},
};
export const LongUserName: Story = {
args: {
...Default.args,
avatarSrc: "https://avatars.githubusercontent.com/u/987654321?v=4",
// avatarSrc: "https://avatars.githubusercontent.com/u/987654321?v=4",
},
};
export const NoAvatar: Story = {
args: {
...Default.args,
avatarSrc: undefined,
// avatarSrc: undefined,
},
};
@@ -138,8 +139,8 @@ export const WithInteraction: Story = {
export const NotLoggedIn: Story = {
args: {
...Default.args,
isLoggedIn: false,
avatarSrc: undefined,
// isLoggedIn: false,
// avatarSrc: undefined,
},
};

View File

@@ -63,12 +63,7 @@ export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
<IconAutoGPTLogo className="h-full w-full" />
</div>
{links.map((link) => (
<NavbarLink
key={link.name}
name={link.name}
href={link.href}
className="font-poppins text-[20px] leading-[28px]"
/>
<NavbarLink key={link.name} name={link.name} href={link.href} />
))}
</div>
{/* Profile section */}

View File

@@ -20,7 +20,11 @@ export const NavbarLink = ({ name, href }: NavbarLinkProps) => {
const activeLink = "/" + (parts.length > 2 ? parts[2] : parts[1]);
return (
<Link href={href} data-testid={`navbar-link-${name.toLowerCase()}`}>
<Link
href={href}
data-testid={`navbar-link-${name.toLowerCase()}`}
className="font-poppins text-[20px] leading-[28px]"
>
<div
className={`h-[48px] px-5 py-4 ${
activeLink === href

View File

@@ -9,29 +9,33 @@ const meta: Meta<typeof ProfileInfoForm> = {
},
tags: ["autodocs"],
argTypes: {
displayName: {
control: "text",
description: "The display name of the user",
},
handle: {
control: "text",
description: "The user's handle/username",
},
bio: {
control: "text",
description: "User's biography text",
},
profileImage: {
control: "text",
description: "URL of the user's profile image",
},
links: {
profile: {
control: "object",
description: "Array of social media links",
},
categories: {
control: "object",
description: "Array of selected categories",
description: "The profile details of the user",
displayName: {
control: "text",
description: "The display name of the user",
},
handle: {
control: "text",
description: "The user's handle/username",
},
bio: {
control: "text",
description: "User's biography text",
},
profileImage: {
control: "text",
description: "URL of the user's profile image",
},
links: {
control: "object",
description: "Array of social media links",
},
categories: {
control: "object",
description: "Array of selected categories",
},
},
},
};
@@ -41,30 +45,35 @@ type Story = StoryObj<typeof ProfileInfoForm>;
export const Empty: Story = {
args: {
displayName: "",
handle: "",
bio: "",
profileImage: undefined,
links: [],
categories: [],
profile: {
name: "",
username: "",
description: "",
avatar_url: "",
links: [],
top_categories: [],
agent_rating: 0,
agent_runs: 0,
},
},
};
export const Filled: Story = {
args: {
displayName: "Olivia Grace",
handle: "@ograce1421",
bio: "Our agents are designed to bring happiness and positive vibes to your daily routine. Each template helps you create and live more efficiently.",
profileImage: "https://via.placeholder.com/130x130",
links: [
{ id: 1, url: "www.websitelink.com" },
{ id: 2, url: "twitter.com/oliviagrace" },
{ id: 3, url: "github.com/ograce" },
],
categories: [
{ id: 1, name: "Entertainment" },
{ id: 2, name: "Blog" },
{ id: 3, name: "Content creation" },
],
profile: {
name: "Olivia Grace",
username: "@ograce1421",
description:
"Our agents are designed to bring happiness and positive vibes to your daily routine. Each template helps you create and live more efficiently.",
avatar_url: "https://via.placeholder.com/130x130",
links: [
"www.websitelink.com",
"twitter.com/oliviagrace",
"github.com/ograce",
],
top_categories: ["Entertainment", "Blog", "Content creation"],
agent_rating: 4.5,
agent_runs: 100,
},
},
};

View File

@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { PublishAgentSelect } from "./PublishAgentSelect";
import { Agent, PublishAgentSelect } from "./PublishAgentSelect";
const meta: Meta<typeof PublishAgentSelect> = {
title: "AGPT UI/Publish Agent Select",
@@ -10,51 +10,69 @@ const meta: Meta<typeof PublishAgentSelect> = {
export default meta;
type Story = StoryObj<typeof PublishAgentSelect>;
const mockAgents = [
const mockAgents: Agent[] = [
{
name: "SEO Optimizer",
lastEdited: "2 days ago",
imageSrc: "https://picsum.photos/seed/seo/300/200",
id: "1",
version: 1,
},
{
name: "Content Writer",
lastEdited: "5 days ago",
imageSrc: "https://picsum.photos/seed/writer/300/200",
id: "1",
version: 1,
},
{
name: "Data Analyzer",
lastEdited: "1 week ago",
imageSrc: "https://picsum.photos/seed/data/300/200",
id: "1",
version: 1,
},
{
name: "Image Recognition",
lastEdited: "2 weeks ago",
imageSrc: "https://picsum.photos/seed/image/300/200",
id: "1",
version: 1,
},
{
name: "Chatbot Assistant",
lastEdited: "3 weeks ago",
imageSrc: "https://picsum.photos/seed/chat/300/200",
id: "1",
version: 1,
},
{
name: "Code Generator",
lastEdited: "1 month ago",
imageSrc: "https://picsum.photos/seed/code/300/200",
id: "1",
version: 1,
},
{
name: "AI Translator",
lastEdited: "6 weeks ago",
imageSrc: "https://picsum.photos/seed/translate/300/200",
id: "1",
version: 1,
},
{
name: "Voice Assistant",
lastEdited: "2 months ago",
imageSrc: "https://picsum.photos/seed/voice/300/200",
id: "1",
version: 1,
},
{
name: "Data Visualizer",
lastEdited: "3 months ago",
imageSrc: "https://picsum.photos/seed/visualize/300/200",
id: "1",
version: 1,
},
];

View File

@@ -29,6 +29,8 @@ export const Filled: Story = {
args: {
...Default.args,
initialData: {
agent_id: "1",
slug: "super-seo-optimizer",
title: "Super SEO Optimizer",
subheader: "Boost your website's search engine rankings",
thumbnailSrc: "https://picsum.photos/seed/seo/500/350",
@@ -44,6 +46,8 @@ export const ThreeImages: Story = {
args: {
...Default.args,
initialData: {
agent_id: "1",
slug: "super-seo-optimizer",
title: "Multi-Image Agent",
subheader: "Showcasing multiple images",
thumbnailSrc: "https://picsum.photos/seed/initial/500/350",
@@ -63,6 +67,8 @@ export const SixImages: Story = {
args: {
...Default.args,
initialData: {
agent_id: "1",
slug: "super-seo-optimizer",
title: "Gallery Agent",
subheader: "Showcasing a gallery of images",
thumbnailSrc: "https://picsum.photos/seed/gallery1/500/350",

View File

@@ -6,6 +6,18 @@ import { Button } from "../agptui/Button";
import { IconClose, IconPlus } from "../ui/icons";
import BackendAPI from "@/lib/autogpt-server-api";
export interface PublishAgentInfoInitialData {
agent_id: string;
title: string;
subheader: string;
slug: string;
thumbnailSrc: string;
youtubeLink: string;
category: string;
description: string;
additionalImages?: string[];
}
interface PublishAgentInfoProps {
onBack: () => void;
onSubmit: (
@@ -18,17 +30,7 @@ interface PublishAgentInfoProps {
categories: string[],
) => void;
onClose: () => void;
initialData?: {
agent_id: string;
title: string;
subheader: string;
slug: string;
thumbnailSrc: string;
youtubeLink: string;
category: string;
description: string;
additionalImages?: string[];
};
initialData?: PublishAgentInfoInitialData;
}
export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({

View File

@@ -16,29 +16,32 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
agentName: "Test Agent",
onSubmit: (rating) => {
console.log("Rating submitted:", rating);
},
onClose: () => {
console.log("Rating card closed");
},
// onSubmit: (rating) => {
// console.log("Rating submitted:", rating);
// },
// onClose: () => {
// console.log("Rating card closed");
// },
storeListingVersionId: "1",
},
};
export const LongAgentName: Story = {
args: {
agentName: "Very Long Agent Name That Might Need Special Handling",
onSubmit: (rating) => {
console.log("Rating submitted:", rating);
},
onClose: () => {
console.log("Rating card closed");
},
// onSubmit: (rating) => {
// console.log("Rating submitted:", rating);
// },
// onClose: () => {
// console.log("Rating card closed");
// },
storeListingVersionId: "1",
},
};
export const WithoutCallbacks: Story = {
args: {
agentName: "Test Agent",
storeListingVersionId: "1",
},
};

View File

@@ -44,6 +44,9 @@ export const Rejected: Story = {
};
export const AllStatuses: Story = {
args: {
status: "draft" as StatusType,
},
render: () => (
<div className="flex flex-col gap-4">
<Status status="draft" />

View File

@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { AgentsSection } from "./AgentsSection";
import { Agent, AgentsSection } from "./AgentsSection";
import { userEvent, within, expect } from "@storybook/test";
const meta = {
@@ -16,7 +16,7 @@ const meta = {
argTypes: {
sectionTitle: { control: "text" },
agents: { control: "object" },
onCardClick: { action: "clicked" },
// onCardClick: { action: "clicked" },
},
} satisfies Meta<typeof AgentsSection>;
@@ -25,41 +25,50 @@ type Story = StoryObj<typeof meta>;
const mockTopAgents = [
{
agentName: "SEO Optimizer Pro",
agentImage:
agent_name: "SEO Optimizer Pro",
agent_image:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
description:
"Boost your website's search engine rankings with our advanced AI-powered SEO optimization tool.",
runs: 50000,
rating: 4.7,
avatarSrc: "https://example.com/avatar1.jpg",
creator_avatar: "https://example.com/avatar1.jpg",
slug: "seo-optimizer-pro",
creator: "John Doe",
sub_heading: "SEO Expert",
},
{
agentName: "Content Writer AI",
agentImage:
agent_name: "Content Writer AI",
agent_image:
"https://upload.wikimedia.org/wikipedia/commons/c/c5/Big_buck_bunny_poster_big.jpg",
description:
"Generate high-quality, engaging content for your blog, social media, or marketing campaigns.",
runs: 75000,
rating: 4.5,
avatarSrc: "https://example.com/avatar2.jpg",
creator_avatar: "https://example.com/avatar2.jpg",
slug: "content-writer-ai",
creator: "Jane Doe",
sub_heading: "Content Writer",
},
{
agentName: "Data Analyzer Lite",
agentImage:
agent_name: "Data Analyzer Lite",
agent_image:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
description: "A basic tool for analyzing small to medium-sized datasets.",
runs: 10000,
rating: 3.8,
avatarSrc: "https://example.com/avatar3.jpg",
creator_avatar: "https://example.com/avatar3.jpg",
slug: "data-analyzer-lite",
creator: "John Doe",
sub_heading: "Data Analyst",
},
];
] satisfies Agent[];
export const Default: Story = {
args: {
sectionTitle: "Top Agents",
agents: mockTopAgents,
onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
},
};
@@ -67,7 +76,7 @@ export const SingleAgent: Story = {
args: {
sectionTitle: "Top Agents",
agents: [mockTopAgents[0]],
onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
},
};
@@ -75,7 +84,7 @@ export const NoAgents: Story = {
args: {
sectionTitle: "Top Agents",
agents: [],
onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
},
};
@@ -83,7 +92,7 @@ export const WithInteraction: Story = {
args: {
sectionTitle: "Top Agents",
agents: mockTopAgents,
onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
@@ -107,6 +116,9 @@ export const MultiRowAgents: Story = {
runs: 60000,
rating: 4.6,
creator_avatar: "https://example.com/avatar4.jpg",
slug: "image-recognition-ai",
creator: "John Doe",
sub_heading: "Image Recognition",
},
{
agent_name: "Natural Language Processor",
@@ -117,6 +129,9 @@ export const MultiRowAgents: Story = {
runs: 80000,
rating: 4.8,
creator_avatar: "https://example.com/avatar5.jpg",
slug: "natural-language-processor",
creator: "John Doe",
sub_heading: "Natural Language Processing",
},
{
agent_name: "Sentiment Analyzer",
@@ -127,6 +142,9 @@ export const MultiRowAgents: Story = {
runs: 45000,
rating: 4.3,
creator_avatar: "https://example.com/avatar6.jpg",
slug: "sentiment-analyzer",
creator: "John Doe",
sub_heading: "Sentiment Analysis",
},
{
agent_name: "Chatbot Builder",
@@ -137,6 +155,9 @@ export const MultiRowAgents: Story = {
runs: 55000,
rating: 4.4,
creator_avatar: "https://example.com/avatar7.jpg",
slug: "chatbot-builder",
creator: "John Doe",
sub_heading: "Chatbot Developer",
},
{
agent_name: "Predictive Analytics Tool",
@@ -147,6 +168,9 @@ export const MultiRowAgents: Story = {
runs: 40000,
rating: 4.2,
creator_avatar: "https://example.com/avatar8.jpg",
slug: "predictive-analytics-tool",
creator: "John Doe",
sub_heading: "Predictive Analytics",
},
{
agent_name: "Text-to-Speech Converter",
@@ -157,9 +181,12 @@ export const MultiRowAgents: Story = {
runs: 35000,
rating: 4.1,
creator_avatar: "https://example.com/avatar9.jpg",
slug: "text-to-speech-converter",
creator: "John Doe",
sub_heading: "Text-to-Speech",
},
],
onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
},
};

View File

@@ -8,6 +8,7 @@ import {
CarouselItem,
} from "@/components/ui/carousel";
import { useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
export interface Agent {
slug: string;

View File

@@ -15,7 +15,7 @@ const meta = {
tags: ["autodocs"],
argTypes: {
featuredCreators: { control: "object" },
onCardClick: { action: "cardClicked" },
// onCardClick: { action: "cardClicked" },
},
} satisfies Meta<typeof FeaturedCreators>;
@@ -64,14 +64,14 @@ const defaultCreators = [
export const Default: Story = {
args: {
featuredCreators: defaultCreators,
onCardClick: (creatorName) => console.log(`Clicked on ${creatorName}`),
// onCardClick: (creatorName) => console.log(`Clicked on ${creatorName}`),
},
};
export const SingleCreator: Story = {
args: {
featuredCreators: [defaultCreators[0]],
onCardClick: (creatorName) => console.log(`Clicked on ${creatorName}`),
// onCardClick: (creatorName) => console.log(`Clicked on ${creatorName}`),
},
};
@@ -98,14 +98,14 @@ export const ManyCreators: Story = {
num_agents: 25,
},
],
onCardClick: (creatorName) => console.log(`Clicked on ${creatorName}`),
// onCardClick: (creatorName) => console.log(`Clicked on ${creatorName}`),
},
};
export const WithInteraction: Story = {
args: {
featuredCreators: defaultCreators,
onCardClick: (creatorName) => console.log(`Clicked on ${creatorName}`),
// onCardClick: (creatorName) => console.log(`Clicked on ${creatorName}`),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

View File

@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { FeaturedSection } from "./FeaturedSection";
import { FeaturedAgent, FeaturedSection } from "./FeaturedSection";
import { userEvent, within, expect } from "@storybook/test";
const meta = {
@@ -15,7 +15,7 @@ const meta = {
tags: ["autodocs"],
argTypes: {
featuredAgents: { control: "object" },
onCardClick: { action: "clicked" },
// onCardClick: { action: "clicked" },
},
} satisfies Meta<typeof FeaturedSection>;
@@ -24,97 +24,102 @@ type Story = StoryObj<typeof meta>;
const mockFeaturedAgents = [
{
agentName: "Personalized Morning Coffee Newsletter example of three lines",
subHeading:
agent_name: "Personalized Morning Coffee Newsletter example of three lines",
sub_heading:
"Transform ideas into breathtaking images with this AI-powered Image Generator.",
creatorName: "AI Solutions Inc.",
creator: "AI Solutions Inc.",
description:
"Elevate your web content with this powerful AI Webpage Copy Improver. Designed for marketers, SEO specialists, and web developers, this tool analyses and enhances website copy for maximum impact. Using advanced language models, it optimizes text for better clarity, SEO performance, and increased conversion rates.",
runs: 50000,
rating: 4.7,
agentImage:
agent_image:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorImage:
creator_avatar:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
slug: "personalized-morning-coffee-newsletter",
},
{
agentName: "Data Analyzer Lite",
subHeading: "Basic data analysis tool",
creatorName: "DataTech",
agent_name: "Data Analyzer Lite",
sub_heading: "Basic data analysis tool",
creator: "DataTech",
description:
"A lightweight data analysis tool for basic data processing needs.",
runs: 10000,
rating: 2.8,
agentImage:
agent_image:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorImage:
creator_avatar:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
slug: "data-analyzer-lite",
},
{
agentName: "CodeAssist AI",
subHeading: "Your AI coding companion",
creatorName: "DevTools Co.",
agent_name: "CodeAssist AI",
sub_heading: "Your AI coding companion",
creator: "DevTools Co.",
description:
"An intelligent coding assistant that helps developers write better code faster.",
runs: 1000000,
rating: 4.9,
agentImage:
agent_image:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorImage:
creator_avatar:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
slug: "codeassist-ai",
},
{
agentName: "MultiTasker",
subHeading: "All-in-one productivity suite",
creatorName: "Productivity Plus",
agent_name: "MultiTasker",
sub_heading: "All-in-one productivity suite",
creator: "Productivity Plus",
description:
"A comprehensive productivity suite that combines task management, note-taking, and project planning into one seamless interface. Features include smart task prioritization, automated scheduling, and AI-powered insights to help you work more efficiently.",
runs: 75000,
rating: 4.5,
agentImage:
agent_image:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorImage:
creator_avatar:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
slug: "multitasker",
},
{
agentName: "QuickTask",
subHeading: "Fast task automation",
creatorName: "EfficientWorks",
agent_name: "QuickTask",
sub_heading: "Fast task automation",
creator: "EfficientWorks",
description: "Simple and efficient task automation tool.",
runs: 50000,
rating: 4.2,
agentImage:
agent_image:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorImage:
creator_avatar:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
slug: "quicktask",
},
];
] satisfies FeaturedAgent[];
export const Default: Story = {
args: {
featuredAgents: mockFeaturedAgents,
onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
},
};
export const SingleAgent: Story = {
args: {
featuredAgents: [mockFeaturedAgents[0]],
onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
},
};
export const NoAgents: Story = {
args: {
featuredAgents: [],
onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
},
};
export const WithInteraction: Story = {
args: {
featuredAgents: mockFeaturedAgents,
onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

View File

@@ -8,7 +8,10 @@ import {
PopoverAnchor,
} from "@/components/ui/popover";
import { PublishAgentSelect } from "../PublishAgentSelect";
import { PublishAgentInfo } from "../PublishAgentSelectInfo";
import {
PublishAgentInfo,
PublishAgentInfoInitialData,
} from "../PublishAgentSelectInfo";
import { PublishAgentAwaitingReview } from "../PublishAgentAwaitingReview";
import { Button } from "../Button";
import {
@@ -45,17 +48,17 @@ export const PublishAgentPopout: React.FC<PublishAgentPopoutProps> = ({
);
const [myAgents, setMyAgents] = React.useState<MyAgentsResponse | null>(null);
const [selectedAgent, setSelectedAgent] = React.useState<string | null>(null);
const [initialData, setInitialData] = React.useState<{
agent_id: string;
title: string;
subheader: string;
slug: string;
thumbnailSrc: string;
youtubeLink: string;
category: string;
description: string;
additionalImages?: string[];
} | null>(null);
const [initialData, setInitialData] =
React.useState<PublishAgentInfoInitialData>({
agent_id: "",
title: "",
subheader: "",
slug: "",
thumbnailSrc: "",
youtubeLink: "",
category: "",
description: "",
});
const [publishData, setPublishData] =
React.useState<StoreSubmissionRequest>(submissionData);
const [selectedAgentId, setSelectedAgentId] = React.useState<string | null>(

View File

@@ -2,16 +2,14 @@ import { LDProvider } from "launchdarkly-react-client-sdk";
import { ReactNode } from "react";
export function LaunchDarklyProvider({ children }: { children: ReactNode }) {
if (
process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === true &&
!process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID
) {
const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID;
const enabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true";
if (!enabled) return <>{children}</>;
if (!clientId) {
throw new Error("NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID is not defined");
}
return (
<LDProvider clientSideID={process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID}>
{children}
</LDProvider>
);
return <LDProvider clientSideID={clientId}>{children}</LDProvider>;
}

View File

@@ -85,6 +85,7 @@ export const providerIcons: Record<
unreal_speech: fallbackIcon,
exa: fallbackIcon,
hubspot: FaHubspot,
todoist: fallbackIcon,
};
// --8<-- [end:ProviderIconsEmbed]

View File

@@ -46,6 +46,7 @@ const providerDisplayNames: Record<CredentialsProviderName, string> = {
replicate: "Replicate",
revid: "Rev.ID",
twitter: "Twitter",
todoist: "Todoist",
unreal_speech: "Unreal Speech",
} as const;
// --8<-- [end:CredentialsProviderNames]

View File

@@ -41,7 +41,6 @@ import { LocalValuedInput } from "./ui/input";
import NodeHandle from "./NodeHandle";
import { ConnectionData } from "./CustomNode";
import { CredentialsInput } from "./integrations/credentials-input";
import { MultiSelect } from "./ui/multiselect-input";
type NodeObjectInputTreeProps = {
nodeId: string;
@@ -663,20 +662,6 @@ export const NodeGenericInputField: FC<{
handleInputClick={handleInputClick}
/>
);
case "object":
return (
<NodeKeyValueInput
nodeId={nodeId}
selfKey={propKey}
schema={propSchema}
entries={currentValue}
errors={errors}
className={className}
displayName={displayName}
connections={connections}
handleInputChange={handleInputChange}
/>
);
default:
console.warn(
`Schema for '${propKey}' specifies unknown type:`,
@@ -968,10 +953,8 @@ const NodeKeyValueInput: FC<{
>
<div>
{keyValuePairs.map(({ key, value }, index) => (
/*
The `index` is used as a DOM key instead of the actual `key`
because the `key` can change with each input, causing the input to lose focus.
*/
// The `index` is used as a DOM key instead of the actual `key`
// because the `key` can change with each input, causing the input to lose focus.
<div key={index}>
<NodeHandle
keyName={getEntryKey(key)}

View File

@@ -1,6 +1,14 @@
import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { useForm } from "react-hook-form";
import {
FieldValues,
InternalFieldName,
RegisterOptions,
useForm,
UseFormHandleSubmit,
UseFormRegister,
UseFormWatch,
} from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -72,10 +80,300 @@ const FormExample = () => {
};
export const Default: Story = {
args: {
children: <FormExample />,
// watch(callback: (data, { name, type }) => void, defaultValues?: {[key:string]: unknown}): { unsubscribe: () => void }
watch: (name?: any, defaultValue?: any) => {
if (typeof name === "function") {
return { unsubscribe: () => {} };
}
return defaultValue || {};
},
getValues: () => [],
getFieldState: (name, formState) => ({
invalid: false,
isDirty: false,
isTouched: false,
isValidating: false,
error: undefined,
}),
setError: () => {},
setValue: () => {},
trigger: async () => true,
reset: () => {},
clearErrors: () => {},
formState: {
errors: {},
isDirty: false,
isSubmitting: false,
isValid: true,
isLoading: false,
isSubmitted: false,
isSubmitSuccessful: false,
isValidating: false,
defaultValues: {},
dirtyFields: {},
touchedFields: {},
disabled: false,
submitCount: 0,
validatingFields: {},
},
resetField: () => {},
handleSubmit: (() => {
return async (e?: React.BaseSyntheticEvent) => {
e?.preventDefault();
return Promise.resolve();
};
}) as unknown as UseFormHandleSubmit<any>,
unregister: () => {},
control: {
_subjects: {
state: {
observers: [],
subscribe: () => ({ unsubscribe: () => {} }),
unsubscribe: () => {},
next: () => {},
},
array: {
observers: [],
subscribe: () => ({ unsubscribe: () => {} }),
unsubscribe: () => {},
next: () => {},
},
values: {
observers: [],
subscribe: () => ({ unsubscribe: () => {} }),
unsubscribe: () => {},
next: () => {},
},
},
_reset: () => {},
_resetDefaultValues: () => {},
_getFieldArray: () => [],
_setErrors: () => {},
_updateDisabledField: () => {},
_executeSchema: () => Promise.resolve({ errors: {} }),
handleSubmit: (onSubmit?: any) => (e?: React.BaseSyntheticEvent) => {
e?.preventDefault();
return Promise.resolve();
},
unregister: () => {},
getFieldState: () => ({
invalid: false,
isDirty: false,
isTouched: false,
isValidating: false,
error: undefined,
}),
setError: () => {},
_disableForm: () => {},
_removeUnmounted: () => {},
_names: {
mount: new Set(),
array: new Set(),
watch: new Set(),
unMount: new Set(),
disabled: new Set(),
},
_state: { mount: false, watch: false, action: false },
_options: { mode: "onSubmit", defaultValues: {} },
_formState: {
isDirty: false,
isSubmitted: false,
submitCount: 0,
isLoading: false,
isSubmitSuccessful: false,
isSubmitting: false,
isValidating: false,
isValid: true,
disabled: false,
dirtyFields: {},
touchedFields: {},
errors: {},
validatingFields: {},
},
_fields: {},
_defaultValues: {},
_formValues: {},
_proxyFormState: {
isDirty: false,
dirtyFields: false,
touchedFields: false,
errors: false,
isValid: true,
isValidating: false,
validatingFields: false,
},
_getDirty: () => false,
_updateValid: () => {},
_updateFieldArray: () => {},
_getWatch: () => ({}),
_updateFormState: () => {},
register: ((name: string, options?: RegisterOptions<any>) => ({
name,
onChange: (e: any) => Promise.resolve(),
onBlur: (e: any) => Promise.resolve(),
ref: () => {},
})) as unknown as UseFormRegister<any>,
},
register: ((name: string) => ({
name,
onChange: (e: any) => Promise.resolve(),
onBlur: (e: any) => Promise.resolve(),
ref: () => {},
})) as UseFormRegister<FieldValues>,
setFocus: () => {},
},
render: () => <FormExample />,
};
export const WithError: Story = {
args: {
children: <FormExample />,
// watch(callback: (data, { name, type }) => void, defaultValues?: {[key:string]: unknown}): { unsubscribe: () => void }
watch: (name?: any, defaultValue?: any) => {
if (typeof name === "function") {
return { unsubscribe: () => {} };
}
return defaultValue || {};
},
getValues: () => [],
getFieldState: (name, formState) => ({
invalid: false,
isDirty: false,
isTouched: false,
isValidating: false,
error: undefined,
}),
setError: () => {},
setValue: () => {},
trigger: async () => true,
reset: () => {},
clearErrors: () => {},
formState: {
errors: {},
isDirty: false,
isSubmitting: false,
isValid: true,
isLoading: false,
isSubmitted: false,
isSubmitSuccessful: false,
isValidating: false,
defaultValues: {},
dirtyFields: {},
touchedFields: {},
disabled: false,
submitCount: 0,
validatingFields: {},
},
resetField: () => {},
handleSubmit: (() => {
return async (e?: React.BaseSyntheticEvent) => {
e?.preventDefault();
return Promise.resolve();
};
}) as unknown as UseFormHandleSubmit<any>,
unregister: () => {},
control: {
_subjects: {
state: {
observers: [],
subscribe: () => ({ unsubscribe: () => {} }),
unsubscribe: () => {},
next: () => {},
},
array: {
observers: [],
subscribe: () => ({ unsubscribe: () => {} }),
unsubscribe: () => {},
next: () => {},
},
values: {
observers: [],
subscribe: () => ({ unsubscribe: () => {} }),
unsubscribe: () => {},
next: () => {},
},
},
_reset: () => {},
_resetDefaultValues: () => {},
_getFieldArray: () => [],
_setErrors: () => {},
_updateDisabledField: () => {},
_executeSchema: () => Promise.resolve({ errors: {} }),
handleSubmit: (onSubmit?: any) => (e?: React.BaseSyntheticEvent) => {
e?.preventDefault();
return Promise.resolve();
},
unregister: () => {},
getFieldState: () => ({
invalid: false,
isDirty: false,
isTouched: false,
isValidating: false,
error: undefined,
}),
setError: () => {},
_disableForm: () => {},
_removeUnmounted: () => {},
_names: {
mount: new Set(),
array: new Set(),
watch: new Set(),
unMount: new Set(),
disabled: new Set(),
},
_state: { mount: false, watch: false, action: false },
_options: { mode: "onSubmit", defaultValues: {} },
_formState: {
isDirty: false,
isSubmitted: false,
submitCount: 0,
isLoading: false,
isSubmitSuccessful: false,
isSubmitting: false,
isValidating: false,
isValid: true,
disabled: false,
dirtyFields: {},
touchedFields: {},
errors: {},
validatingFields: {},
},
_fields: {},
_defaultValues: {},
_formValues: {},
_proxyFormState: {
isDirty: false,
dirtyFields: false,
touchedFields: false,
errors: false,
isValid: true,
isValidating: false,
validatingFields: false,
},
_getDirty: () => false,
_updateValid: () => {},
_updateFieldArray: () => {},
_getWatch: () => ({}),
_updateFormState: () => {},
register: ((name: string, options?: RegisterOptions<any>) => ({
name,
onChange: (e: any) => Promise.resolve(),
onBlur: (e: any) => Promise.resolve(),
ref: () => {},
})) as unknown as UseFormRegister<any>,
},
register: ((name: string) => ({
name,
onChange: (e: any) => Promise.resolve(),
onBlur: (e: any) => Promise.resolve(),
ref: () => {},
})) as UseFormRegister<FieldValues>,
setFocus: () => {},
},
render: () => {
const FormWithError = () => {
const form = useForm<z.infer<typeof formSchema>>({
@@ -126,6 +424,151 @@ export const WithError: Story = {
};
export const WithDefaultValue: Story = {
args: {
children: <FormExample />,
// watch(callback: (data, { name, type }) => void, defaultValues?: {[key:string]: unknown}): { unsubscribe: () => void }
watch: (name?: any, defaultValue?: any) => {
if (typeof name === "function") {
return { unsubscribe: () => {} };
}
return defaultValue || {};
},
getValues: () => [],
getFieldState: (name, formState) => ({
invalid: false,
isDirty: false,
isTouched: false,
isValidating: false,
error: undefined,
}),
setError: () => {},
setValue: () => {},
trigger: async () => true,
reset: () => {},
clearErrors: () => {},
formState: {
errors: {},
isDirty: false,
isSubmitting: false,
isValid: true,
isLoading: false,
isSubmitted: false,
isSubmitSuccessful: false,
isValidating: false,
defaultValues: {},
dirtyFields: {},
touchedFields: {},
disabled: false,
submitCount: 0,
validatingFields: {},
},
resetField: () => {},
handleSubmit: (() => {
return async (e?: React.BaseSyntheticEvent) => {
e?.preventDefault();
return Promise.resolve();
};
}) as unknown as UseFormHandleSubmit<any>,
unregister: () => {},
control: {
_subjects: {
state: {
observers: [],
subscribe: () => ({ unsubscribe: () => {} }),
unsubscribe: () => {},
next: () => {},
},
array: {
observers: [],
subscribe: () => ({ unsubscribe: () => {} }),
unsubscribe: () => {},
next: () => {},
},
values: {
observers: [],
subscribe: () => ({ unsubscribe: () => {} }),
unsubscribe: () => {},
next: () => {},
},
},
_reset: () => {},
_resetDefaultValues: () => {},
_getFieldArray: () => [],
_setErrors: () => {},
_updateDisabledField: () => {},
_executeSchema: () => Promise.resolve({ errors: {} }),
handleSubmit: (onSubmit?: any) => (e?: React.BaseSyntheticEvent) => {
e?.preventDefault();
return Promise.resolve();
},
unregister: () => {},
getFieldState: () => ({
invalid: false,
isDirty: false,
isTouched: false,
isValidating: false,
error: undefined,
}),
setError: () => {},
_disableForm: () => {},
_removeUnmounted: () => {},
_names: {
mount: new Set(),
array: new Set(),
watch: new Set(),
unMount: new Set(),
disabled: new Set(),
},
_state: { mount: false, watch: false, action: false },
_options: { mode: "onSubmit", defaultValues: {} },
_formState: {
isDirty: false,
isSubmitted: false,
submitCount: 0,
isLoading: false,
isSubmitSuccessful: false,
isSubmitting: false,
isValidating: false,
isValid: true,
disabled: false,
dirtyFields: {},
touchedFields: {},
errors: {},
validatingFields: {},
},
_fields: {},
_defaultValues: {},
_formValues: {},
_proxyFormState: {
isDirty: false,
dirtyFields: false,
touchedFields: false,
errors: false,
isValid: true,
isValidating: false,
validatingFields: false,
},
_getDirty: () => false,
_updateValid: () => {},
_updateFieldArray: () => {},
_getWatch: () => ({}),
_updateFormState: () => {},
register: ((name: string, options?: RegisterOptions<any>) => ({
name,
onChange: (e: any) => Promise.resolve(),
onBlur: (e: any) => Promise.resolve(),
ref: () => {},
})) as unknown as UseFormRegister<any>,
},
register: ((name: string) => ({
name,
onChange: (e: any) => Promise.resolve(),
onBlur: (e: any) => Promise.resolve(),
ref: () => {},
})) as UseFormRegister<FieldValues>,
setFocus: () => {},
},
render: () => {
const FormWithDefaultValue = () => {
const form = useForm<z.infer<typeof formSchema>>({

View File

@@ -7,7 +7,13 @@ import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = ({ children, delayDuration = 10 }) => (
const Tooltip = ({
children,
delayDuration = 10,
}: {
children: React.ReactNode;
delayDuration?: number;
}) => (
<TooltipPrimitive.Root delayDuration={delayDuration}>
{children}
</TooltipPrimitive.Root>

View File

@@ -1,6 +1,11 @@
import { useCallback } from "react";
import { Node, Edge, useReactFlow, useViewport } from "@xyflow/react";
interface CopyableData {
nodes: Node[];
edges: Edge[];
}
export function useCopyPaste(getNextNodeId: () => string) {
const { setNodes, addEdges, getNodes, getEdges } = useReactFlow();
const { x, y, zoom } = useViewport();
@@ -20,7 +25,7 @@ export function useCopyPaste(getNextNodeId: () => string) {
selectedNodeIds.has(edge.target),
);
const copiedData = {
const copiedData: CopyableData = {
nodes: selectedNodes.map((node) => ({
...node,
data: {
@@ -36,7 +41,7 @@ export function useCopyPaste(getNextNodeId: () => string) {
if (event.key === "v" || event.key === "V") {
const copiedDataString = localStorage.getItem("copiedFlowData");
if (copiedDataString) {
const copiedData = JSON.parse(copiedDataString);
const copiedData = JSON.parse(copiedDataString) as CopyableData;
const oldToNewIdMap: Record<string, string> = {};
const viewportCenter = {
@@ -77,7 +82,7 @@ export function useCopyPaste(getNextNodeId: () => string) {
};
});
const pastedEdges = copiedData.edges.map((edge: Edge) => {
const pastedEdges = copiedData.edges.map((edge) => {
const newSourceId = oldToNewIdMap[edge.source] ?? edge.source;
const newTargetId = oldToNewIdMap[edge.target] ?? edge.target;
return {
@@ -99,10 +104,10 @@ export function useCopyPaste(getNextNodeId: () => string) {
if (oldToNewIdMap[node.id]) {
const nodeConnections = pastedEdges
.filter(
(edge) =>
(edge: Edge) =>
edge.source === node.id || edge.target === node.id,
)
.map((edge) => ({
.map((edge: Edge) => ({
edge_id: edge.id,
source: edge.source,
target: edge.target,

View File

@@ -10,7 +10,7 @@ const stripePromise = loadStripe(
export default function useCredits(): {
credits: number | null;
fetchCredits: () => void;
requestTopUp: (amount: number) => Promise<void>;
requestTopUp: (credit_amount: number) => Promise<void>;
autoTopUpConfig: { amount: number; threshold: number } | null;
fetchAutoTopUpConfig: () => void;
updateAutoTopUpConfig: (amount: number, threshold: number) => Promise<void>;
@@ -51,15 +51,14 @@ export default function useCredits(): {
);
const requestTopUp = useCallback(
async (amount: number) => {
async (credit_amount: number) => {
const stripe = await stripePromise;
if (!stripe) {
return;
}
// Convert dollar amount to credit count
const response = await api.requestTopUp(amount);
const response = await api.requestTopUp(credit_amount);
router.push(response.checkout_url);
},
[api, router],

View File

@@ -100,8 +100,8 @@ export default class BackendAPI {
return this._request("POST", "/credits/auto-top-up", config);
}
requestTopUp(amount: number): Promise<{ checkout_url: string }> {
return this._request("POST", "/credits", { amount });
requestTopUp(credit_amount: number): Promise<{ checkout_url: string }> {
return this._request("POST", "/credits", { credit_amount });
}
getUserPaymentPortalLink(): Promise<{ url: string }> {

View File

@@ -65,18 +65,21 @@ export type BlockIOObjectSubSchema = BlockIOSubSchemaMeta & {
properties: { [key: string]: BlockIOSubSchema };
default?: { [key: keyof BlockIOObjectSubSchema["properties"]]: any };
required?: (keyof BlockIOObjectSubSchema["properties"])[];
secret?: boolean;
};
export type BlockIOKVSubSchema = BlockIOSubSchemaMeta & {
type: "object";
additionalProperties: { type: "string" | "number" | "integer" };
default?: { [key: string]: string | number };
secret?: boolean;
};
export type BlockIOArraySubSchema = BlockIOSubSchemaMeta & {
type: "array";
items?: BlockIOSimpleTypeSubSchema;
default?: Array<string>;
secret?: boolean;
};
export type BlockIOStringSubSchema = BlockIOSubSchemaMeta & {
@@ -90,11 +93,13 @@ export type BlockIOStringSubSchema = BlockIOSubSchemaMeta & {
export type BlockIONumberSubSchema = BlockIOSubSchemaMeta & {
type: "integer" | "number";
default?: number;
secret?: boolean;
};
export type BlockIOBooleanSubSchema = BlockIOSubSchemaMeta & {
type: "boolean";
default?: boolean;
secret?: boolean;
};
export type CredentialsType = "api_key" | "oauth2" | "user_password";
@@ -136,6 +141,7 @@ export const PROVIDER_NAMES = {
REDDIT: "reddit",
REVID: "revid",
UNREAL_SPEECH: "unreal_speech",
TODOIST: "todoist",
} as const;
// --8<-- [end:BlockIOCredentialsSubSchema]
@@ -143,16 +149,19 @@ export type CredentialsProviderName =
(typeof PROVIDER_NAMES)[keyof typeof PROVIDER_NAMES];
export type BlockIOCredentialsSubSchema = BlockIOSubSchemaMeta & {
type: "object";
/* Mirror of backend/data/model.py:CredentialsFieldSchemaExtra */
credentials_provider: CredentialsProviderName[];
credentials_scopes?: string[];
credentials_types: Array<CredentialsType>;
discriminator?: string;
discriminator_mapping?: { [key: string]: CredentialsProviderName };
secret?: boolean;
};
export type BlockIONullSubSchema = BlockIOSubSchemaMeta & {
type: "null";
secret?: boolean;
};
// At the time of writing, combined schemas only occur on the first nested level in a
@@ -160,15 +169,21 @@ export type BlockIONullSubSchema = BlockIOSubSchemaMeta & {
type BlockIOCombinedTypeSubSchema = BlockIOSubSchemaMeta &
(
| {
type: "allOf";
allOf: [BlockIOSimpleTypeSubSchema];
secret?: boolean;
}
| {
type: "anyOf";
anyOf: BlockIOSimpleTypeSubSchema[];
default?: string | number | boolean | null;
secret?: boolean;
}
| {
type: "oneOf";
oneOf: BlockIOSimpleTypeSubSchema[];
default?: string | number | boolean | null;
secret?: boolean;
}
);

View File

@@ -1,6 +1,5 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { createClient, SupabaseClient } from "@supabase/supabase-js";
import { faker } from "@faker-js/faker";
export type TestUser = {
email: string;
@@ -26,24 +25,6 @@ function getSupabaseAdmin() {
return supabase;
}
async function createTestUser(userData: TestUser): Promise<TestUser> {
const supabase = getSupabaseAdmin();
const { data: authUser, error: authError } = await supabase.auth.signUp({
email: userData.email,
password: userData.password,
});
if (authError) {
throw new Error(`Failed to create test user: ${authError.message}`);
}
return {
...userData,
id: authUser.user?.id,
};
}
async function deleteTestUser(userId: string) {
const supabase = getSupabaseAdmin();
@@ -59,25 +40,3 @@ async function deleteTestUser(userId: string) {
);
}
}
function generateUserData(): TestUser {
return {
email: `test.${faker.string.uuid()}@example.com`,
password: faker.internet.password({ length: 12 }),
};
}
// Export just the fixture function
export const createTestUserFixture = async ({}, use) => {
let user: TestUser | null = null;
try {
const userData = generateUserData();
user = await createTestUser(userData);
await use(user);
} finally {
if (user?.id) {
await deleteTestUser(user.id);
}
}
};

View File

@@ -134,7 +134,7 @@ Below is a comprehensive list of all available blocks, categorized by their prim
|------------|-------------|
| [Twitter Post Tweet](twitter/twitter.md#twitter-post-tweet-block) | Creates a tweet on Twitter with text content and optional attachments including media, polls, quotes, or deep links |
| [Twitter Delete Tweet](twitter/twitter.md#twitter-delete-tweet-block) | Deletes a specified tweet using its tweet ID |
| [Twitter Search Recent](twitter/twitter.md#twitter-search-recent-block) | Searches for tweets matching specified criteria with options for filtering and pagination |
| [Twitter Search Recent Tweets](twitter/twitter.md#twitter-search-recent-tweets-block) | Searches for tweets matching specified criteria with options for filtering and pagination |
| [Twitter Get Quote Tweets](twitter/twitter.md#twitter-get-quote-tweets-block) | Gets tweets that quote a specified tweet ID with options for pagination and filtering |
| [Twitter Retweet](twitter/twitter.md#twitter-retweet-block) | Creates a retweet of a specified tweet using its tweet ID |
| [Twitter Remove Retweet](twitter/twitter.md#twitter-remove-retweet-block) | Removes an existing retweet of a specified tweet |
@@ -167,4 +167,31 @@ Below is a comprehensive list of all available blocks, categorized by their prim
| Twitter Send Direct Message | Working... Sends a direct message to a specified user |
| Twitter Create DM Conversation | Working... Creates a new direct message conversation |
## Todoist Integration
| Block Name | Description |
|------------|-------------|
| [Todoist Create Label](todoist.md#todoist-create-label) | Creates a new label in Todoist |
| [Todoist List Labels](todoist.md#todoist-list-labels) | Retrieves all personal labels from Todoist |
| [Todoist Get Label](todoist.md#todoist-get-label) | Retrieves a specific label by ID |
| [Todoist Create Task](todoist.md#todoist-create-task) | Creates a new task in Todoist |
| [Todoist Get Tasks](todoist.md#todoist-get-tasks) | Retrieves active tasks from Todoist |
| [Todoist Update Task](todoist.md#todoist-update-task) | Updates an existing task |
| [Todoist Close Task](todoist.md#todoist-close-task) | Completes/closes a task |
| [Todoist Reopen Task](todoist.md#todoist-reopen-task) | Reopens a completed task |
| [Todoist Delete Task](todoist.md#todoist-delete-task) | Permanently deletes a task |
| [Todoist List Projects](todoist.md#todoist-list-projects) | Retrieves all projects from Todoist |
| [Todoist Create Project](todoist.md#todoist-create-project) | Creates a new project in Todoist |
| [Todoist Get Project](todoist.md#todoist-get-project) | Retrieves details for a specific project |
| [Todoist Update Project](todoist.md#todoist-update-project) | Updates an existing project |
| [Todoist Delete Project](todoist.md#todoist-delete-project) | Deletes a project and its contents |
| [Todoist List Collaborators](todoist.md#todoist-list-collaborators) | Retrieves collaborators on a project |
| [Todoist List Sections](todoist.md#todoist-list-sections) | Retrieves sections from Todoist |
| [Todoist Get Section](todoist.md#todoist-get-section) | Retrieves details for a specific section |
| [Todoist Delete Section](todoist.md#todoist-delete-section) | Deletes a section and its tasks |
| [Todoist Create Comment](todoist.md#todoist-create-comment) | Creates a new comment on a task or project |
| [Todoist Get Comments](todoist.md#todoist-get-comments) | Retrieves all comments for a task or project |
| [Todoist Get Comment](todoist.md#todoist-get-comment) | Retrieves a specific comment by ID |
| [Todoist Update Comment](todoist.md#todoist-update-comment) | Updates an existing comment |
| [Todoist Delete Comment](todoist.md#todoist-delete-comment) | Deletes a comment |
This comprehensive list covers all the blocks available in AutoGPT. Each block is designed to perform a specific task, and they can be combined to create powerful, automated workflows. For more detailed information on each block, click on its name to view the full documentation.

View File

@@ -0,0 +1,722 @@
# Todoist Blocks
## Todoist Create Label
### What it is
A block that creates a new label in Todoist.
### What it does
Creates a new label in Todoist with specified name, order, color and favorite status.
### How it works
It takes label details as input, connects to Todoist API, creates the label and returns the created label's details.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Name | Name of the label |
| Order | Optional label order |
| Color | Optional color of the label icon |
| Is Favorite | Whether label is marked as favorite |
### Outputs
| Output | Description |
|--------|-------------|
| ID | ID of the created label |
| Name | Name of the label |
| Color | Color of the label |
| Order | Label order |
| Is Favorite | Favorite status |
| Error | Error message if request failed |
### Possible use case
Creating new labels to organize and categorize tasks in Todoist.
---
## Todoist List Labels
### What it is
A block that retrieves all personal labels from Todoist.
### What it does
Fetches all personal labels from the user's Todoist account.
### How it works
Connects to Todoist API using provided credentials and retrieves all labels.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
### Outputs
| Output | Description |
|--------|-------------|
| Labels | List of complete label data |
| Label IDs | List of label IDs |
| Label Names | List of label names |
| Error | Error message if request failed |
### Possible use case
Getting an overview of all labels to organize tasks or find specific labels.
---
## Todoist Get Label
### What it is
A block that retrieves a specific label by ID.
### What it does
Fetches details of a specific label using its ID.
### How it works
Uses the label ID to retrieve label details from Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Label ID | ID of label to retrieve |
### Outputs
| Output | Description |
|--------|-------------|
| ID | Label ID |
| Name | Label name |
| Color | Label color |
| Order | Label order |
| Is Favorite | Favorite status |
| Error | Error message if request failed |
### Possible use case
Looking up details of a specific label for editing or verification.
---
## Todoist Create Task
### What it is
A block that creates a new task in Todoist.
### What it does
Creates a new task with specified content, description, project assignment and other optional parameters.
### How it works
Takes task details and creates a new task via Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Content | Task content |
| Description | Optional task description |
| Project ID | Optional project to add task to |
| Section ID | Optional section to add task to |
| Parent ID | Optional parent task ID |
| Order | Optional task order |
| Labels | Optional task labels |
| Priority | Optional priority (1-4) |
| Due Date | Optional due date |
| Deadline Date | Optional deadline date |
| Assignee ID | Optional assignee |
| Duration Unit | Optional duration unit |
| Duration | Optional duration amount |
### Outputs
| Output | Description |
|--------|-------------|
| ID | Created task ID |
| URL | Task URL |
| Complete Data | Complete task data |
| Error | Error message if request failed |
### Possible use case
Creating new tasks with full customization of parameters.
---
## Todoist Get Tasks
### What it is
A block that retrieves active tasks from Todoist.
### What it does
Fetches tasks based on optional filters like project, section, label etc.
### How it works
Queries Todoist API with provided filters to get matching tasks.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Project ID | Optional filter by project |
| Section ID | Optional filter by section |
| Label | Optional filter by label |
| Filter | Optional custom filter string |
| Lang | Optional filter language |
| IDs | Optional specific task IDs |
### Outputs
| Output | Description |
|--------|-------------|
| IDs | List of task IDs |
| URLs | List of task URLs |
| Complete Data | Complete task data |
| Error | Error message if request failed |
### Possible use case
Retrieving tasks matching specific criteria for review or processing.
---
## Todoist Update Task
### What it is
A block that updates an existing task.
### What it does
Updates specified fields of an existing task.
### How it works
Takes task ID and updated fields, applies changes via Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Task ID | ID of task to update |
| Content | New task content |
| Description | New description |
| Project ID | New project ID |
| Section ID | New section ID |
| Parent ID | New parent task ID |
| Order | New order |
| Labels | New labels |
| Priority | New priority |
| Due Date | New due date |
| Deadline Date | New deadline date |
| Assignee ID | New assignee |
| Duration Unit | New duration unit |
| Duration | New duration |
### Outputs
| Output | Description |
|--------|-------------|
| Success | Whether update succeeded |
| Error | Error message if failed |
### Possible use case
Modifying task details like due dates, priority etc.
---
## Todoist Close Task
### What it is
A block that completes/closes a task.
### What it does
Marks a task as complete in Todoist.
### How it works
Uses task ID to mark it complete via Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Task ID | ID of task to close |
### Outputs
| Output | Description |
|--------|-------------|
| Success | Whether task was closed |
| Error | Error message if failed |
### Possible use case
Marking tasks as done in automated workflows.
---
## Todoist Reopen Task
### What it is
A block that reopens a completed task.
### What it does
Marks a completed task as active again.
### How it works
Uses task ID to reactivate via Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Task ID | ID of task to reopen |
### Outputs
| Output | Description |
|--------|-------------|
| Success | Whether task was reopened |
| Error | Error message if failed |
### Possible use case
Reactivating tasks that were closed accidentally or need to be repeated.
---
## Todoist Delete Task
### What it is
A block that permanently deletes a task.
### What it does
Removes a task completely from Todoist.
### How it works
Uses task ID to delete via Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Task ID | ID of task to delete |
### Outputs
| Output | Description |
|--------|-------------|
| Success | Whether deletion succeeded |
| Error | Error message if failed |
### Possible use case
Removing unwanted or obsolete tasks from the system.
---
## Todoist List Projects
### What it is
A block that retrieves all projects from Todoist.
### What it does
Fetches all projects and their details from a user's Todoist account.
### How it works
Connects to Todoist API using provided credentials and retrieves all projects.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
### Outputs
| Output | Description |
|--------|-------------|
| Names List | List of project names |
| IDs List | List of project IDs |
| URL List | List of project URLs |
| Complete Data | Complete project data |
| Error | Error message if request failed |
### Possible use case
Getting an overview of all projects for organization or automation.
---
## Todoist Create Project
### What it is
A block that creates a new project in Todoist.
### What it does
Creates a new project with specified name, parent project, color and other settings.
### How it works
Takes project details and creates via Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Name | Name of the project |
| Parent ID | Optional parent project ID |
| Color | Optional color of project icon |
| Is Favorite | Whether project is favorite |
| View Style | Display style (list/board) |
### Outputs
| Output | Description |
|--------|-------------|
| Success | Whether creation succeeded |
| Error | Error message if failed |
### Possible use case
Creating new projects programmatically for workflow automation.
---
## Todoist Get Project
### What it is
A block that retrieves details for a specific project.
### What it does
Fetches complete details of a single project by ID.
### How it works
Uses project ID to retrieve details via Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Project ID | ID of project to get |
### Outputs
| Output | Description |
|--------|-------------|
| Project ID | ID of the project |
| Project Name | Name of the project |
| Project URL | URL of the project |
| Complete Data | Complete project data |
| Error | Error message if failed |
### Possible use case
Looking up project details for verification or editing.
---
## Todoist Update Project
### What it is
A block that updates an existing project.
### What it does
Updates specified fields of an existing project.
### How it works
Takes project ID and updated fields, applies via Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Project ID | ID of project to update |
| Name | New project name |
| Color | New color for icon |
| Is Favorite | New favorite status |
| View Style | New display style |
### Outputs
| Output | Description |
|--------|-------------|
| Success | Whether update succeeded |
| Error | Error message if failed |
### Possible use case
Modifying project settings or reorganizing projects.
---
## Todoist Delete Project
### What it is
A block that deletes a project and its contents.
### What it does
Permanently removes a project including sections and tasks.
### How it works
Uses project ID to delete via Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Project ID | ID of project to delete |
### Outputs
| Output | Description |
|--------|-------------|
| Success | Whether deletion succeeded |
| Error | Error message if failed |
### Possible use case
Removing completed or obsolete projects.
---
## Todoist List Collaborators
### What it is
A block that retrieves collaborators on a project.
### What it does
Fetches all collaborators and their details for a specific project.
### How it works
Uses project ID to get collaborator list via Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Project ID | ID of project to check |
### Outputs
| Output | Description |
|--------|-------------|
| Collaborator IDs | List of collaborator IDs |
| Collaborator Names | List of collaborator names |
| Collaborator Emails | List of collaborator emails |
| Complete Data | Complete collaborator data |
| Error | Error message if failed |
### Possible use case
Managing project sharing and collaboration.
---
## Todoist List Sections
### What it is
A block that retrieves sections from Todoist.
### What it does
Fetches all sections, optionally filtered by project.
### How it works
Connects to Todoist API to retrieve sections list.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Project ID | Optional project filter |
### Outputs
| Output | Description |
|--------|-------------|
| Names List | List of section names |
| IDs List | List of section IDs |
| Complete Data | Complete section data |
| Error | Error message if failed |
### Possible use case
Getting section information for task organization.
---
## Todoist Get Section
### What it is
A block that retrieves details for a specific section.
### What it does
Fetches complete details of a single section by ID.
### How it works
Uses section ID to retrieve details via Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Section ID | ID of section to get |
### Outputs
| Output | Description |
|--------|-------------|
| ID | Section ID |
| Project ID | Parent project ID |
| Order | Section order |
| Name | Section name |
| Error | Error message if failed |
### Possible use case
Looking up section details for task management.
---
## Todoist Delete Section
### What it is
A block that deletes a section and its tasks.
### What it does
Permanently removes a section including all tasks.
### How it works
Uses section ID to delete via Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Section ID | ID of section to delete |
### Outputs
| Output | Description |
|--------|-------------|
| Success | Whether deletion succeeded |
| Error | Error message if failed |
### Possible use case
Removing unused sections or reorganizing projects.
---
## Todoist Create Comment
### What it is
A block that creates a new comment on a Todoist task or project.
### What it does
Creates a comment with specified content on either a task or project.
### How it works
Takes comment content and task/project ID, creates comment via Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Content | Comment content |
| ID Type | Task ID or Project ID to comment on |
| Attachment | Optional file attachment |
### Outputs
| Output | Description |
|--------|-------------|
| ID | ID of created comment |
| Content | Comment content |
| Posted At | Comment timestamp |
| Task ID | Associated task ID |
| Project ID | Associated project ID |
| Error | Error message if request failed |
### Possible use case
Adding notes and comments to tasks or projects automatically.
---
## Todoist Get Comments
### What it is
A block that retrieves all comments for a task or project.
### What it does
Fetches all comments associated with a specific task or project.
### How it works
Uses task/project ID to get comments list via Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| ID Type | Task ID or Project ID to get comments for |
### Outputs
| Output | Description |
|--------|-------------|
| Comments | List of comments |
| Error | Error message if request failed |
### Possible use case
Reviewing comment history on tasks or projects.
---
## Todoist Get Comment
### What it is
A block that retrieves a specific comment by ID.
### What it does
Fetches details of a single comment using its ID.
### How it works
Uses comment ID to retrieve details via Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Comment ID | ID of comment to retrieve |
### Outputs
| Output | Description |
|--------|-------------|
| Content | Comment content |
| ID | Comment ID |
| Posted At | Comment timestamp |
| Project ID | Associated project ID |
| Task ID | Associated task ID |
| Attachment | Optional file attachment |
| Error | Error message if request failed |
### Possible use case
Looking up specific comment details for reference.
---
## Todoist Update Comment
### What it is
A block that updates an existing comment.
### What it does
Updates the content of a specific comment.
### How it works
Takes comment ID and new content, updates via Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Comment ID | ID of comment to update |
| Content | New content for the comment |
### Outputs
| Output | Description |
|--------|-------------|
| Success | Whether update succeeded |
| Error | Error message if request failed |
### Possible use case
Modifying existing comments to fix errors or update information.
---
## Todoist Delete Comment
### What it is
A block that deletes a comment.
### What it does
Permanently removes a comment from a task or project.
### How it works
Uses comment ID to delete via Todoist API.
### Inputs
| Input | Description |
|-------|-------------|
| Credentials | Todoist API credentials |
| Comment ID | ID of comment to delete |
### Outputs
| Output | Description |
|--------|-------------|
| Success | Whether deletion succeeded |
| Error | Error message if request failed |
### Possible use case
Removing outdated or incorrect comments from tasks/projects.

View File

@@ -122,22 +122,22 @@ It uses the Twitter API (Tweepy) to fetch quote tweets for a given tweet ID, han
| max_results | Maximum number of results to return (max 100) |
| exclude | Types of tweets to exclude |
| pagination_token | Token for getting next page of results |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
|--------|-------------|
| ids | List of quote tweet IDs |
| texts | List of quote tweet text contents |
| next_token | Token for retrieving next page [more info](twitter/twitter.md#common-output). |
| next_token | Token for retrieving next page [more info](twitter.md#common-output). |
| data | Complete tweet data |
| included | Additional requested data [more info](twitter/twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). |
| included | Additional requested data [more info](twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter.md#common-output). |
| error | Error message if request failed |
### Possible use case
@@ -219,12 +219,12 @@ It uses the Twitter API (Tweepy) to fetch retweeter information for a given twee
| tweet_id | ID of the tweet to get retweeters for |
| max_results | Maximum number of results per page (1-100) |
| pagination_token | Token for getting next page of results |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -234,8 +234,8 @@ It uses the Twitter API (Tweepy) to fetch retweeter information for a given twee
| usernames | List of usernames who retweeted |
| next_token | Token for retrieving next page |
| data | Complete user data |
| included | Additional requested data [more info](twitter/twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). |
| included | Additional requested data [more info](twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter.md#common-output). |
| error | Error message if request failed |
### Possible use case
@@ -261,12 +261,12 @@ It queries the Twitter API (Tweepy) with the provided user ID to fetch tweets me
| user_id | ID of user to get mentions for |
| max_results | Number of results per page (5-100) |
| pagination_token | Token for getting next page of results |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -277,8 +277,8 @@ It queries the Twitter API (Tweepy) with the provided user ID to fetch tweets me
| userNames | List of usernames who mentioned target user |
| next_token | Token for retrieving next page |
| data | Complete tweet data |
| included | Additional requested data [more info](twitter/twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). |
| included | Additional requested data [more info](twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter.md#common-output). |
| error | Error message if request failed |
### Possible use case
@@ -303,12 +303,12 @@ It uses the Twitter API (Tweepy) to fetch tweets from the home timeline, handlin
| credentials | Twitter API credentials with required scopes |
| max_results | Number of results per page (5-100) |
| pagination_token | Token for getting next page of results |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -319,8 +319,8 @@ It uses the Twitter API (Tweepy) to fetch tweets from the home timeline, handlin
| userNames | List of usernames who authored tweets |
| next_token | Token for retrieving next page |
| data | Complete tweet data |
| included | Additional requested data [more info](twitter/twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). |
| included | Additional requested data [more info](twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter.md#common-output). |
| error | Error message if request failed |
### Possible use case
@@ -346,12 +346,12 @@ It uses the Twitter API (Tweepy) to fetch tweets from a specified user's timelin
| user_id | ID of user to get tweets from |
| max_results | Number of results per page (5-100) |
| pagination_token | Token for getting next page of results |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -362,8 +362,8 @@ It uses the Twitter API (Tweepy) to fetch tweets from a specified user's timelin
| userNames | List of usernames who authored tweets |
| next_token | Token for retrieving next page |
| data | Complete tweet data |
| included | Additional requested data [more info](twitter/twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). |
| included | Additional requested data [more info](twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter.md#common-output). |
| error | Error message if request failed |
### Possible use case
@@ -387,12 +387,12 @@ It uses the Twitter API (Tweepy) to fetch a single tweet by its ID, handling aut
|-------|-------------|
| credentials | Twitter API credentials with required scopes |
| tweet_id | ID of the tweet to fetch |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -402,8 +402,8 @@ It uses the Twitter API (Tweepy) to fetch a single tweet by its ID, handling aut
| userId | ID of tweet author |
| userName | Username of tweet author |
| data | Complete tweet data |
| included | Additional requested data [more info](twitter/twitter.md#common-output). |
| meta | Tweet metadata [more info](twitter/twitter.md#common-output). |
| included | Additional requested data [more info](twitter.md#common-output). |
| meta | Tweet metadata [more info](twitter.md#common-output). |
| error | Error message if request failed |
### Possible use case
@@ -427,12 +427,12 @@ It uses the Twitter API (Tweepy) to batch fetch tweets by their IDs, handling au
|-------|-------------|
| credentials | Twitter API credentials with required scopes |
| tweet_ids | List of tweet IDs to fetch (max 100) |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -442,8 +442,8 @@ It uses the Twitter API (Tweepy) to batch fetch tweets by their IDs, handling au
| userIds | List of tweet author IDs |
| userNames | List of tweet author usernames |
| data | Complete tweet data array |
| included | Additional requested data [more info](twitter/twitter.md#common-output). |
| meta | Tweet metadata [more info](twitter/twitter.md#common-output). |
| included | Additional requested data [more info](twitter.md#common-output). |
| meta | Tweet metadata [more info](twitter.md#common-output). |
| error | Error message if request failed |
### Possible use case
@@ -497,9 +497,9 @@ It uses the Twitter API (Tweepy) to fetch information about users who liked a gi
| tweet_id | ID of tweet to get liking users for |
| max_results | Maximum number of results to return (1-100) |
| pagination_token | Token for getting next page of results |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -508,8 +508,8 @@ It uses the Twitter API (Tweepy) to fetch information about users who liked a gi
| username | List of usernames who liked |
| next_token | Token for retrieving next page |
| data | Complete user data |
| included | Additional requested data [more info](twitter/twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). |
| included | Additional requested data [more info](twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter.md#common-output). |
| error | Error message if request failed |
### Possible use case
@@ -535,12 +535,12 @@ It uses the Twitter API (Tweepy) to fetch tweets liked by a given user ID, handl
| user_id | ID of user to get liked tweets for |
| max_results | Maximum number of results per page (5-100) |
| pagination_token | Token for getting next page of results |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -551,8 +551,8 @@ It uses the Twitter API (Tweepy) to fetch tweets liked by a given user ID, handl
| userNames | List of tweet author usernames |
| next_token | Token for retrieving next page |
| data | Complete tweet data |
| included | Additional requested data [more info](twitter/twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). |
| included | Additional requested data [more info](twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter.md#common-output). |
| error | Error message if request failed |
### Possible use case
@@ -689,12 +689,12 @@ It uses the Twitter API (Tweepy) to fetch bookmarked tweets, handling pagination
| credentials | Twitter API credentials with required scopes |
| max_results | Maximum number of results per page (1-100) |
| pagination_token | Token for getting next page of results |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -705,8 +705,8 @@ It uses the Twitter API (Tweepy) to fetch bookmarked tweets, handling pagination
| userName | List of tweet author usernames |
| next_token | Token for retrieving next page |
| data | Complete tweet data |
| included | Additional requested data [more info](twitter/twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). |
| included | Additional requested data [more info](twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter.md#common-output). |
| error | Error message if request failed |
### Possible use case
@@ -906,9 +906,9 @@ It uses the Twitter API (Tweepy) to fetch followers for a given user ID, handlin
| target_user_id | ID of user to get followers for |
| max_results | Maximum number of results per page (1-1000) |
| pagination_token | Token for getting next page of results |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -917,8 +917,8 @@ It uses the Twitter API (Tweepy) to fetch followers for a given user ID, handlin
| usernames | List of follower usernames |
| next_token | Token for retrieving next page |
| data | Complete user data |
| includes | Additional requested data [more info](twitter/twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). |
| includes | Additional requested data [more info](twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter.md#common-output). |
| error | Error message if request failed |
### Possible use case
@@ -944,9 +944,9 @@ It uses the Twitter API (Tweepy) to fetch following list for a given user ID, ha
| target_user_id | ID of user to get following list for |
| max_results | Maximum number of results per page (1-1000) |
| pagination_token | Token for getting next page of results |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -955,8 +955,8 @@ It uses the Twitter API (Tweepy) to fetch following list for a given user ID, ha
| usernames | List of following usernames |
| next_token | Token for retrieving next page |
| data | Complete user data |
| includes | Additional requested data [more info](twitter/twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). |
| includes | Additional requested data [more info](twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter.md#common-output). |
| error | Error message if request failed |
### Possible use case
@@ -1009,9 +1009,9 @@ It uses the Twitter API (Tweepy) to fetch muted users, handling authentication a
| credentials | Twitter API credentials with required scopes |
| max_results | Maximum results per page (1-1000, default 10) |
| pagination_token | Token for getting next/previous page |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -1073,9 +1073,9 @@ It uses the Twitter API (Tweepy) to fetch user data for a single user identified
|-------|-------------|
| credentials | Twitter API credentials with required scopes |
| identifier | User identifier (either user ID or username) |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -1108,9 +1108,9 @@ It uses the Twitter API (Tweepy) to batch fetch user data for multiple users ide
|-------|-------------|
| credentials | Twitter API credentials with required scopes |
| identifier | List of user identifiers (either user IDs or usernames, max 100) |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -1145,9 +1145,9 @@ It uses the Twitter API (Tweepy) to search for Spaces matching the query paramet
| query | Search term to find in Space titles |
| max_results | Maximum number of results to return (1-100, default 10) |
| state | Type of Spaces to return (live, scheduled, or all) |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| space_fields | Space-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| space_fields | Space-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -1182,9 +1182,9 @@ It uses the Twitter API (Tweepy) to batch fetch Space data for multiple Spaces,
|-------|-------------|
| credentials | Twitter API credentials with required scopes |
| identifier | Choice of lookup by Space IDs or creator user IDs |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| space_fields | Space-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| space_fields | Space-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -1216,9 +1216,9 @@ It uses the Twitter API (Tweepy) to fetch Space data for a single Space ID, hand
|-------|-------------|
| credentials | Twitter API credentials with required scopes |
| space_id | ID of Space to retrieve |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| space_fields | Space-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| space_fields | Space-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -1251,8 +1251,8 @@ It uses the Twitter API (Tweepy) to fetch buyer information for a Space, handlin
|-------|-------------|
| credentials | Twitter API credentials with required scopes |
| space_id | ID of Space to get buyers for |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -1284,12 +1284,12 @@ It uses the Twitter API (Tweepy) to fetch tweets from a Space, handling authenti
|-------|-------------|
| credentials | Twitter API credentials with required scopes |
| space_id | ID of Space to get tweets for |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -1322,9 +1322,9 @@ It uses the Twitter API (Tweepy) to fetch list data for a single list ID, handli
|-------|-------------|
| credentials | Twitter API credentials with required scopes |
| list_id | ID of the Twitter List to retrieve |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| list_fields | List-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| list_fields | List-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -1361,9 +1361,9 @@ It uses the Twitter API (Tweepy) to fetch owned lists for a given user ID, handl
| user_id | ID of user whose Lists to retrieve |
| max_results | Maximum results per page (1-100, default 10) |
| pagination_token | Token for getting next page |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| list_fields | List-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| list_fields | List-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -1532,12 +1532,12 @@ It uses the Twitter API (Tweepy) to fetch tweets from a specified List, handling
| list_id | ID of the List to get tweets from |
| max_results | Maximum number of results per page (1-100, default 10) |
| pagination_token | Token for getting next page of results |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| media_fields | Media-related fields to include [more info](twitter.md#common-input). |
| place_fields | Location-related fields to include [more info](twitter.md#common-input). |
| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). |
| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -1546,8 +1546,8 @@ It uses the Twitter API (Tweepy) to fetch tweets from a specified List, handling
| texts | List of tweet text contents |
| next_token | Token for retrieving next page |
| data | Complete tweet data array |
| included | Additional requested data [more info](twitter/twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). |
| included | Additional requested data [more info](twitter.md#common-output). |
| meta | Pagination and result metadata [more info](twitter.md#common-output). |
| error | Error message if request failed |
### Possible use case
@@ -1715,9 +1715,9 @@ It uses the Twitter API (Tweepy) to fetch pinned Lists data, handling authentica
| Input | Description |
|-------|-------------|
| credentials | Twitter API credentials with required scopes |
| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). |
| list_fields | List-specific fields to include [more info](twitter/twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). |
| expansions | Additional data fields to include [more info](twitter.md#common-input). |
| list_fields | List-specific fields to include [more info](twitter.md#common-input). |
| user_fields | User-related fields to include [more info](twitter.md#common-input). |
### Outputs
| Output | Description |
@@ -1905,3 +1905,62 @@ Information about Twitter users
- For user data in Tweets, add `expansions=Author_User_ID` and appropriate `user_fields`.
- Data returned under `includes` helps cross-reference expanded data objects with their parent entities using IDs.
## Common Output
The Twitter API returns standardized response elements across many endpoints. Here are the common output fields you'll encounter:
### data
The primary data requested in the response
| Field | Description |
|-------|-------------|
| ID | Unique identifier for the object |
| Type | Type of object (tweet, user, etc) |
| Properties | Object-specific fields like text for tweets |
### includes
Additional expanded data objects referenced in the primary data
| Field | Description |
|-------|-------------|
| Tweets | Full tweet objects that were referenced |
| Users | User profile data for authors/mentions |
| Places | Location data for geo-tagged content |
| Media | Details about attached photos/videos |
| Polls | Information about embedded polls |
### meta
Metadata about the response and pagination
| Field | Description |
|-------|-------------|
| Result_Count | Number of items returned |
| Next_Token | Token to get next page of results |
| Previous_Token | Token to get previous page |
| Newest_ID | Most recent ID in results |
| Oldest_ID | Oldest ID in results |
| Total_Tweet_Count | Total matching tweets (search) |
### errors
Details about any errors that occurred
| Field | Description |
|-------|-------------|
| Title | Brief error description |
| Detail | Detailed error message |
| Type | Error category/classification |
| Status | HTTP status code |
### Non-paginated responses
For single-object lookups:
- data: Contains requested object
- includes: Referenced objects
- errors: Any errors encountered
### Paginated responses
For multi-object lookups:
- data: Array of objects
- includes: Referenced objects
- meta: Pagination details
- errors: Any errors encountered