Compare commits

...
4 Commits
4 changed files with 295 additions and 1 deletions
+119
View File
@@ -0,0 +1,119 @@
import csv
import io
import re
import requests
class SFGoogleSheetCell:
"""
Reads a single cell from a publicly shared Google Sheet.
Row and column are 1-indexed to match Google Sheets display numbers.
The sheet must be shared as "Anyone with the link can view."
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"url": (
"STRING",
{
"multiline": False,
"default": "https://docs.google.com/spreadsheets/d/.../edit",
"tooltip": (
"Full Google Sheets URL. "
"The sheet must be shared: Share → Anyone with the link → Viewer."
),
},
),
"sheet_gid": (
"STRING",
{
"default": "0",
"tooltip": (
"Sheet tab GID — found in the URL after '#gid='. "
"Use 0 for the first/default sheet. "
"Example: for ...#gid=1234567890 enter 1234567890."
),
},
),
"row": (
"INT",
{
"default": 1,
"min": 1,
"step": 1,
"tooltip": "Row number starting from 1 (matches the row numbers shown in Google Sheets).",
},
),
"col": (
"INT",
{
"default": 1,
"min": 1,
"step": 1,
"tooltip": "Column number starting from 1 (A=1, B=2, C=3, etc.).",
},
),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("cell_value",)
FUNCTION = "get_cell"
CATEGORY = "Stillfront/Utils"
def get_cell(self, url: str, sheet_gid: str, row: int, col: int) -> tuple:
spreadsheet_id = self._extract_id(url)
gid = sheet_gid.strip() or "0"
# gviz/tq endpoint correctly handles any sheet GID, unlike export?format=csv
csv_url = (
f"https://docs.google.com/spreadsheets/d/{spreadsheet_id}"
f"/gviz/tq?tqx=out:csv&gid={gid}"
)
try:
response = requests.get(csv_url, timeout=15)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
raise RuntimeError(
f"Could not access Google Sheet (HTTP {response.status_code}). "
f"Make sure the sheet is shared as 'Anyone with the link can view'. Error: {e}"
)
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Failed to fetch Google Sheet: {e}")
content = response.content.decode("utf-8")
table = list(csv.reader(io.StringIO(content)))
# Convert 1-indexed user input to 0-indexed Python list access
row_idx = row - 1
col_idx = col - 1
if row_idx >= len(table):
raise ValueError(
f"Row {row} is out of range — the sheet has {len(table)} rows."
)
row_data = table[row_idx]
if col_idx >= len(row_data):
raise ValueError(
f"Column {col} is out of range — row {row} has {len(row_data)} columns."
)
return (str(row_data[col_idx]),)
def _extract_id(self, url: str) -> str:
match = re.search(r"/d/([a-zA-Z0-9-_]+)", url)
if match:
return match.group(1)
if re.match(r"^[a-zA-Z0-9-_]+$", url.strip()):
return url.strip()
raise ValueError(
"Could not extract spreadsheet ID. Please provide a full Google Sheets URL."
)
NODE_CLASS_MAPPINGS = {"SFGoogleSheetCell": SFGoogleSheetCell}
NODE_DISPLAY_NAME_MAPPINGS = {"SFGoogleSheetCell": "SF Google Sheet Cell"}
+159
View File
@@ -0,0 +1,159 @@
import csv
import io
import re
import requests
class SFGoogleSheetRange:
"""
Reads a range of cells from a publicly shared Google Sheet and joins them
into a single string using a configurable delimiter.
Cells are read left to right, top to bottom.
Row and column numbers are 1-indexed to match Google Sheets display numbers.
The sheet must be shared as "Anyone with the link can view."
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"url": (
"STRING",
{
"multiline": False,
"default": "https://docs.google.com/spreadsheets/d/.../edit",
"tooltip": (
"Full Google Sheets URL. "
"The sheet must be shared: Share → Anyone with the link → Viewer."
),
},
),
"sheet_gid": (
"STRING",
{
"default": "0",
"tooltip": (
"Sheet tab GID — found in the URL after '#gid='. "
"Use 0 for the first/default sheet."
),
},
),
"start_row": (
"INT",
{
"default": 1,
"min": 1,
"step": 1,
"tooltip": "First row of the range (1 = first row in the sheet).",
},
),
"start_col": (
"INT",
{
"default": 1,
"min": 1,
"step": 1,
"tooltip": "First column of the range (1 = column A).",
},
),
"end_row": (
"INT",
{
"default": 1,
"min": 1,
"step": 1,
"tooltip": "Last row of the range (inclusive).",
},
),
"end_col": (
"INT",
{
"default": 1,
"min": 1,
"step": 1,
"tooltip": "Last column of the range (inclusive).",
},
),
"delimiter": (
"STRING",
{
"default": ", ",
"multiline": False,
"tooltip": (
"String placed between each cell value in the output. "
"Can be typed directly or connected from another string node."
),
},
),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "get_range"
CATEGORY = "Stillfront/Utils"
def get_range(
self,
url: str,
sheet_gid: str,
start_row: int,
start_col: int,
end_row: int,
end_col: int,
delimiter: str,
) -> tuple:
if start_row > end_row:
raise ValueError(f"start_row ({start_row}) must be less than or equal to end_row ({end_row}).")
if start_col > end_col:
raise ValueError(f"start_col ({start_col}) must be less than or equal to end_col ({end_col}).")
table = self._fetch_sheet(url, sheet_gid)
cells = []
for r in range(start_row - 1, end_row):
if r >= len(table):
break
row_data = table[r]
for c in range(start_col - 1, end_col):
value = row_data[c] if c < len(row_data) else ""
cells.append(str(value))
return (delimiter.join(cells),)
def _fetch_sheet(self, url: str, sheet_gid: str) -> list:
spreadsheet_id = self._extract_id(url)
gid = sheet_gid.strip() or "0"
csv_url = (
f"https://docs.google.com/spreadsheets/d/{spreadsheet_id}"
f"/gviz/tq?tqx=out:csv&gid={gid}"
)
try:
response = requests.get(csv_url, timeout=15)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
raise RuntimeError(
f"Could not access Google Sheet (HTTP {response.status_code}). "
f"Make sure the sheet is shared as 'Anyone with the link can view'. Error: {e}"
)
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Failed to fetch Google Sheet: {e}")
content = response.content.decode("utf-8")
return list(csv.reader(io.StringIO(content)))
def _extract_id(self, url: str) -> str:
match = re.search(r"/d/([a-zA-Z0-9-_]+)", url)
if match:
return match.group(1)
if re.match(r"^[a-zA-Z0-9-_]+$", url.strip()):
return url.strip()
raise ValueError(
"Could not extract spreadsheet ID. Please provide a full Google Sheets URL."
)
NODE_CLASS_MAPPINGS = {"SFGoogleSheetRange": SFGoogleSheetRange}
NODE_DISPLAY_NAME_MAPPINGS = {"SFGoogleSheetRange": "SF Google Sheet Range"}
+16
View File
@@ -0,0 +1,16 @@
class SFVersionCheck:
@classmethod
def INPUT_TYPES(cls):
return {"required": {}}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("version",)
FUNCTION = "get_version"
CATEGORY = "Stillfront/Utils"
def get_version(self):
return ("SF ComfyUI Nodes v1.0.2",)
NODE_CLASS_MAPPINGS = {"SFVersionCheck": SFVersionCheck}
NODE_DISPLAY_NAME_MAPPINGS = {"SFVersionCheck": "SF Version Check"}
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-sf-nodes"
description = "Custom nodes for ComfyUI including LLM Chat with support for Claude, Gemini, and OpenAI models, WaveSpeed AI image/video generation, VertexAI integration, and workflow utilities"
version = "1.0.0"
version = "1.0.4"
license = { file = "LICENSE" }
dependencies = [
"requests>=2.31.0",