spice-mcp 0.1.0__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of spice-mcp might be problematic. Click here for more details.
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/.github/workflows/ci.yml +6 -6
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/PKG-INFO +75 -15
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/README.md +74 -14
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/pyproject.toml +18 -1
- spice_mcp-0.1.1/scripts/bridgez/make_circle_comparison.py +114 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/extract.py +12 -7
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/typing_utils.py +3 -1
- spice_mcp-0.1.1/tests/offline/test_typing_utils.py +62 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/uv.lock +408 -2
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/.factory/droids/agentic-documentation-systems-droid.md +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/.factory/droids/development-workflow-optimization-droid.md +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/.factory/droids/production-python-development-droid.md +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/.factory/droids/temp-smoke-droid.md +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/.gitignore +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/.python-version +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/CONTRIBUTING.md +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/LICENSE +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/bin/bat +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/bin/fd +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/debug_api.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/architecture.md +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/codex_cli.md +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/codex_cli_tools.md +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/commands.md +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/config.md +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/development.md +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/discovery.md +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/dune_api.md +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/index.md +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/installation.md +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/tools.md +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/pytest.ini +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/scripts/codex_tools_doctor.sh +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/__init__.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/__init__.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/__init__.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/admin.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/cache.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/client.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/helpers.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/transport.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/types.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/urls.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/http_client.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/config.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/core/__init__.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/core/errors.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/core/models.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/core/ports.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/logging/query_history.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/mcp/__init__.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/mcp/server.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/mcp/tools/__init__.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/mcp/tools/base.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/mcp/tools/execute_query.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/mcp/tools/sui_package_overview.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/observability/__init__.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/observability/logging.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/polars_utils.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/py.typed +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/service_layer/__init__.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/service_layer/discovery_service.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/service_layer/query_admin_service.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/service_layer/query_service.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/service_layer/sui_service.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/cassettes/.gitkeep +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/config/environments.yaml +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/config/test_queries.yaml +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/conftest.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/fastmcp/test_resources_and_validation.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/fastmcp/test_server_fastmcp.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/fastmcp/test_server_mcp_extras.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/http_stubbed/test_age.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/http_stubbed/test_errors.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/http_stubbed/test_pagination.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/live/test_live_basic.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/live/test_live_sui.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/mcp/conftest.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/mcp/test_resources.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/mcp/test_tool_contracts.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/offline/test_cache.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/offline/test_discovery.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/offline/test_dune_adapter.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/offline/test_parsing.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/offline/test_query_history.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/offline/test_show_rewrite.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/offline/test_timeout.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/offline/test_urls.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/comprehensive_test_runner.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/run_tests.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_api_health.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_cache_functionality.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_data_types.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_dune_connectivity.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_dune_query_execution.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_error_handling.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_mcp_simulation.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_mcp_tools.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_performance.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_query_lifecycle.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_resilience.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_resource_management.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/style/test_polars_lazy.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/support/api_client.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/support/helpers.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/support/query_factory.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/support/test_data.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/tools/test_additional_mcp_tools.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/tools/test_execute_query_tool.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/tools/test_health_tool.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/tools/test_query_service.py +0 -0
- {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/tools/test_schemas.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
name: CI
|
|
1
|
+
name: CI (disabled by default)
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
# Disable automatic triggers; allow manual runs only
|
|
5
|
+
workflow_dispatch:
|
|
6
6
|
|
|
7
7
|
permissions:
|
|
8
8
|
contents: read
|
|
@@ -48,7 +48,7 @@ jobs:
|
|
|
48
48
|
- name: Install dependencies (uv)
|
|
49
49
|
run: |
|
|
50
50
|
uv --version
|
|
51
|
-
uv sync --frozen --dev
|
|
51
|
+
uv sync --frozen --group dev
|
|
52
52
|
|
|
53
53
|
- name: Ruff lint
|
|
54
54
|
run: uv run ruff check --output-format=github --color always .
|
|
@@ -93,7 +93,7 @@ jobs:
|
|
|
93
93
|
- name: Install dependencies (uv)
|
|
94
94
|
run: |
|
|
95
95
|
uv --version
|
|
96
|
-
uv sync --frozen --dev
|
|
96
|
+
uv sync --frozen --group dev
|
|
97
97
|
|
|
98
98
|
- name: mypy
|
|
99
99
|
env:
|
|
@@ -132,7 +132,7 @@ jobs:
|
|
|
132
132
|
- name: Install dependencies (uv)
|
|
133
133
|
run: |
|
|
134
134
|
uv --version
|
|
135
|
-
uv sync --frozen --dev
|
|
135
|
+
uv sync --frozen --group dev
|
|
136
136
|
|
|
137
137
|
- name: Run tests (skip live) with coverage
|
|
138
138
|
env:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spice-mcp
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: MCP server for Dune Analytics data access
|
|
5
5
|
Author-email: Evan-Kim2028 <ekcopersonal@gmail.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -21,27 +21,87 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
|
|
22
22
|
# spice-mcp
|
|
23
23
|
|
|
24
|
-
spice-mcp is an MCP server for Dune Analytics. It wraps a curated subset of the original Spice client inside a clean architecture (`core` models/ports → `adapters.dune` → service layer → FastMCP tools) and adds agent-friendly workflows for discovery and Sui package exploration. Results are Polars-first in Python and compact, token-efficient in MCP responses.
|
|
24
|
+
spice-mcp is an MCP server for [Dune](https://dune.com/) Analytics. It wraps a curated subset of the original Spice client inside a clean architecture (`core` models/ports → `adapters.dune` → service layer → FastMCP tools) and adds agent-friendly workflows for discovery and Sui package exploration. Results are Polars-first in Python and compact, token-efficient in MCP responses.
|
|
25
25
|
|
|
26
26
|
Requirements: Python 3.13+
|
|
27
27
|
|
|
28
28
|
This project uses FastMCP for typed, decorator-registered tools and resources.
|
|
29
29
|
|
|
30
|
-
Highlights
|
|
30
|
+
## Highlights
|
|
31
31
|
- Polars LazyFrame-first pipeline: results stay lazy until explicitly materialized
|
|
32
|
-
- Ports/adapters layering for maintainable integrations (
|
|
32
|
+
- Ports/adapters layering for maintainable integrations ([docs/architecture.md](docs/architecture.md))
|
|
33
33
|
- Discovery utilities (find schemas/tables, describe columns)
|
|
34
34
|
- Sui package workflows (events/transactions/objects) with safe defaults
|
|
35
35
|
- JSONL query history + SQL artifacts (SHA-256) for reproducibility
|
|
36
36
|
- Rich MCP surface: query info/run, discovery, health, Sui, and Dune admin (create/update/fork)
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
## What is Dune?
|
|
39
|
+
[Dune](https://dune.com/) is a crypto data platform providing curated blockchain datasets and a public API to run and fetch query results. See the [Dune Docs](https://dune.com/docs) and [Dune API](https://dune.com/docs/api/) for full details.
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
39
42
|
- Export `DUNE_API_KEY` in your shell (the server can also load a local `.env`; set `SPICE_MCP_SKIP_DOTENV=1` to skip during tests).
|
|
40
43
|
- Install dependencies (`uv sync` or `pip install -e .`).
|
|
41
44
|
- Start the FastMCP stdio server:
|
|
42
45
|
- `python -m spice_mcp.mcp.server --env PYTHONPATH=$(pwd)/src`
|
|
43
46
|
- or install the console script via `uv tool install .` and run `spice-mcp`.
|
|
44
47
|
|
|
48
|
+
## Cursor IDE Setup
|
|
49
|
+
|
|
50
|
+
To use spice-mcp with Cursor IDE:
|
|
51
|
+
|
|
52
|
+
1. **Install the MCP Server**:
|
|
53
|
+
```bash
|
|
54
|
+
# Install dependencies and package
|
|
55
|
+
uv sync
|
|
56
|
+
pip install -e .
|
|
57
|
+
|
|
58
|
+
# Or install via uv tool (creates console script)
|
|
59
|
+
uv tool install .
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
2. **Configure Cursor**:
|
|
63
|
+
- Open Cursor Settings → MCP Servers
|
|
64
|
+
- Add new MCP server configuration:
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"name": "spice-mcp",
|
|
68
|
+
"command": "spice-mcp",
|
|
69
|
+
"env": {
|
|
70
|
+
"DUNE_API_KEY": "your-dune-api-key-here"
|
|
71
|
+
},
|
|
72
|
+
"disabled": false
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
Alternatively, if you prefer running from source:
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"name": "spice-mcp",
|
|
79
|
+
"command": "python",
|
|
80
|
+
"args": ["-m", "spice_mcp.mcp.server"],
|
|
81
|
+
"env": {
|
|
82
|
+
"PYTHONPATH": "/path/to/your/spice-mcp/src",
|
|
83
|
+
"DUNE_API_KEY": "your-dune-api-key-here"
|
|
84
|
+
},
|
|
85
|
+
"disabled": false
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
3. **Restart Cursor** to load the MCP server
|
|
90
|
+
|
|
91
|
+
4. **Verify Connection**:
|
|
92
|
+
- Open Cursor and use the command palette (Cmd/Ctrl + Shift + P)
|
|
93
|
+
- Search for "MCP" or "spice" commands
|
|
94
|
+
- Test with `dune_health_check` to verify the connection
|
|
95
|
+
|
|
96
|
+
5. **Available Tools in Cursor**:
|
|
97
|
+
- `dune_query`: Run Dune queries by ID, URL, or raw SQL
|
|
98
|
+
- `dune_find_tables`: Search schemas and list tables
|
|
99
|
+
- `dune_describe_table`: Get column metadata
|
|
100
|
+
- `sui_package_overview`: Analyze Sui packages
|
|
101
|
+
- `dune_health_check`: Verify API connection
|
|
102
|
+
|
|
103
|
+
**Tip**: Create a `.env` file in your project root with `DUNE_API_KEY=your-key-here` for easier configuration.
|
|
104
|
+
|
|
45
105
|
## MCP Tools and Features
|
|
46
106
|
|
|
47
107
|
All tools expose typed parameters, titles, and tags; failures return a consistent error envelope.
|
|
@@ -114,16 +174,16 @@ Core Tools (with parameters)
|
|
|
114
174
|
- Use: Verify API key presence and logging paths
|
|
115
175
|
- Output: `api_key_present`, `query_history_path`, `logging_enabled`, `status`
|
|
116
176
|
|
|
117
|
-
Docs
|
|
118
|
-
- See
|
|
119
|
-
- Dune API structure and capabilities:
|
|
120
|
-
- Discovery patterns and examples:
|
|
121
|
-
- Sui package workflows:
|
|
122
|
-
- Tool reference and schemas:
|
|
123
|
-
- Codex CLI + tooling integration:
|
|
124
|
-
- Architecture overview:
|
|
125
|
-
- Installation and configuration:
|
|
126
|
-
- Development and linting:
|
|
177
|
+
## Docs
|
|
178
|
+
- See [docs/index.md](docs/index.md) for full documentation:
|
|
179
|
+
- Dune API structure and capabilities: [docs/dune_api.md](docs/dune_api.md)
|
|
180
|
+
- Discovery patterns and examples: [docs/discovery.md](docs/discovery.md)
|
|
181
|
+
- Sui package workflows: [docs/sui_packages.md](docs/sui_packages.md)
|
|
182
|
+
- Tool reference and schemas: [docs/tools.md](docs/tools.md)
|
|
183
|
+
- Codex CLI + tooling integration: [docs/codex_cli.md](docs/codex_cli.md), [docs/codex_cli_tools.md](docs/codex_cli_tools.md)
|
|
184
|
+
- Architecture overview: [docs/architecture.md](docs/architecture.md)
|
|
185
|
+
- Installation and configuration: [docs/installation.md](docs/installation.md), [docs/config.md](docs/config.md)
|
|
186
|
+
- Development and linting: [docs/development.md](docs/development.md)
|
|
127
187
|
|
|
128
188
|
Notes
|
|
129
189
|
- Legacy Spice code now lives under `src/spice_mcp/adapters/dune` (extract, cache, urls, types).
|
|
@@ -1,26 +1,86 @@
|
|
|
1
1
|
# spice-mcp
|
|
2
2
|
|
|
3
|
-
spice-mcp is an MCP server for Dune Analytics. It wraps a curated subset of the original Spice client inside a clean architecture (`core` models/ports → `adapters.dune` → service layer → FastMCP tools) and adds agent-friendly workflows for discovery and Sui package exploration. Results are Polars-first in Python and compact, token-efficient in MCP responses.
|
|
3
|
+
spice-mcp is an MCP server for [Dune](https://dune.com/) Analytics. It wraps a curated subset of the original Spice client inside a clean architecture (`core` models/ports → `adapters.dune` → service layer → FastMCP tools) and adds agent-friendly workflows for discovery and Sui package exploration. Results are Polars-first in Python and compact, token-efficient in MCP responses.
|
|
4
4
|
|
|
5
5
|
Requirements: Python 3.13+
|
|
6
6
|
|
|
7
7
|
This project uses FastMCP for typed, decorator-registered tools and resources.
|
|
8
8
|
|
|
9
|
-
Highlights
|
|
9
|
+
## Highlights
|
|
10
10
|
- Polars LazyFrame-first pipeline: results stay lazy until explicitly materialized
|
|
11
|
-
- Ports/adapters layering for maintainable integrations (
|
|
11
|
+
- Ports/adapters layering for maintainable integrations ([docs/architecture.md](docs/architecture.md))
|
|
12
12
|
- Discovery utilities (find schemas/tables, describe columns)
|
|
13
13
|
- Sui package workflows (events/transactions/objects) with safe defaults
|
|
14
14
|
- JSONL query history + SQL artifacts (SHA-256) for reproducibility
|
|
15
15
|
- Rich MCP surface: query info/run, discovery, health, Sui, and Dune admin (create/update/fork)
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## What is Dune?
|
|
18
|
+
[Dune](https://dune.com/) is a crypto data platform providing curated blockchain datasets and a public API to run and fetch query results. See the [Dune Docs](https://dune.com/docs) and [Dune API](https://dune.com/docs/api/) for full details.
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
18
21
|
- Export `DUNE_API_KEY` in your shell (the server can also load a local `.env`; set `SPICE_MCP_SKIP_DOTENV=1` to skip during tests).
|
|
19
22
|
- Install dependencies (`uv sync` or `pip install -e .`).
|
|
20
23
|
- Start the FastMCP stdio server:
|
|
21
24
|
- `python -m spice_mcp.mcp.server --env PYTHONPATH=$(pwd)/src`
|
|
22
25
|
- or install the console script via `uv tool install .` and run `spice-mcp`.
|
|
23
26
|
|
|
27
|
+
## Cursor IDE Setup
|
|
28
|
+
|
|
29
|
+
To use spice-mcp with Cursor IDE:
|
|
30
|
+
|
|
31
|
+
1. **Install the MCP Server**:
|
|
32
|
+
```bash
|
|
33
|
+
# Install dependencies and package
|
|
34
|
+
uv sync
|
|
35
|
+
pip install -e .
|
|
36
|
+
|
|
37
|
+
# Or install via uv tool (creates console script)
|
|
38
|
+
uv tool install .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
2. **Configure Cursor**:
|
|
42
|
+
- Open Cursor Settings → MCP Servers
|
|
43
|
+
- Add new MCP server configuration:
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"name": "spice-mcp",
|
|
47
|
+
"command": "spice-mcp",
|
|
48
|
+
"env": {
|
|
49
|
+
"DUNE_API_KEY": "your-dune-api-key-here"
|
|
50
|
+
},
|
|
51
|
+
"disabled": false
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
Alternatively, if you prefer running from source:
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"name": "spice-mcp",
|
|
58
|
+
"command": "python",
|
|
59
|
+
"args": ["-m", "spice_mcp.mcp.server"],
|
|
60
|
+
"env": {
|
|
61
|
+
"PYTHONPATH": "/path/to/your/spice-mcp/src",
|
|
62
|
+
"DUNE_API_KEY": "your-dune-api-key-here"
|
|
63
|
+
},
|
|
64
|
+
"disabled": false
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
3. **Restart Cursor** to load the MCP server
|
|
69
|
+
|
|
70
|
+
4. **Verify Connection**:
|
|
71
|
+
- Open Cursor and use the command palette (Cmd/Ctrl + Shift + P)
|
|
72
|
+
- Search for "MCP" or "spice" commands
|
|
73
|
+
- Test with `dune_health_check` to verify the connection
|
|
74
|
+
|
|
75
|
+
5. **Available Tools in Cursor**:
|
|
76
|
+
- `dune_query`: Run Dune queries by ID, URL, or raw SQL
|
|
77
|
+
- `dune_find_tables`: Search schemas and list tables
|
|
78
|
+
- `dune_describe_table`: Get column metadata
|
|
79
|
+
- `sui_package_overview`: Analyze Sui packages
|
|
80
|
+
- `dune_health_check`: Verify API connection
|
|
81
|
+
|
|
82
|
+
**Tip**: Create a `.env` file in your project root with `DUNE_API_KEY=your-key-here` for easier configuration.
|
|
83
|
+
|
|
24
84
|
## MCP Tools and Features
|
|
25
85
|
|
|
26
86
|
All tools expose typed parameters, titles, and tags; failures return a consistent error envelope.
|
|
@@ -93,16 +153,16 @@ Core Tools (with parameters)
|
|
|
93
153
|
- Use: Verify API key presence and logging paths
|
|
94
154
|
- Output: `api_key_present`, `query_history_path`, `logging_enabled`, `status`
|
|
95
155
|
|
|
96
|
-
Docs
|
|
97
|
-
- See
|
|
98
|
-
- Dune API structure and capabilities:
|
|
99
|
-
- Discovery patterns and examples:
|
|
100
|
-
- Sui package workflows:
|
|
101
|
-
- Tool reference and schemas:
|
|
102
|
-
- Codex CLI + tooling integration:
|
|
103
|
-
- Architecture overview:
|
|
104
|
-
- Installation and configuration:
|
|
105
|
-
- Development and linting:
|
|
156
|
+
## Docs
|
|
157
|
+
- See [docs/index.md](docs/index.md) for full documentation:
|
|
158
|
+
- Dune API structure and capabilities: [docs/dune_api.md](docs/dune_api.md)
|
|
159
|
+
- Discovery patterns and examples: [docs/discovery.md](docs/discovery.md)
|
|
160
|
+
- Sui package workflows: [docs/sui_packages.md](docs/sui_packages.md)
|
|
161
|
+
- Tool reference and schemas: [docs/tools.md](docs/tools.md)
|
|
162
|
+
- Codex CLI + tooling integration: [docs/codex_cli.md](docs/codex_cli.md), [docs/codex_cli_tools.md](docs/codex_cli_tools.md)
|
|
163
|
+
- Architecture overview: [docs/architecture.md](docs/architecture.md)
|
|
164
|
+
- Installation and configuration: [docs/installation.md](docs/installation.md), [docs/config.md](docs/config.md)
|
|
165
|
+
- Development and linting: [docs/development.md](docs/development.md)
|
|
106
166
|
|
|
107
167
|
Notes
|
|
108
168
|
- Legacy Spice code now lives under `src/spice_mcp/adapters/dune` (extract, cache, urls, types).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "spice-mcp"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.1"
|
|
4
4
|
description = "MCP server for Dune Analytics data access"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Evan-Kim2028", email = "ekcopersonal@gmail.com" }
|
|
@@ -36,6 +36,23 @@ managed = true
|
|
|
36
36
|
dev-dependencies = [
|
|
37
37
|
"pytest>=8.0.0",
|
|
38
38
|
"pytest-asyncio>=0.23.0",
|
|
39
|
+
"pytest-cov>=5.0.0",
|
|
40
|
+
"freezegun>=1.4.0",
|
|
41
|
+
"jsonschema>=4.21.1",
|
|
42
|
+
"responses>=0.25.0",
|
|
43
|
+
"aioresponses>=0.7.6",
|
|
44
|
+
"vcrpy>=6.0.1",
|
|
45
|
+
"hypothesis>=6.100.0",
|
|
46
|
+
"ruff>=0.6.9",
|
|
47
|
+
"mypy>=1.11.2",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# uv dependency groups for CI and local dev
|
|
51
|
+
[dependency-groups]
|
|
52
|
+
dev = [
|
|
53
|
+
"pytest>=8.0.0",
|
|
54
|
+
"pytest-asyncio>=0.23.0",
|
|
55
|
+
"pytest-cov>=5.0.0",
|
|
39
56
|
"freezegun>=1.4.0",
|
|
40
57
|
"jsonschema>=4.21.1",
|
|
41
58
|
"responses>=0.25.0",
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
BASE = Path("notes/bridgez")
|
|
6
|
+
CIRCLE_DIR = BASE / "circle"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def read_single_row_metrics(path: Path):
|
|
10
|
+
with path.open() as f:
|
|
11
|
+
reader = csv.DictReader(f)
|
|
12
|
+
row = next(reader)
|
|
13
|
+
return {
|
|
14
|
+
"tx_count": float(row.get("tx_count", 0) or 0),
|
|
15
|
+
"total_usd": float(row.get("total_usd", 0) or 0),
|
|
16
|
+
"avg_usd": float(row.get("avg_usd", 0) or 0),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def read_circle_totals(path: Path):
|
|
21
|
+
rows = []
|
|
22
|
+
with path.open() as f:
|
|
23
|
+
reader = csv.DictReader(f)
|
|
24
|
+
for r in reader:
|
|
25
|
+
rows.append(
|
|
26
|
+
{
|
|
27
|
+
"schema": r["schema"],
|
|
28
|
+
"usd_out_30d": float(r["usd_out_30d"]) if r["usd_out_30d"] else 0.0,
|
|
29
|
+
"usd_out_ytd": float(r["usd_out_ytd"]) if r["usd_out_ytd"] else 0.0,
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
return rows
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def write_text(path: Path, content: str):
|
|
36
|
+
path.write_text(content)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def update_readme(readme_path: Path, section: str):
|
|
40
|
+
# Append or replace a Circle vs LayerZero section
|
|
41
|
+
marker_start = "\n## Circle vs LayerZero (Summary)\n"
|
|
42
|
+
content = readme_path.read_text() if readme_path.exists() else ""
|
|
43
|
+
if "## Circle vs LayerZero (Summary)" in content:
|
|
44
|
+
# replace from marker to end or next header
|
|
45
|
+
parts = content.split("## Circle vs LayerZero (Summary)")
|
|
46
|
+
head = parts[0]
|
|
47
|
+
tail = "## Circle vs LayerZero (Summary)" + parts[1]
|
|
48
|
+
# cut tail at the next header if present
|
|
49
|
+
tail_lines = tail.split("\n## ")
|
|
50
|
+
new_tail = "## Circle vs LayerZero (Summary)\n" + section
|
|
51
|
+
if len(tail_lines) > 1:
|
|
52
|
+
# keep next headers
|
|
53
|
+
new_content = head + new_tail + "\n## " + "\n## ".join(tail_lines[1:])
|
|
54
|
+
else:
|
|
55
|
+
new_content = head + new_tail
|
|
56
|
+
readme_path.write_text(new_content)
|
|
57
|
+
else:
|
|
58
|
+
readme_path.write_text(content.rstrip() + marker_start + section)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def fmt_usd(x: float) -> str:
|
|
62
|
+
return f"${x:,.2f}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main():
|
|
66
|
+
# LayerZero metrics
|
|
67
|
+
lz_30d = read_single_row_metrics(BASE / "metrics_last_30d.csv")
|
|
68
|
+
lz_ytd = read_single_row_metrics(BASE / "metrics_ytd.csv")
|
|
69
|
+
|
|
70
|
+
# Circle per-schema summary
|
|
71
|
+
circle_rows = read_circle_totals(CIRCLE_DIR / "flows_outbound_summary.csv")
|
|
72
|
+
c_30d_total = sum(r["usd_out_30d"] for r in circle_rows)
|
|
73
|
+
c_ytd_total = sum(r["usd_out_ytd"] for r in circle_rows)
|
|
74
|
+
|
|
75
|
+
# Build comparison text
|
|
76
|
+
pct_30d = (c_30d_total / lz_30d["total_usd"] * 100.0) if lz_30d["total_usd"] else 0.0
|
|
77
|
+
pct_ytd = (c_ytd_total / lz_ytd["total_usd"] * 100.0) if lz_ytd["total_usd"] else 0.0
|
|
78
|
+
|
|
79
|
+
top_circle_30d = sorted(circle_rows, key=lambda r: r["usd_out_30d"], reverse=True)[:5]
|
|
80
|
+
top_circle_ytd = sorted(circle_rows, key=lambda r: r["usd_out_ytd"], reverse=True)[:5]
|
|
81
|
+
|
|
82
|
+
lines = []
|
|
83
|
+
lines.append("Circle vs LayerZero (USD volumes)\n")
|
|
84
|
+
lines.append(f"- LayerZero — 30d: {fmt_usd(lz_30d['total_usd'])}; YTD: {fmt_usd(lz_ytd['total_usd'])}")
|
|
85
|
+
lines.append(f"- Circle (CCTP outbound) — 30d: {fmt_usd(c_30d_total)} ({pct_30d:.1f}% of LZ); YTD: {fmt_usd(c_ytd_total)} ({pct_ytd:.1f}% of LZ)\n")
|
|
86
|
+
lines.append("Top Circle schemas (30d):")
|
|
87
|
+
for r in top_circle_30d:
|
|
88
|
+
lines.append(f"- {r['schema']}: {fmt_usd(r['usd_out_30d'])}")
|
|
89
|
+
lines.append("")
|
|
90
|
+
lines.append("Top Circle schemas (YTD):")
|
|
91
|
+
for r in top_circle_ytd:
|
|
92
|
+
lines.append(f"- {r['schema']}: {fmt_usd(r['usd_out_ytd'])}")
|
|
93
|
+
lines.append("")
|
|
94
|
+
lines.append("Note: circle_bnb and circle_zksync may be missing if their tables/columns differ; totals reflect included schemas only.")
|
|
95
|
+
|
|
96
|
+
text = "\n".join(lines) + "\n"
|
|
97
|
+
|
|
98
|
+
# Write dedicated comparison under circle
|
|
99
|
+
write_text(CIRCLE_DIR / "COMPARISON.txt", text)
|
|
100
|
+
|
|
101
|
+
# Update top-level README with a concise section
|
|
102
|
+
section = (
|
|
103
|
+
f"\n- LayerZero — 30d: {fmt_usd(lz_30d['total_usd'])}; YTD: {fmt_usd(lz_ytd['total_usd'])}\n"
|
|
104
|
+
f"- Circle (CCTP outbound) — 30d: {fmt_usd(c_30d_total)} ({pct_30d:.1f}% of LZ); YTD: {fmt_usd(c_ytd_total)} ({pct_ytd:.1f}% of LZ)\n"
|
|
105
|
+
"- Top Circle schemas (30d): "
|
|
106
|
+
+ ", ".join([f"{r['schema']} {fmt_usd(r['usd_out_30d'])}" for r in top_circle_30d])
|
|
107
|
+
+ "\n"
|
|
108
|
+
)
|
|
109
|
+
update_readme(BASE / "README.md", section)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
main()
|
|
114
|
+
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import io
|
|
4
4
|
import time
|
|
5
5
|
from typing import TYPE_CHECKING, overload
|
|
6
|
+
import os
|
|
6
7
|
|
|
7
8
|
from ..http_client import HttpClient
|
|
8
9
|
from . import cache as _cache
|
|
@@ -32,6 +33,10 @@ from .typing_utils import resolve_raw_sql_template_id
|
|
|
32
33
|
|
|
33
34
|
ADAPTER_VERSION = "spice-mcp-adapter"
|
|
34
35
|
|
|
36
|
+
# Runtime-configurable HTTP timeouts (helps avoid host-level timeouts)
|
|
37
|
+
_GET_TIMEOUT: float = float(os.getenv("SPICE_DUNE_GET_TIMEOUT", os.getenv("SPICE_HTTP_TIMEOUT", "30")))
|
|
38
|
+
_POST_TIMEOUT: float = float(os.getenv("SPICE_DUNE_POST_TIMEOUT", os.getenv("SPICE_HTTP_TIMEOUT", "30")))
|
|
39
|
+
|
|
35
40
|
if TYPE_CHECKING:
|
|
36
41
|
from collections.abc import Mapping, Sequence
|
|
37
42
|
from typing import Any, Literal
|
|
@@ -749,7 +754,7 @@ def _execute(
|
|
|
749
754
|
print('executing query, query_id = ' + str(query_id))
|
|
750
755
|
|
|
751
756
|
# perform request
|
|
752
|
-
response = _transport_post(url, headers=headers, json=data, timeout=
|
|
757
|
+
response = _transport_post(url, headers=headers, json=data, timeout=_POST_TIMEOUT)
|
|
753
758
|
result: Mapping[str, Any] = response.json()
|
|
754
759
|
|
|
755
760
|
# check for errors
|
|
@@ -786,7 +791,7 @@ async def _async_execute(
|
|
|
786
791
|
print('executing query, query_id = ' + str(query_id))
|
|
787
792
|
|
|
788
793
|
# perform request
|
|
789
|
-
timeout = aiohttp.ClientTimeout(total=
|
|
794
|
+
timeout = aiohttp.ClientTimeout(total=_POST_TIMEOUT)
|
|
790
795
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
791
796
|
async with session.post(url, headers=headers, json=data) as response:
|
|
792
797
|
result: Mapping[str, Any] = await response.json()
|
|
@@ -862,12 +867,12 @@ def _get_results(
|
|
|
862
867
|
def _get_with_retries(u: str):
|
|
863
868
|
client = current_http_client()
|
|
864
869
|
if client is not None:
|
|
865
|
-
return client.request("GET", u, headers=headers, timeout=
|
|
870
|
+
return client.request("GET", u, headers=headers, timeout=_GET_TIMEOUT)
|
|
866
871
|
|
|
867
872
|
attempts = 0
|
|
868
873
|
backoff = 0.5
|
|
869
874
|
while True:
|
|
870
|
-
resp = _transport_get(u, headers=headers, timeout=
|
|
875
|
+
resp = _transport_get(u, headers=headers, timeout=_GET_TIMEOUT)
|
|
871
876
|
if resp.status_code in (429, 502):
|
|
872
877
|
attempts += 1
|
|
873
878
|
if attempts >= 3:
|
|
@@ -963,7 +968,7 @@ async def _async_get_results(
|
|
|
963
968
|
print('getting results, execution_id = ' + str(execution['execution_id']))
|
|
964
969
|
|
|
965
970
|
# perform request
|
|
966
|
-
timeout = aiohttp.ClientTimeout(total=
|
|
971
|
+
timeout = aiohttp.ClientTimeout(total=_GET_TIMEOUT)
|
|
967
972
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
968
973
|
# GET with simple retry/backoff for 429/502
|
|
969
974
|
attempts = 0
|
|
@@ -1169,7 +1174,7 @@ def _poll_execution(
|
|
|
1169
1174
|
)
|
|
1170
1175
|
|
|
1171
1176
|
# poll
|
|
1172
|
-
response = _http_get(url, headers=headers, timeout=
|
|
1177
|
+
response = _http_get(url, headers=headers, timeout=_GET_TIMEOUT)
|
|
1173
1178
|
result = response.json()
|
|
1174
1179
|
if (
|
|
1175
1180
|
'is_execution_finished' not in result
|
|
@@ -1243,7 +1248,7 @@ async def _async_poll_execution(
|
|
|
1243
1248
|
t_start = time.time()
|
|
1244
1249
|
|
|
1245
1250
|
# poll until completion
|
|
1246
|
-
timeout = aiohttp.ClientTimeout(total=
|
|
1251
|
+
timeout = aiohttp.ClientTimeout(total=_GET_TIMEOUT)
|
|
1247
1252
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
1248
1253
|
sleep_amount = poll_interval
|
|
1249
1254
|
while True:
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import os
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
def resolve_raw_sql_template_id() -> int:
|
|
5
7
|
"""Return a stable template ID used for executing raw SQL text.
|
|
@@ -7,4 +9,4 @@ def resolve_raw_sql_template_id() -> int:
|
|
|
7
9
|
Tests stub HTTP boundaries and only require a consistent integer. This
|
|
8
10
|
placeholder can be adjusted if upstream semantics change.
|
|
9
11
|
"""
|
|
10
|
-
return
|
|
12
|
+
return int(os.getenv("SPICE_RAW_SQL_QUERY_ID", "4060379"))
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Tests for typing utilities, especially environment variable handling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
from spice_mcp.adapters.dune.typing_utils import resolve_raw_sql_template_id
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_resolve_raw_sql_template_id_default():
|
|
12
|
+
"""Test that the function returns the default template ID when no env var is set."""
|
|
13
|
+
# Ensure env var is not set
|
|
14
|
+
with patch.dict(os.environ, {}, clear=True):
|
|
15
|
+
result = resolve_raw_sql_template_id()
|
|
16
|
+
assert result == 4060379
|
|
17
|
+
assert isinstance(result, int)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_resolve_raw_sql_template_id_from_env():
|
|
21
|
+
"""Test that the function reads from the SPICE_RAW_SQL_QUERY_ID environment variable."""
|
|
22
|
+
custom_id = "12345"
|
|
23
|
+
with patch.dict(os.environ, {"SPICE_RAW_SQL_QUERY_ID": custom_id}):
|
|
24
|
+
result = resolve_raw_sql_template_id()
|
|
25
|
+
assert result == int(custom_id)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_resolve_raw_sql_template_id_invalid_env():
|
|
29
|
+
"""Test that the function handles invalid environment variable values gracefully."""
|
|
30
|
+
with patch.dict(os.environ, {"SPICE_RAW_SQL_QUERY_ID": "invalid_number"}):
|
|
31
|
+
# This should raise a ValueError when trying to convert to int
|
|
32
|
+
try:
|
|
33
|
+
resolve_raw_sql_template_id()
|
|
34
|
+
assert False, "Expected ValueError for invalid number"
|
|
35
|
+
except ValueError:
|
|
36
|
+
pass # Expected
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_resolve_raw_sql_template_id_zero_env():
|
|
40
|
+
"""Test that the function handles zero value correctly."""
|
|
41
|
+
with patch.dict(os.environ, {"SPICE_RAW_SQL_QUERY_ID": "0"}):
|
|
42
|
+
result = resolve_raw_sql_template_id()
|
|
43
|
+
assert result == 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_determine_input_type_raw_sql_uses_template_id(monkeypatch):
|
|
47
|
+
"""Test that determine_input_type correctly resolves raw SQL using the template ID."""
|
|
48
|
+
from spice_mcp.adapters.dune.extract import determine_input_type
|
|
49
|
+
|
|
50
|
+
# Test with default template ID
|
|
51
|
+
with patch.dict(os.environ, {}, clear=True):
|
|
52
|
+
query_id, execution, parameters = determine_input_type("SELECT 1 as test")
|
|
53
|
+
assert query_id == 4060379
|
|
54
|
+
assert execution is None
|
|
55
|
+
assert parameters == {"query": "SELECT 1 as test"}
|
|
56
|
+
|
|
57
|
+
# Test with custom template ID
|
|
58
|
+
with patch.dict(os.environ, {"SPICE_RAW_SQL_QUERY_ID": "12345"}):
|
|
59
|
+
query_id, execution, parameters = determine_input_type("SELECT count(*) as total")
|
|
60
|
+
assert query_id == 12345
|
|
61
|
+
assert execution is None
|
|
62
|
+
assert parameters == {"query": "SELECT count(*) as total"}
|