spice-mcp 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl

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.

@@ -20,7 +20,7 @@ class MCPTool(ABC):
20
20
  raise NotImplementedError
21
21
 
22
22
  @abstractmethod
23
- async def execute(self, **kwargs) -> dict[str, Any]:
23
+ def execute(self, **kwargs) -> dict[str, Any]:
24
24
  """Execute tool logic and return result dictionary."""
25
25
  raise NotImplementedError
26
26
 
@@ -5,8 +5,10 @@ import re
5
5
  import time
6
6
  from typing import Any
7
7
 
8
- from ...adapters.dune import extract as dune_extract
9
8
  from ...adapters.dune import urls as dune_urls
9
+ from ...adapters.dune.query_wrapper import execute_query as execute_dune_query
10
+ # Import user_agent from separate module to avoid importing overloaded functions
11
+ from ...adapters.dune.user_agent import get_user_agent as get_dune_user_agent
10
12
  from ...adapters.http_client import HttpClient
11
13
  from ...config import Config
12
14
  from ...core.errors import error_response
@@ -80,7 +82,7 @@ class ExecuteQueryTool(MCPTool):
80
82
  "additionalProperties": False,
81
83
  }
82
84
 
83
- async def execute(
85
+ def execute(
84
86
  self,
85
87
  *,
86
88
  query: str,
@@ -103,7 +105,7 @@ class ExecuteQueryTool(MCPTool):
103
105
  q_use = _maybe_rewrite_show_sql(query) or query
104
106
  # Poll-only: return execution handle without fetching results
105
107
  if format == "poll":
106
- exec_obj = dune_extract.query(
108
+ exec_obj = execute_dune_query(
107
109
  q_use,
108
110
  parameters=parameters,
109
111
  api_key=self.config.dune.api_key,
@@ -149,74 +151,41 @@ class ExecuteQueryTool(MCPTool):
149
151
  return {"type": "execution", "execution_id": execution_id, "status": "submitted"}
150
152
 
151
153
  if format == "metadata":
152
- try:
153
- meta = self.query_service.fetch_metadata(
154
- query=q_use,
155
- parameters=parameters,
156
- max_age=max_age,
157
- limit=limit,
158
- offset=offset,
159
- sample_count=sample_count,
160
- sort_by=sort_by,
161
- columns=columns,
162
- extras=extras,
163
- performance=performance,
164
- )
165
- except TypeError:
166
- # Back-compat: older services without 'extras'
167
- meta = self.query_service.fetch_metadata(
168
- query=q_use,
169
- parameters=parameters,
170
- max_age=max_age,
171
- limit=limit,
172
- offset=offset,
173
- sample_count=sample_count,
174
- sort_by=sort_by,
175
- columns=columns,
176
- performance=performance,
177
- )
178
- return {
179
- "type": "metadata",
180
- "metadata": meta.get("metadata"),
181
- "next_uri": meta.get("next_uri"),
182
- "next_offset": meta.get("next_offset"),
183
- }
184
- try:
185
- result = self.query_service.execute(
154
+ meta = self.query_service.fetch_metadata(
186
155
  query=q_use,
187
156
  parameters=parameters,
188
- refresh=refresh,
189
157
  max_age=max_age,
190
- poll=True,
191
- timeout_seconds=timeout_seconds,
192
158
  limit=limit,
193
159
  offset=offset,
194
160
  sample_count=sample_count,
195
161
  sort_by=sort_by,
196
162
  columns=columns,
197
163
  extras=extras,
198
- include_execution=True,
199
164
  performance=performance,
200
- return_raw=format == "raw",
201
- )
202
- except TypeError:
203
- # Back-compat: older services without 'extras'
204
- result = self.query_service.execute(
205
- query=q_use,
206
- parameters=parameters,
207
- refresh=refresh,
208
- max_age=max_age,
209
- poll=True,
210
- timeout_seconds=timeout_seconds,
211
- limit=limit,
212
- offset=offset,
213
- sample_count=sample_count,
214
- sort_by=sort_by,
215
- columns=columns,
216
- include_execution=True,
217
- performance=performance,
218
- return_raw=format == "raw",
219
165
  )
166
+ return {
167
+ "type": "metadata",
168
+ "metadata": meta.get("metadata"),
169
+ "next_uri": meta.get("next_uri"),
170
+ "next_offset": meta.get("next_offset"),
171
+ }
172
+ result = self.query_service.execute(
173
+ query=q_use,
174
+ parameters=parameters,
175
+ refresh=refresh,
176
+ max_age=max_age,
177
+ poll=True,
178
+ timeout_seconds=timeout_seconds,
179
+ limit=limit,
180
+ offset=offset,
181
+ sample_count=sample_count,
182
+ sort_by=sort_by,
183
+ columns=columns,
184
+ extras=extras,
185
+ include_execution=True,
186
+ performance=performance,
187
+ return_raw=format == "raw",
188
+ )
220
189
 
221
190
  duration_ms = int((time.time() - t0) * 1000)
222
191
  execution = result.get("execution", {})
@@ -340,7 +309,7 @@ class ExecuteQueryTool(MCPTool):
340
309
  # Add debugging information for raw SQL failures
341
310
  if q_type == "raw_sql" and "could not determine execution" in str(exc):
342
311
  context.update({
343
- "debug_info": "Raw SQL execution failed - this may be related to FastMCP async/concurrency handling",
312
+ "debug_info": "Raw SQL execution failed - check template query configuration and API key",
344
313
  "template_query_id": template_id_value,
345
314
  "environment_vars": {
346
315
  "SPICE_RAW_SQL_QUERY_ID": os.getenv("SPICE_RAW_SQL_QUERY_ID"),
@@ -366,7 +335,7 @@ class ExecuteQueryTool(MCPTool):
366
335
  query_id = dune_urls.get_query_id(query)
367
336
  headers = {
368
337
  "X-Dune-API-Key": dune_urls.get_api_key(),
369
- "User-Agent": dune_extract.get_user_agent(),
338
+ "User-Agent": get_dune_user_agent(),
370
339
  }
371
340
  resp = self._http.request(
372
341
  "GET",
@@ -395,7 +364,7 @@ class ExecuteQueryTool(MCPTool):
395
364
  url = dune_urls.get_execution_status_url(execution_id)
396
365
  headers = {
397
366
  "X-Dune-API-Key": dune_urls.get_api_key(),
398
- "User-Agent": dune_extract.get_user_agent(),
367
+ "User-Agent": get_dune_user_agent(),
399
368
  }
400
369
  resp = self._http.request("GET", url, headers=headers, timeout=10.0)
401
370
  data = resp.json()
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: spice-mcp
3
+ Version: 0.1.4
4
+ Summary: MCP server for Dune Analytics data access
5
+ Author-email: Evan-Kim2028 <ekcopersonal@gmail.com>
6
+ License-File: LICENSE
7
+ Classifier: Operating System :: OS Independent
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Typing :: Typed
12
+ Requires-Python: >=3.13
13
+ Requires-Dist: aiohttp>=3.9.5
14
+ Requires-Dist: fastmcp>=0.3.0
15
+ Requires-Dist: mcp>=0.9.0
16
+ Requires-Dist: polars>=1.35.1
17
+ Requires-Dist: requests>=2.31.0
18
+ Requires-Dist: rich-argparse>=1.5.2
19
+ Requires-Dist: rich>=13.3.3
20
+ Description-Content-Type: text/markdown
21
+
22
+ # spice-mcp
23
+
24
+ [![PyPI version](https://img.shields.io/pypi/v/spice-mcp.svg)](https://pypi.org/project/spice-mcp/)
25
+ <a href="https://glama.ai/mcp/servers/@Evan-Kim2028/spice-mcp">
26
+ <img width="380" height="200" src="https://glama.ai/mcp/servers/@Evan-Kim2028/spice-mcp/badge" alt="Spice MCP server" />
27
+ </a>
28
+
29
+ An MCP server that provides AI agents with direct access to [Dune Analytics](https://dune.com/) data. Execute queries, discover schemas and tables, and manage saved queries—all through a clean, type-safe interface optimized for AI workflows.
30
+
31
+ ## Why spice-mcp?
32
+
33
+ - **Agent-friendly**: Designed for AI agents using the Model Context Protocol (MCP)
34
+ - **Efficient**: Polars-first pipeline keeps data lazy until needed, reducing memory usage
35
+ - **Discovery**: Built-in tools to explore Dune's extensive blockchain datasets
36
+ - **Type-safe**: Fully typed parameters and responses with FastMCP
37
+ - **Reproducible**: Automatic query history logging and SQL artifact storage
38
+
39
+ ## Quick Start
40
+
41
+ 1. **Install**:
42
+ ```bash
43
+ uv pip install spice-mcp
44
+ ```
45
+
46
+ 2. **Set API key** (choose one method):
47
+ - **Option A**: Create a `.env` file in your project root:
48
+ ```bash
49
+ echo "DUNE_API_KEY=your-api-key-here" > .env
50
+ ```
51
+ - **Option B**: Export in your shell:
52
+ ```bash
53
+ export DUNE_API_KEY=your-api-key-here
54
+ ```
55
+
56
+ 3. **Use with Cursor IDE**:
57
+ Add to Cursor Settings → MCP Servers:
58
+ ```json
59
+ {
60
+ "name": "spice-mcp",
61
+ "command": "spice-mcp",
62
+ "env": {
63
+ "DUNE_API_KEY": "your-dune-api-key-here"
64
+ }
65
+ }
66
+ ```
67
+
68
+ **Note**: Query history logging is enabled by default. Logs are saved to `logs/queries.jsonl` (or `~/.spice_mcp/logs/queries.jsonl` if not in a project directory). To customize paths, set `SPICE_QUERY_HISTORY` and `SPICE_ARTIFACT_ROOT` environment variables.
69
+
70
+ ## Core Tools
71
+
72
+ | Tool | Description | Key Parameters |
73
+ |------|-------------|----------------|
74
+ | `dune_query` | Execute queries by ID, URL, or raw SQL | `query` (str), `parameters` (object), `limit` (int), `offset` (int), `format` (`preview\|raw\|metadata\|poll`), `refresh` (bool), `timeout_seconds` (float) |
75
+ | `dune_query_info` | Get metadata for a saved query | `query` (str - ID or URL) |
76
+ | `dune_discover` | Unified discovery across Dune API and Spellbook | `keyword` (str\|list), `schema` (str), `limit` (int), `source` (`dune\|spellbook\|both`), `include_columns` (bool) |
77
+ | `dune_find_tables` | Search schemas and list tables | `keyword` (str), `schema` (str), `limit` (int) |
78
+ | `dune_describe_table` | Get column metadata for a table | `schema` (str), `table` (str) |
79
+ | `spellbook_find_models` | Search Spellbook dbt models | `keyword` (str\|list), `schema` (str), `limit` (int), `include_columns` (bool) |
80
+ | `dune_health_check` | Verify API key and configuration | (no parameters) |
81
+ | `dune_query_create` | Create a new saved query | `name` (str), `query_sql` (str), `description` (str), `tags` (list), `parameters` (list) |
82
+ | `dune_query_update` | Update an existing saved query | `query_id` (int), `name` (str), `query_sql` (str), `description` (str), `tags` (list), `parameters` (list) |
83
+ | `dune_query_fork` | Fork an existing saved query | `source_query_id` (int), `name` (str) |
84
+
85
+ ## Resources
86
+
87
+ - `spice:history/tail/{n}` — View last N lines of query history (1-1000)
88
+ - `spice:artifact/{sha}` — Retrieve stored SQL by SHA-256 hash
89
+
90
+ ## What is Dune?
91
+
92
+ [Dune](https://dune.com/) is a crypto data platform providing curated blockchain datasets and a public API. It aggregates on-chain data from Ethereum, Solana, Polygon, and other chains into queryable SQL tables. See the [Dune Docs](https://dune.com/docs) for more information.
93
+
94
+ ## Installation
95
+
96
+ **From PyPI** (recommended):
97
+ ```bash
98
+ uv pip install spice-mcp
99
+ ```
100
+
101
+ **From source**:
102
+ ```bash
103
+ git clone https://github.com/Evan-Kim2028/spice-mcp.git
104
+ cd spice-mcp
105
+ uv sync
106
+ uv pip install -e .
107
+ ```
108
+
109
+ **Requirements**: Python 3.13+
110
+
111
+ ## Documentation
112
+
113
+ - [Tool Reference](docs/tools.md) — Complete tool documentation with parameters
114
+ - [Architecture](docs/architecture.md) — Code structure and design patterns
115
+ - [Discovery Guide](docs/discovery.md) — How to explore Dune schemas and tables
116
+ - [Dune API Guide](docs/dune_api.md) — Understanding Dune's data structure
117
+ - [Configuration](docs/config.md) — Environment variables and settings
118
+
119
+ ## License
120
+
121
+ See [LICENSE](LICENSE) file for details.
@@ -1,39 +1,41 @@
1
1
  spice_mcp/__init__.py,sha256=SpDcj8yU1fpT2YZCqal1R3KLZpBty3w--6bkmrmWU6o,55
2
- spice_mcp/config.py,sha256=edQvaksAPhOcOSvT-_KUYKvnWyGAwxbSFMO7dvfsGU0,2712
2
+ spice_mcp/config.py,sha256=QX-c3veCFEoKjXIHsEX1_Kt4kjrd7tmrdTiOLFS5pSw,2766
3
3
  spice_mcp/polars_utils.py,sha256=hCw3M_iZhTFdBnfmcx2SN8J8jpoucfJnpX_ZrXz6JwY,442
4
4
  spice_mcp/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
5
5
  spice_mcp/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  spice_mcp/adapters/http_client.py,sha256=CYgSKAsx-5c-uxaNIBCBTgQdaoBe5J3dJvnw8iqda34,5033
7
- spice_mcp/adapters/dune/__init__.py,sha256=8HRHPxkz0y2cyHSs4oCkHq_LQaUW5MBevbeNxM9RZvo,372
7
+ spice_mcp/adapters/dune/__init__.py,sha256=nspEuDpVOktAxm8B066s-d0LwouCYGpvNEexi0mRMN8,386
8
8
  spice_mcp/adapters/dune/admin.py,sha256=yxOueVz-rmgC-ZFbT06k59G24yRgYjiEkZlall5hXNQ,3157
9
- spice_mcp/adapters/dune/cache.py,sha256=7UmmxpBNeSGPH3KYBBSPHlVXCpiZUi4-X300U2FsSBs,5252
10
- spice_mcp/adapters/dune/client.py,sha256=JPrQZ_dtaPcGf6lYUguDxGweOtxG-8qMqOiXuhWL9QA,9122
11
- spice_mcp/adapters/dune/extract.py,sha256=frIFRUNJ99UlP26N9F-p1psX-e_TVq9vJTQxh0MBCr8,48510
9
+ spice_mcp/adapters/dune/cache.py,sha256=7ykT58WN1yHGIN2uV3t7fWOqGb1VJdCvf3I-xZwsv74,4304
10
+ spice_mcp/adapters/dune/client.py,sha256=4-Ay2FQf_vo-eB6I9Kul3f1PgS78PPuUJ7zldebOtKU,9424
11
+ spice_mcp/adapters/dune/extract.py,sha256=30D-5NyiOXDtMZoo1dU9pf5yAAFR_ALn6JWvhipPRG0,30405
12
12
  spice_mcp/adapters/dune/helpers.py,sha256=BgDKr_g-UqmU2hoMb0ejQZHta_NbKwR1eDJp33sJYNk,227
13
+ spice_mcp/adapters/dune/query_wrapper.py,sha256=Km64otc00u9ieUhpZmL2aNYb9ETt6PoNb8czShIuPbY,2925
13
14
  spice_mcp/adapters/dune/transport.py,sha256=eRP-jPY2ZXxvTX9HSjIFqFUlbIzXspgH95jBFoTlpaQ,1436
14
15
  spice_mcp/adapters/dune/types.py,sha256=57TMX07u-Gq4BYwRAuZV0xI81nVXgtpp7KBID9YbKyQ,1195
15
16
  spice_mcp/adapters/dune/typing_utils.py,sha256=EpWneGDn-eQdo6lkLuESR09KXkDj9OqGz8bEF3JaFkM,574
16
17
  spice_mcp/adapters/dune/urls.py,sha256=bcuPERkFQduRTT2BrgzVhoFrMn-Lkvw9NmktcBZYEig,3902
18
+ spice_mcp/adapters/dune/user_agent.py,sha256=c6Kt4zczbuT9mapDoh8-3sgm268MUtvyIRxDF9yJwXQ,218
19
+ spice_mcp/adapters/spellbook/__init__.py,sha256=D2cdVtSUbmAJdbPRvAyKxYS4-wUQ3unXyX4ZFYxenuk,150
20
+ spice_mcp/adapters/spellbook/explorer.py,sha256=Q3UfEGlALizCDeeW_ZZVRRedzwMXiknmxwSkeOnxxgc,11515
17
21
  spice_mcp/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
22
  spice_mcp/core/errors.py,sha256=jlfTuyRaAaA_oU07KUk-1pDAAa43KG0BbZc5CINXtoE,3256
19
- spice_mcp/core/models.py,sha256=2AgAPcaDPEXLdka1E6-0cXN_QNsN2rnDoBnDTDDsO6I,2121
20
- spice_mcp/core/ports.py,sha256=-wtWjpYSjL-qugUtsm0MTngtJZWzr7QNDbAUCj4BTS0,2028
23
+ spice_mcp/core/models.py,sha256=i0C_-UE16OWyyZo_liooEJeYvbChE5lpK80aN2OF4lk,1795
24
+ spice_mcp/core/ports.py,sha256=nEdeA3UH7v0kB_hbguMrpDljb9EhSxUAO0SdhjpoijQ,1618
21
25
  spice_mcp/logging/query_history.py,sha256=doE9lod64uzJxlA2XzHH2-VAmC6WstYAkQ0taEAxiIM,4315
22
26
  spice_mcp/mcp/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
23
- spice_mcp/mcp/server.py,sha256=RtvK0u9Q3dp3JMALrVFL43Li1lKdaQwLvtZuoo0pkbY,18082
27
+ spice_mcp/mcp/server.py,sha256=lHYEI76oQpPcpPT9pEoGXpUmAjDFXlOXuUrlOWV8s2c,28729
24
28
  spice_mcp/mcp/tools/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
25
- spice_mcp/mcp/tools/base.py,sha256=Xw8k5WAXotCdZRd-cVJIFPH87JH8DH0sh1zKu-KmW3w,1047
26
- spice_mcp/mcp/tools/execute_query.py,sha256=y2HU3Cv6T7QBXBDMea0TlXcf8_ERkq2sJeaexR6kBXE,17556
27
- spice_mcp/mcp/tools/sui_package_overview.py,sha256=a1CcpfBzEFzScjXgxLu9urxdtSbXjF98ugWPOX167rc,1736
29
+ spice_mcp/mcp/tools/base.py,sha256=zJkVxLgXR48iZcJeng8cZ2rXvbyicagoGlMN7BK7Img,1041
30
+ spice_mcp/mcp/tools/execute_query.py,sha256=CJtoNKpRY6pCkyqjwFy_cTYqTIg9-EsZA8GrH-2MFjk,16262
28
31
  spice_mcp/observability/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
32
  spice_mcp/observability/logging.py,sha256=ceJUEpKGpf5PAgPBmpB49zjqhdGCAESfLemFUhDSmI8,529
30
33
  spice_mcp/service_layer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
34
  spice_mcp/service_layer/discovery_service.py,sha256=202O0SzCZGQukd9kb2JYfarLygZHgiXlHqp_nTAdrWA,730
32
35
  spice_mcp/service_layer/query_admin_service.py,sha256=4q1NAAuTui7cm83Aq2rFDLIzKTHX17yzbSoSJyCmLbI,1356
33
36
  spice_mcp/service_layer/query_service.py,sha256=q0eAVW5I3sUxm29DgzPN_cH3rZEzmKwmdE3Xj4qP9lI,3878
34
- spice_mcp/service_layer/sui_service.py,sha256=G-LOsl-z-ldMxAYAetnG17IQlpVtLchGUjN6opU-PF0,4712
35
- spice_mcp-0.1.2.dist-info/METADATA,sha256=TB63rlJQOinwNKj3XvevK0OLLF2Ic0xYmeLU_kaiuas,9316
36
- spice_mcp-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
37
- spice_mcp-0.1.2.dist-info/entry_points.txt,sha256=4XiXX13Vy-oiUJwlcO_82OltBaxFnEnkJ-76sZGm5os,56
38
- spice_mcp-0.1.2.dist-info/licenses/LICENSE,sha256=r0GNDnDY1RSkVQp7kEEf6MQU21OrNGJkxUHIsv6eyLk,1079
39
- spice_mcp-0.1.2.dist-info/RECORD,,
37
+ spice_mcp-0.1.4.dist-info/METADATA,sha256=MyGS87Cwkx0G5urib8a2JbqZVP4stYdGeVknRxzPw5A,5053
38
+ spice_mcp-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
39
+ spice_mcp-0.1.4.dist-info/entry_points.txt,sha256=4XiXX13Vy-oiUJwlcO_82OltBaxFnEnkJ-76sZGm5os,56
40
+ spice_mcp-0.1.4.dist-info/licenses/LICENSE,sha256=r0GNDnDY1RSkVQp7kEEf6MQU21OrNGJkxUHIsv6eyLk,1079
41
+ spice_mcp-0.1.4.dist-info/RECORD,,
@@ -1,56 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
-
5
- from ...core.errors import error_response
6
- from ...service_layer.sui_service import SuiService
7
- from .base import MCPTool
8
-
9
-
10
- class SuiPackageOverviewTool(MCPTool):
11
- """Overview of Sui packages: events, transactions, objects (small preview)."""
12
-
13
- def __init__(self, sui_service: SuiService):
14
- self.sui_service = sui_service
15
-
16
- @property
17
- def name(self) -> str:
18
- return "sui_package_overview"
19
-
20
- @property
21
- def description(self) -> str:
22
- return (
23
- "Return a compact overview (counts + previews) of Sui package activity "
24
- "over a time window, with polling timeouts."
25
- )
26
-
27
- def get_parameter_schema(self) -> dict[str, Any]:
28
- return {
29
- "type": "object",
30
- "properties": {
31
- "packages": {"type": "array", "items": {"type": "string"}},
32
- "hours": {"type": "integer", "default": 72},
33
- "timeout_seconds": {"type": "number", "default": 30},
34
- },
35
- "required": ["packages"],
36
- "additionalProperties": False,
37
- }
38
-
39
- async def execute(
40
- self, *, packages: list[str], hours: int = 72, timeout_seconds: float | None = 30
41
- ) -> dict[str, Any]:
42
- try:
43
- return self.sui_service.package_overview(
44
- packages,
45
- hours=hours,
46
- timeout_seconds=timeout_seconds,
47
- )
48
- except Exception as exc:
49
- return error_response(
50
- exc,
51
- context={
52
- "tool": "sui_package_overview",
53
- "packages": packages,
54
- "hours": hours,
55
- },
56
- )
@@ -1,131 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import time
4
-
5
- from .query_service import QueryService
6
-
7
-
8
- class SuiService:
9
- """Opinionated helpers for Sui package exploration."""
10
-
11
- def __init__(self, query_service: QueryService):
12
- self.query_service = query_service
13
-
14
- def events_preview(
15
- self, packages: list[str], *, hours: int = 72, limit: int = 50, performance: str = "large"
16
- ) -> dict[str, object]:
17
- now_ms = int(time.time() * 1000)
18
- start_ms = now_ms - hours * 3600 * 1000
19
-
20
- norms = _normalize_packages(packages)
21
- if norms:
22
- preds = [f"lower(event_type) LIKE '{pkg}::%'" for pkg in norms]
23
- where = " OR ".join(preds)
24
- sql = (
25
- "SELECT timestamp_ms, package, module, event_type FROM sui.events "
26
- f"WHERE ({where}) AND timestamp_ms BETWEEN {start_ms} AND {now_ms} "
27
- "ORDER BY timestamp_ms DESC "
28
- f"LIMIT {limit}"
29
- )
30
- else:
31
- sql = (
32
- "SELECT timestamp_ms, package, module, event_type FROM sui.events "
33
- f"WHERE timestamp_ms BETWEEN {start_ms} AND {now_ms} "
34
- "ORDER BY timestamp_ms DESC LIMIT {limit}"
35
- ).format(limit=limit)
36
-
37
- result = self.query_service.execute(
38
- sql,
39
- performance=performance,
40
- timeout_seconds=60,
41
- limit=limit,
42
- )
43
- return {
44
- "rowcount": result["rowcount"],
45
- "columns": result["columns"],
46
- "data_preview": result["data_preview"],
47
- }
48
-
49
- def package_overview(
50
- self,
51
- packages: list[str],
52
- *,
53
- hours: int = 72,
54
- timeout_seconds: float | None = 30,
55
- performance: str = "large",
56
- ) -> dict[str, object]:
57
- now_ms = int(time.time() * 1000)
58
- start_ms = now_ms - hours * 3600 * 1000
59
- norms = _normalize_packages(packages)
60
- in_clause = ",".join(f"'{pkg}'" for pkg in norms)
61
-
62
- out: dict[str, object] = {"ok": True}
63
-
64
- def _run(sql: str, limit: int) -> dict[str, object]:
65
- return self.query_service.execute(
66
- sql,
67
- performance=performance,
68
- timeout_seconds=timeout_seconds,
69
- limit=limit,
70
- )
71
-
72
- # Events
73
- try:
74
- sql_ev = (
75
- "SELECT timestamp_ms, package, module, event_type, event_json FROM sui.events "
76
- f"WHERE lower(package) IN ({in_clause}) AND timestamp_ms BETWEEN {start_ms} AND {now_ms} "
77
- "ORDER BY timestamp_ms DESC LIMIT 200"
78
- )
79
- ev = _run(sql_ev, 200)
80
- out["events_preview"] = ev.get("data_preview")
81
- out["events_count"] = ev.get("rowcount")
82
- except TimeoutError:
83
- out["events_timeout"] = True
84
- except Exception as exc:
85
- out["events_error"] = str(exc)
86
-
87
- # Transactions
88
- try:
89
- sql_tx = (
90
- "WITH txs AS (SELECT DISTINCT transaction_digest FROM sui.events "
91
- f"WHERE lower(package) IN ({in_clause}) AND timestamp_ms BETWEEN {start_ms} AND {now_ms}) "
92
- "SELECT t.* FROM sui.transactions t "
93
- "JOIN txs ON t.transaction_digest = txs.transaction_digest "
94
- "ORDER BY t.timestamp_ms DESC LIMIT 200"
95
- )
96
- tx = _run(sql_tx, 200)
97
- out["transactions_preview"] = tx.get("data_preview")
98
- out["transactions_count"] = tx.get("rowcount")
99
- except TimeoutError:
100
- out["transactions_timeout"] = True
101
- except Exception as exc:
102
- out["transactions_error"] = str(exc)
103
-
104
- # Objects
105
- try:
106
- sql_ob = (
107
- "WITH txs AS (SELECT DISTINCT transaction_digest FROM sui.events "
108
- f"WHERE lower(package) IN ({in_clause}) AND timestamp_ms BETWEEN {start_ms} AND {now_ms}) "
109
- "SELECT o.* FROM sui.objects o "
110
- "WHERE o.previous_transaction IN (SELECT transaction_digest FROM txs) "
111
- "ORDER BY o.timestamp_ms DESC LIMIT 200"
112
- )
113
- ob = _run(sql_ob, 200)
114
- out["objects_preview"] = ob.get("data_preview")
115
- out["objects_count"] = ob.get("rowcount")
116
- except TimeoutError:
117
- out["objects_timeout"] = True
118
- except Exception as exc:
119
- out["objects_error"] = str(exc)
120
-
121
- return out
122
-
123
-
124
- def _normalize_packages(packages: list[str]) -> list[str]:
125
- norms: list[str] = []
126
- for p in packages:
127
- value = p.lower()
128
- if not value.startswith("0x"):
129
- value = "0x" + value
130
- norms.append(value)
131
- return norms