Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion parallel_web_tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
run_tasks,
)

__version__ = "0.0.3"
__version__ = "0.0.4"

__all__ = [
# Auth
Expand Down
12 changes: 6 additions & 6 deletions parallel_web_tools/integrations/bigquery/cloud_function/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
import os
from typing import Any

import functions_framework

Check failure on line 22 in parallel_web_tools/integrations/bigquery/cloud_function/main.py

View workflow job for this annotation

GitHub Actions / lint

Pyrefly missing-import

Cannot find module `functions_framework` Looked in these locations (from config in `/home/runner/work/parallel-web-tools/parallel-web-tools/pyproject.toml`): Import root (inferred from project layout): "/home/runner/work/parallel-web-tools/parallel-web-tools" Site package path queried from interpreter: ["/opt/hostedtoolcache/Python/3.12.12/x64/lib/python3.12", "/opt/hostedtoolcache/Python/3.12.12/x64/lib/python3.12/lib-dynload", "/home/runner/work/parallel-web-tools/parallel-web-tools/.venv/lib/python3.12/site-packages", "/home/runner/work/parallel-web-tools/parallel-web-tools"]
from flask import Request, jsonify

Check failure on line 23 in parallel_web_tools/integrations/bigquery/cloud_function/main.py

View workflow job for this annotation

GitHub Actions / lint

Pyrefly missing-import

Cannot find module `flask` Looked in these locations (from config in `/home/runner/work/parallel-web-tools/parallel-web-tools/pyproject.toml`): Import root (inferred from project layout): "/home/runner/work/parallel-web-tools/parallel-web-tools" Site package path queried from interpreter: ["/opt/hostedtoolcache/Python/3.12.12/x64/lib/python3.12", "/opt/hostedtoolcache/Python/3.12.12/x64/lib/python3.12/lib-dynload", "/home/runner/work/parallel-web-tools/parallel-web-tools/.venv/lib/python3.12/site-packages", "/home/runner/work/parallel-web-tools/parallel-web-tools"]
from google.cloud import secretmanager

Check failure on line 24 in parallel_web_tools/integrations/bigquery/cloud_function/main.py

View workflow job for this annotation

GitHub Actions / lint

Pyrefly missing-module-attribute

Could not import `secretmanager` from `google.cloud`

# Import shared enrichment utilities
from parallel_web_tools.core import enrich_batch
Expand Down Expand Up @@ -155,11 +155,11 @@
output_columns = [str(output_columns)]

parsed_calls.append(
{
"index": i,
"input_data": input_data,
"output_columns": output_columns,
}

Check failure on line 162 in parallel_web_tools/integrations/bigquery/cloud_function/main.py

View workflow job for this annotation

GitHub Actions / lint

Pyrefly bad-argument-type

Argument `dict[str, int | list[str] | list[Unknown] | str]` is not assignable to parameter `object` with type `dict[str, int | str]` in function `list.append`
)

valid_calls = [c for c in parsed_calls if "error" not in c]
Expand All @@ -167,30 +167,30 @@

# Use batch processing if all calls have same output columns
if len(valid_calls) > 1:
output_cols_set = {tuple(c["output_columns"]) for c in valid_calls}

Check failure on line 170 in parallel_web_tools/integrations/bigquery/cloud_function/main.py

View workflow job for this annotation

GitHub Actions / lint

Pyrefly bad-argument-type

Argument `int | str` is not assignable to parameter `iterable` with type `Iterable[@_]` in function `tuple.__new__` Protocol `Iterable` requires attribute `__iter__`
if len(output_cols_set) == 1:
common_output_cols = valid_calls[0]["output_columns"]
inputs = [c["input_data"] for c in valid_calls]

logger.info(f"Using batch processing for {len(inputs)} inputs")
batch_results = _process_batch(inputs, common_output_cols, processor)

Check failure on line 176 in parallel_web_tools/integrations/bigquery/cloud_function/main.py

View workflow job for this annotation

GitHub Actions / lint

Pyrefly bad-argument-type

Argument `int | str` is not assignable to parameter `output_columns` with type `list[str]` in function `_process_batch`

Check failure on line 176 in parallel_web_tools/integrations/bigquery/cloud_function/main.py

View workflow job for this annotation

GitHub Actions / lint

Pyrefly bad-argument-type

Argument `list[int | str]` is not assignable to parameter `inputs` with type `list[dict[str, Any]]` in function `_process_batch`

replies = [""] * len(calls)
replies: list[dict[str, Any] | None] = [None] * len(calls)
for error_call in error_calls:
replies[error_call["index"]] = json.dumps({"error": error_call["error"]})
replies[error_call["index"]] = {"error": error_call["error"]}

Check failure on line 180 in parallel_web_tools/integrations/bigquery/cloud_function/main.py

View workflow job for this annotation

GitHub Actions / lint

Pyrefly unsupported-operation

Cannot set item in `list[dict[str, Any] | None]` No matching overload found for function `list.__setitem__` called with arguments: (str, dict[str, int | str]) Possible overloads: (key: SupportsIndex, value: dict[str, Any] | None, /) -> None [closest match] (key: slice[Any, Any, Any], value: Iterable[dict[str, Any] | None], /) -> None
for j, valid_call in enumerate(valid_calls):
replies[valid_call["index"]] = json.dumps(batch_results[j])
replies[valid_call["index"]] = batch_results[j]

Check failure on line 182 in parallel_web_tools/integrations/bigquery/cloud_function/main.py

View workflow job for this annotation

GitHub Actions / lint

Pyrefly unsupported-operation

Cannot set item in `list[dict[str, Any] | None]` No matching overload found for function `list.__setitem__` called with arguments: (str, dict[str, Any]) Possible overloads: (key: SupportsIndex, value: dict[str, Any] | None, /) -> None [closest match] (key: slice[Any, Any, Any], value: Iterable[dict[str, Any] | None], /) -> None

return jsonify({"replies": replies})

# Fallback: process individually
replies = [""] * len(calls)
replies: list[dict[str, Any] | None] = [None] * len(calls)
for error_call in error_calls:
replies[error_call["index"]] = json.dumps({"error": error_call["error"]})
replies[error_call["index"]] = {"error": error_call["error"]}

Check failure on line 189 in parallel_web_tools/integrations/bigquery/cloud_function/main.py

View workflow job for this annotation

GitHub Actions / lint

Pyrefly unsupported-operation

Cannot set item in `list[dict[str, Any] | None]` No matching overload found for function `list.__setitem__` called with arguments: (str, dict[str, int | str]) Possible overloads: (key: SupportsIndex, value: dict[str, Any] | None, /) -> None [closest match] (key: slice[Any, Any, Any], value: Iterable[dict[str, Any] | None], /) -> None

for valid_call in valid_calls:
result = _process_batch([valid_call["input_data"]], valid_call["output_columns"], processor)
replies[valid_call["index"]] = json.dumps(result[0] if result else {"error": "No result"})
replies[valid_call["index"]] = result[0] if result else {"error": "No result"}

return jsonify({"replies": replies})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Cloud Function dependencies for BigQuery Remote Function
functions-framework>=3.0.0
flask>=3.0.0
parallel-web-tools>=0.0.3
parallel-web-tools>=0.0.4
google-cloud-secret-manager>=2.20.0
76 changes: 65 additions & 11 deletions parallel_web_tools/integrations/bigquery/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ def _run_command(cmd: list[str], check: bool = True) -> subprocess.CompletedProc
"""Run a shell command and return the result."""
result = subprocess.run(cmd, capture_output=True, text=True)
if check and result.returncode != 0:
raise RuntimeError(f"Command failed: {' '.join(cmd)}\n{result.stderr}")
# Include both stdout and stderr as some tools output errors to stdout
error_output = result.stderr or result.stdout or "(no output)"
raise RuntimeError(f"Command failed: {' '.join(cmd)}\n{error_output}")
return result


Expand Down Expand Up @@ -112,6 +114,21 @@ def deploy_bigquery_integration(
Raises:
RuntimeError: If any deployment step fails or user declines confirmation.
"""
# Check for required CLI tools
import shutil

missing_tools = []
if not shutil.which("gcloud"):
missing_tools.append("gcloud")
if not shutil.which("bq"):
missing_tools.append("bq")

if missing_tools:
raise RuntimeError(
f"Required CLI tools not found: {', '.join(missing_tools)}\n"
"Please install the Google Cloud SDK: https://cloud.google.com/sdk/docs/install"
)

print(f"Checking for existing resources in {project_id}...")

# Check for existing resources
Expand Down Expand Up @@ -150,10 +167,17 @@ def deploy_bigquery_integration(
result = _run_command(["gcloud", "secrets", "describe", secret_name, "--project", project_id], check=False)

if result.returncode == 0:
# Update existing secret
_run_command(
["gcloud", "secrets", "versions", "add", secret_name, "--data-file=-", "--project", project_id], check=True
# Update existing secret - use Popen to provide api_key via stdin
process = subprocess.Popen(
["gcloud", "secrets", "versions", "add", secret_name, "--data-file=-", "--project", project_id],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
stdout, stderr = process.communicate(input=api_key)
if process.returncode != 0:
raise RuntimeError(f"Failed to update secret: {stderr}")
else:
# Create new secret
process = subprocess.Popen(
Expand All @@ -172,7 +196,9 @@ def deploy_bigquery_integration(
stderr=subprocess.PIPE,
text=True,
)
process.communicate(input=api_key)
stdout, stderr = process.communicate(input=api_key)
if process.returncode != 0:
raise RuntimeError(f"Failed to create secret: {stderr}")

secret_resource = f"projects/{project_id}/secrets/{secret_name}/versions/latest"

Expand All @@ -187,7 +213,7 @@ def deploy_bigquery_integration(
"deploy",
function_name,
"--gen2",
"--runtime=python311",
"--runtime=python312",
"--region",
region,
"--source",
Expand Down Expand Up @@ -274,8 +300,8 @@ def deploy_bigquery_integration(
"bq",
"show",
"--connection",
f"{project_id}.{region}.{connection_id}",
"--format=json",
f"{project_id}.{region}.{connection_id}",
]
)
connection_info = json.loads(result.stdout)
Expand Down Expand Up @@ -333,10 +359,24 @@ def deploy_bigquery_integration(
)

# Execute each CREATE FUNCTION statement
for statement in sql.split(";"):
statement = statement.strip()
if statement.startswith("CREATE"):
_run_command(["bq", "query", "--use_legacy_sql=false", statement + ";"])
# Strip SQL comments (lines starting with --) before checking for CREATE
def strip_sql_comments(sql_text: str) -> str:
lines = [line for line in sql_text.split("\n") if not line.strip().startswith("--")]
return "\n".join(lines).strip()

statements = []
for chunk in sql.split(";"):
clean = strip_sql_comments(chunk)
if clean.startswith("CREATE"):
statements.append(clean)

if not statements:
print(" Warning: No CREATE statements found in SQL template")
for statement in statements:
# Extract function name for logging
func_name = statement.split("`")[1] if "`" in statement else "unknown"
print(f" Creating {func_name}...")
_run_command(["bq", "query", "--use_legacy_sql=false", f"--project_id={project_id}", statement + ";"])

print("\nDeployment complete!")

Expand All @@ -346,10 +386,24 @@ def deploy_bigquery_integration(
"dataset_id": dataset_id,
"function_url": function_url,
"example_query": f"""
-- Basic usage (returns JSON with enriched fields and basis/citations)
SELECT `{project_id}.{dataset_id}.parallel_enrich`(
JSON_OBJECT('company_name', 'Google', 'website', 'google.com'),
JSON_ARRAY('CEO name', 'Founding year', 'Brief description')
) as enriched_data;

-- Parsing the JSON result into columns
SELECT
JSON_VALUE(enriched_data, '$.ceo_name') as ceo_name,
JSON_VALUE(enriched_data, '$.founding_year') as founding_year,
JSON_VALUE(enriched_data, '$.brief_description') as description,
JSON_QUERY(enriched_data, '$.basis') as citations
FROM (
SELECT `{project_id}.{dataset_id}.parallel_enrich`(
JSON_OBJECT('company_name', 'Google', 'website', 'google.com'),
JSON_ARRAY('CEO name', 'Founding year', 'Brief description')
) as enriched_data
);
""".strip(),
}

Expand Down
34 changes: 4 additions & 30 deletions parallel_web_tools/integrations/bigquery/sql/create_functions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,14 @@
-- {function_url} - Deployed Cloud Function URL

-- Main enrichment function
-- Accepts JSON input and returns JSON output
CREATE OR REPLACE FUNCTION `{project_id}.{dataset_id}.parallel_enrich`(
input_data STRING,
output_columns STRING
input_data JSON,
output_columns JSON
)
RETURNS STRING
RETURNS JSON
REMOTE WITH CONNECTION `{project_id}.{location}.{connection_id}`
OPTIONS (
endpoint = '{function_url}',
user_defined_context = [("processor", "lite-fast")]
);

-- Convenience function for company enrichment
CREATE OR REPLACE FUNCTION `{project_id}.{dataset_id}.parallel_enrich_company`(
company_name STRING,
company_website STRING,
fields STRING
)
RETURNS STRING
AS (
`{project_id}.{dataset_id}.parallel_enrich`(
JSON_OBJECT('company_name', company_name, 'website', company_website),
fields
)
);

-- Example queries:
--
-- SELECT parallel_enrich(
-- JSON_OBJECT('company_name', 'Google', 'website', 'google.com'),
-- JSON_ARRAY('CEO name', 'Founding year', 'Brief description')
-- );
--
-- SELECT parallel_enrich_company(
-- 'Apple',
-- 'apple.com',
-- JSON_ARRAY('CEO name', 'Market cap', 'Industry')
-- );
11 changes: 10 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "parallel-web-tools"
version = "0.0.3"
version = "0.0.4"
description = "Parallel Tools: CLI and data enrichment utilities for the Parallel API"
readme = "README.md"
requires-python = ">=3.12"
Expand Down Expand Up @@ -88,6 +88,15 @@ dev = [

[tool.hatch.build.targets.wheel]
packages = ["parallel_web_tools"]
artifacts = [
"*.sql",
"*.txt", # requirements.txt for cloud function
]

[tool.hatch.build.targets.sdist]
include = [
"/parallel_web_tools",
]

[tool.pyrefly]
project-includes = [
Expand Down
2 changes: 1 addition & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def test_version(self, runner):
"""Should show version."""
result = runner.invoke(main, ["--version"])
assert result.exit_code == 0
assert "0.0.3" in result.output
assert "0.0.4" in result.output


class TestAuthCommand:
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.