recce-nightly 1.23.0.20251029__py3-none-any.whl → 1.23.0.20251031a0__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 recce-nightly might be problematic. Click here for more details.
- recce/VERSION +1 -1
- recce/cli.py +156 -35
- recce/data/404.html +1 -1
- recce/data/index.html +1 -1
- recce/data/index.txt +1 -1
- recce/event/__init__.py +1 -1
- recce/mcp_server.py +276 -0
- recce/state/const.py +1 -1
- recce/state/state_loader.py +2 -4
- {recce_nightly-1.23.0.20251029.dist-info → recce_nightly-1.23.0.20251031a0.dist-info}/METADATA +4 -1
- {recce_nightly-1.23.0.20251029.dist-info → recce_nightly-1.23.0.20251031a0.dist-info}/RECORD +20 -17
- tests/test_cli.py +76 -0
- tests/test_cli_mcp_optional.py +45 -0
- tests/test_mcp_server.py +208 -0
- /recce/data/_next/static/{fCULljmCJ7BP8dZt7mK5Y → 8naFZUkIPYaSgMEmYrK_j}/_buildManifest.js +0 -0
- /recce/data/_next/static/{fCULljmCJ7BP8dZt7mK5Y → 8naFZUkIPYaSgMEmYrK_j}/_ssgManifest.js +0 -0
- {recce_nightly-1.23.0.20251029.dist-info → recce_nightly-1.23.0.20251031a0.dist-info}/WHEEL +0 -0
- {recce_nightly-1.23.0.20251029.dist-info → recce_nightly-1.23.0.20251031a0.dist-info}/entry_points.txt +0 -0
- {recce_nightly-1.23.0.20251029.dist-info → recce_nightly-1.23.0.20251031a0.dist-info}/licenses/LICENSE +0 -0
- {recce_nightly-1.23.0.20251029.dist-info → recce_nightly-1.23.0.20251031a0.dist-info}/top_level.txt +0 -0
recce/mcp_server.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recce MCP (Model Context Protocol) Server
|
|
3
|
+
|
|
4
|
+
This module implements a stdio-based MCP server that provides tools for
|
|
5
|
+
interacting with Recce's data validation capabilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any, Dict, List
|
|
12
|
+
|
|
13
|
+
from mcp.server import Server
|
|
14
|
+
from mcp.server.stdio import stdio_server
|
|
15
|
+
from mcp.types import TextContent, Tool
|
|
16
|
+
|
|
17
|
+
from recce.core import RecceContext, load_context
|
|
18
|
+
from recce.tasks.profile import ProfileDiffTask
|
|
19
|
+
from recce.tasks.query import QueryDiffTask, QueryTask
|
|
20
|
+
from recce.tasks.rowcount import RowCountDiffTask
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RecceMCPServer:
|
|
26
|
+
"""MCP Server for Recce data validation tools"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, context: RecceContext):
|
|
29
|
+
self.context = context
|
|
30
|
+
self.server = Server("recce")
|
|
31
|
+
self._setup_handlers()
|
|
32
|
+
|
|
33
|
+
def _setup_handlers(self):
|
|
34
|
+
"""Register all tool handlers"""
|
|
35
|
+
|
|
36
|
+
@self.server.list_tools()
|
|
37
|
+
async def list_tools() -> List[Tool]:
|
|
38
|
+
"""List all available tools"""
|
|
39
|
+
return [
|
|
40
|
+
Tool(
|
|
41
|
+
name="get_lineage_diff",
|
|
42
|
+
description="Get the lineage diff between base and current environments. "
|
|
43
|
+
"Shows added, removed, and modified models.",
|
|
44
|
+
inputSchema={
|
|
45
|
+
"type": "object",
|
|
46
|
+
"properties": {
|
|
47
|
+
"select": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"description": "dbt selector syntax to filter models (optional)",
|
|
50
|
+
},
|
|
51
|
+
"exclude": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "dbt selector syntax to exclude models (optional)",
|
|
54
|
+
},
|
|
55
|
+
"packages": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"items": {"type": "string"},
|
|
58
|
+
"description": "List of packages to filter (optional)",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
),
|
|
63
|
+
Tool(
|
|
64
|
+
name="row_count_diff",
|
|
65
|
+
description="Compare row counts between base and current environments for specified models.",
|
|
66
|
+
inputSchema={
|
|
67
|
+
"type": "object",
|
|
68
|
+
"properties": {
|
|
69
|
+
"node_names": {
|
|
70
|
+
"type": "array",
|
|
71
|
+
"items": {"type": "string"},
|
|
72
|
+
"description": "List of model names to check row counts (optional)",
|
|
73
|
+
},
|
|
74
|
+
"node_ids": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"items": {"type": "string"},
|
|
77
|
+
"description": "List of node IDs to check row counts (optional)",
|
|
78
|
+
},
|
|
79
|
+
"select": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": "dbt selector syntax to filter models (optional)",
|
|
82
|
+
},
|
|
83
|
+
"exclude": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"description": "dbt selector syntax to exclude models (optional)",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
),
|
|
90
|
+
Tool(
|
|
91
|
+
name="query",
|
|
92
|
+
description="Execute a SQL query on the current environment. "
|
|
93
|
+
"Supports Jinja templates with dbt macros like {{ ref('model_name') }}.",
|
|
94
|
+
inputSchema={
|
|
95
|
+
"type": "object",
|
|
96
|
+
"properties": {
|
|
97
|
+
"sql_template": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"description": "SQL query template with optional Jinja syntax",
|
|
100
|
+
},
|
|
101
|
+
"base": {
|
|
102
|
+
"type": "boolean",
|
|
103
|
+
"description": "Whether to run on base environment (default: false)",
|
|
104
|
+
"default": False,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
"required": ["sql_template"],
|
|
108
|
+
},
|
|
109
|
+
),
|
|
110
|
+
Tool(
|
|
111
|
+
name="query_diff",
|
|
112
|
+
description="Execute SQL queries on both base and current environments and compare results. "
|
|
113
|
+
"Supports primary keys for row-level comparison.",
|
|
114
|
+
inputSchema={
|
|
115
|
+
"type": "object",
|
|
116
|
+
"properties": {
|
|
117
|
+
"sql_template": {
|
|
118
|
+
"type": "string",
|
|
119
|
+
"description": "SQL query template for current environment",
|
|
120
|
+
},
|
|
121
|
+
"base_sql_template": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"description": "SQL query template for base environment (optional, defaults to sql_template)",
|
|
124
|
+
},
|
|
125
|
+
"primary_keys": {
|
|
126
|
+
"type": "array",
|
|
127
|
+
"items": {"type": "string"},
|
|
128
|
+
"description": "List of primary key columns for row comparison (optional)",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
"required": ["sql_template"],
|
|
132
|
+
},
|
|
133
|
+
),
|
|
134
|
+
Tool(
|
|
135
|
+
name="profile_diff",
|
|
136
|
+
description="Generate and compare statistical profiles (min, max, avg, distinct count, etc.) "
|
|
137
|
+
"for columns in a model between base and current environments.",
|
|
138
|
+
inputSchema={
|
|
139
|
+
"type": "object",
|
|
140
|
+
"properties": {
|
|
141
|
+
"model": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"description": "Model name to profile",
|
|
144
|
+
},
|
|
145
|
+
"columns": {
|
|
146
|
+
"type": "array",
|
|
147
|
+
"items": {"type": "string"},
|
|
148
|
+
"description": "List of column names to profile (optional, profiles all columns if not specified)",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
"required": ["model"],
|
|
152
|
+
},
|
|
153
|
+
),
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
@self.server.call_tool()
|
|
157
|
+
async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
|
|
158
|
+
"""Handle tool calls"""
|
|
159
|
+
try:
|
|
160
|
+
if name == "get_lineage_diff":
|
|
161
|
+
result = await self._get_lineage_diff(arguments)
|
|
162
|
+
elif name == "row_count_diff":
|
|
163
|
+
result = await self._row_count_diff(arguments)
|
|
164
|
+
elif name == "query":
|
|
165
|
+
result = await self._query(arguments)
|
|
166
|
+
elif name == "query_diff":
|
|
167
|
+
result = await self._query_diff(arguments)
|
|
168
|
+
elif name == "profile_diff":
|
|
169
|
+
result = await self._profile_diff(arguments)
|
|
170
|
+
else:
|
|
171
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
172
|
+
|
|
173
|
+
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.exception(f"Error executing tool {name}")
|
|
176
|
+
return [TextContent(type="text", text=json.dumps({"error": str(e)}, indent=2))]
|
|
177
|
+
|
|
178
|
+
async def _get_lineage_diff(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
179
|
+
"""Get lineage diff between base and current"""
|
|
180
|
+
try:
|
|
181
|
+
# Get lineage diff from adapter (returns a Pydantic LineageDiff model)
|
|
182
|
+
lineage_diff = self.context.get_lineage_diff()
|
|
183
|
+
# Convert Pydantic model to dict for JSON serialization
|
|
184
|
+
return lineage_diff.model_dump(mode="json")
|
|
185
|
+
except Exception:
|
|
186
|
+
logger.exception("Error getting lineage diff")
|
|
187
|
+
raise
|
|
188
|
+
|
|
189
|
+
async def _row_count_diff(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
190
|
+
"""Execute row count diff task"""
|
|
191
|
+
try:
|
|
192
|
+
task = RowCountDiffTask(params=arguments)
|
|
193
|
+
|
|
194
|
+
# Execute task synchronously (it's already sync)
|
|
195
|
+
result = await asyncio.get_event_loop().run_in_executor(None, task.execute)
|
|
196
|
+
|
|
197
|
+
return result
|
|
198
|
+
except Exception:
|
|
199
|
+
logger.exception("Error executing row count diff")
|
|
200
|
+
raise
|
|
201
|
+
|
|
202
|
+
async def _query(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
203
|
+
"""Execute a query"""
|
|
204
|
+
try:
|
|
205
|
+
sql_template = arguments.get("sql_template")
|
|
206
|
+
is_base = arguments.get("base", False)
|
|
207
|
+
|
|
208
|
+
params = {"sql_template": sql_template}
|
|
209
|
+
task = QueryTask(params=params)
|
|
210
|
+
task.is_base = is_base
|
|
211
|
+
|
|
212
|
+
# Execute task
|
|
213
|
+
result = await asyncio.get_event_loop().run_in_executor(None, task.execute)
|
|
214
|
+
|
|
215
|
+
# Convert to dict if it's a model
|
|
216
|
+
if hasattr(result, "model_dump"):
|
|
217
|
+
return result.model_dump(mode="json")
|
|
218
|
+
return result
|
|
219
|
+
except Exception:
|
|
220
|
+
logger.exception("Error executing query")
|
|
221
|
+
raise
|
|
222
|
+
|
|
223
|
+
async def _query_diff(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
224
|
+
"""Execute query diff task"""
|
|
225
|
+
try:
|
|
226
|
+
task = QueryDiffTask(params=arguments)
|
|
227
|
+
|
|
228
|
+
# Execute task
|
|
229
|
+
result = await asyncio.get_event_loop().run_in_executor(None, task.execute)
|
|
230
|
+
|
|
231
|
+
# Convert to dict if it's a model
|
|
232
|
+
if hasattr(result, "model_dump"):
|
|
233
|
+
return result.model_dump(mode="json")
|
|
234
|
+
return result
|
|
235
|
+
except Exception:
|
|
236
|
+
logger.exception("Error executing query diff")
|
|
237
|
+
raise
|
|
238
|
+
|
|
239
|
+
async def _profile_diff(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
240
|
+
"""Execute profile diff task"""
|
|
241
|
+
try:
|
|
242
|
+
task = ProfileDiffTask(params=arguments)
|
|
243
|
+
|
|
244
|
+
# Execute task
|
|
245
|
+
result = await asyncio.get_event_loop().run_in_executor(None, task.execute)
|
|
246
|
+
|
|
247
|
+
# Convert to dict if it's a model
|
|
248
|
+
if hasattr(result, "model_dump"):
|
|
249
|
+
return result.model_dump(mode="json")
|
|
250
|
+
return result
|
|
251
|
+
except Exception:
|
|
252
|
+
logger.exception("Error executing profile diff")
|
|
253
|
+
raise
|
|
254
|
+
|
|
255
|
+
async def run(self):
|
|
256
|
+
"""Run the MCP server"""
|
|
257
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
258
|
+
await self.server.run(read_stream, write_stream, self.server.create_initialization_options())
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
async def run_mcp_server(**kwargs):
|
|
262
|
+
"""
|
|
263
|
+
Entry point for running the MCP server
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
**kwargs: Arguments for loading RecceContext (dbt options, etc.)
|
|
267
|
+
"""
|
|
268
|
+
# Setup logging
|
|
269
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
270
|
+
|
|
271
|
+
# Load Recce context
|
|
272
|
+
context = load_context(**kwargs)
|
|
273
|
+
|
|
274
|
+
# Create and run server
|
|
275
|
+
server = RecceMCPServer(context)
|
|
276
|
+
await server.run()
|
recce/state/const.py
CHANGED
|
@@ -21,6 +21,6 @@ RECCE_CLOUD_PASSWORD_MISSING = ErrorMessage(
|
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
RECCE_API_TOKEN_MISSING = ErrorMessage(
|
|
24
|
-
error_message="No
|
|
24
|
+
error_message="No Recce API token is provided",
|
|
25
25
|
hint_message="Please login to Recce Cloud and copy the API token from the settings page",
|
|
26
26
|
)
|
recce/state/state_loader.py
CHANGED
|
@@ -8,9 +8,7 @@ from recce.exceptions import RecceException
|
|
|
8
8
|
from recce.pull_request import fetch_pr_metadata
|
|
9
9
|
|
|
10
10
|
from ..util.io import SupportedFileTypes, file_io_factory
|
|
11
|
-
from .const import
|
|
12
|
-
RECCE_CLOUD_TOKEN_MISSING,
|
|
13
|
-
)
|
|
11
|
+
from .const import RECCE_API_TOKEN_MISSING
|
|
14
12
|
from .state import RecceState
|
|
15
13
|
|
|
16
14
|
logger = logging.getLogger("uvicorn")
|
|
@@ -55,7 +53,7 @@ class RecceStateLoader(ABC):
|
|
|
55
53
|
self.catalog = "preview"
|
|
56
54
|
self.share_id = self.cloud_options.get("share_id")
|
|
57
55
|
else:
|
|
58
|
-
raise RecceException(
|
|
56
|
+
raise RecceException(RECCE_API_TOKEN_MISSING.error_message)
|
|
59
57
|
|
|
60
58
|
@property
|
|
61
59
|
def token(self):
|
{recce_nightly-1.23.0.20251029.dist-info → recce_nightly-1.23.0.20251031a0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: recce-nightly
|
|
3
|
-
Version: 1.23.0.
|
|
3
|
+
Version: 1.23.0.20251031a0
|
|
4
4
|
Summary: Environment diff tool for dbt
|
|
5
5
|
Home-page: https://github.com/InfuseAI/recce
|
|
6
6
|
Author: InfuseAI Dev Team
|
|
@@ -39,8 +39,11 @@ Requires-Dist: python-multipart
|
|
|
39
39
|
Requires-Dist: GitPython
|
|
40
40
|
Requires-Dist: PyGithub
|
|
41
41
|
Requires-Dist: sqlglot
|
|
42
|
+
Provides-Extra: mcp
|
|
43
|
+
Requires-Dist: mcp>=1.0.0; extra == "mcp"
|
|
42
44
|
Provides-Extra: dev
|
|
43
45
|
Requires-Dist: pytest>=4.6; extra == "dev"
|
|
46
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
44
47
|
Requires-Dist: pytest-flake8; extra == "dev"
|
|
45
48
|
Requires-Dist: black>=25.1.0; extra == "dev"
|
|
46
49
|
Requires-Dist: isort>=6.0.1; extra == "dev"
|
{recce_nightly-1.23.0.20251029.dist-info → recce_nightly-1.23.0.20251031a0.dist-info}/RECORD
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
recce/VERSION,sha256=
|
|
1
|
+
recce/VERSION,sha256=KfJWiXeB8a7A5SLsRmYGbk8qKvTWEGgq2T3t6UwIJS4,18
|
|
2
2
|
recce/__init__.py,sha256=gT5G7sJ78C1GHAWr4pna6UvVNxPKs29_EgFS_ia3DT4,2554
|
|
3
3
|
recce/artifact.py,sha256=BdNW5eQQqEKu5cRzA_XusKMIN73V7-MTQP0VxnLdd9E,9265
|
|
4
|
-
recce/cli.py,sha256=
|
|
4
|
+
recce/cli.py,sha256=ZhORH6w30u-xBFlYXe1nTvEkt3zEqOPcnZlG9whtX4Q,56601
|
|
5
5
|
recce/config.py,sha256=noWimlOdGHAtBhNoApK5gRTQMwNPmUGAmZu39MV7szo,4697
|
|
6
6
|
recce/connect_to_cloud.py,sha256=CT7ELcVCTNa9ukSp_gMrJ0Av2UCUpF9H03tIk9y7U-4,4768
|
|
7
7
|
recce/core.py,sha256=Fph2kodFqzXy3cob80M9-nPVcv-Xxzme-E-bKFJ4cBQ,10754
|
|
@@ -9,6 +9,7 @@ recce/diff.py,sha256=L2_bzQ3__PO-0aeir8PHF8FvSOUmQ8WcDXgML1-mHdY,748
|
|
|
9
9
|
recce/exceptions.py,sha256=SclQ678GrHGjw7p_ZFJ3vZaL_yMU5xABeIAm2u_W2bk,592
|
|
10
10
|
recce/git.py,sha256=8Eg-6NzL-KjA3rT-ibbAyaCwGlzV0JqH3yGikrJNMDA,2344
|
|
11
11
|
recce/github.py,sha256=vIwHTpB8YWEUcHQlW0nToUUKmGGO0NcmB_Q5RvxQLLE,7438
|
|
12
|
+
recce/mcp_server.py,sha256=NjR1q4pMzph0Ps1pQlFX2Go83ml1JQD4ilP_4mLH7QI,11461
|
|
12
13
|
recce/pull_request.py,sha256=dLHsdiMtL4NC6Vkwp8Mq4baJmnqbm19lfiDti9MREN0,3850
|
|
13
14
|
recce/run.py,sha256=ctp-K2FmrsGrS4xEZKDMbMtitNw2QKJ6Tq_0OvCWaaM,14080
|
|
14
15
|
recce/server.py,sha256=FURnuOtooA4ezglEouV_-J-y2ECrNjd6vvSiROnm7Nc,22851
|
|
@@ -23,11 +24,13 @@ recce/apis/check_api.py,sha256=KMCXSMl1qqzx2jQgRqCrD4j_cY3EHBbM3H2-t-6saAU,6227
|
|
|
23
24
|
recce/apis/check_func.py,sha256=gktbCcyk3WGvWRJJ-wDnwv7NrIny2nTHWLl1-kdiVRo,4183
|
|
24
25
|
recce/apis/run_api.py,sha256=eOaxOxXDkH59uqGCd4blld7edavUx7JU_DCd2WAYrL8,3416
|
|
25
26
|
recce/apis/run_func.py,sha256=6wC8TDU-h7TLr2VZH7HNsWaUVlQ9HBN5N_dwqfi4lMY,7440
|
|
26
|
-
recce/data/404.html,sha256=
|
|
27
|
+
recce/data/404.html,sha256=54wgc42EETu-yl0H_ed7Z9n6DGzOuZwFDma8epFw7QQ,59085
|
|
27
28
|
recce/data/auth_callback.html,sha256=H-XfdlAFiw5VU2RpKBVQbwh1AIqJrPHrFA0S09nNJZA,94779
|
|
28
29
|
recce/data/favicon.ico,sha256=B2mBumUOnzvUrXrqNkrc5QfdDXjzEXRcWkWur0fJ6sM,2565
|
|
29
|
-
recce/data/index.html,sha256=
|
|
30
|
-
recce/data/index.txt,sha256=
|
|
30
|
+
recce/data/index.html,sha256=xiP2UJABklQHBxDO-oRjrWyb4K4nPpYfFjyTnanbzBM,77170
|
|
31
|
+
recce/data/index.txt,sha256=ZSdcpMDQqb6u6iRM5xF1z1phKZaH2ZMNmTMw4Bs2UK8,6406
|
|
32
|
+
recce/data/_next/static/8naFZUkIPYaSgMEmYrK_j/_buildManifest.js,sha256=oqgeL8q3W6T625zlDuGLFDWY3H4_3tlJCNWvj42kYcs,544
|
|
33
|
+
recce/data/_next/static/8naFZUkIPYaSgMEmYrK_j/_ssgManifest.js,sha256=Z49s4suAsf5y_GfnQSvm4qtq2ggxEbZPfEDTXjy6XgA,80
|
|
31
34
|
recce/data/_next/static/chunks/12f8fac4-a2fd98f42126999f.js,sha256=ILGeY9xCblLTFgnG9zdwGqe5jxxulBBz761KM3HRxMc,152792
|
|
32
35
|
recce/data/_next/static/chunks/272-058426d7d18ce061.js,sha256=-MHfq8GlLLjSveELfok_YIHlMtuBkYY-hCsJc-EyQOA,10714
|
|
33
36
|
recce/data/_next/static/chunks/3a92ee20-9c4519f55ecaa314.js,sha256=3pmoD3iJD1ZSIoY-v_9ZsGqXu-ILHdy5FiKF3vvaN2s,177908
|
|
@@ -68,8 +71,6 @@ recce/data/_next/static/css/5da8812af0008c76.css,sha256=1JLGRCtK9HwSjFvlp-iimWYY
|
|
|
68
71
|
recce/data/_next/static/css/8edca58d4abcf908.css,sha256=Ab-wk7RhsXgiX5fkJsrCJ-EVvrS1hZtOFpI0uZQSgL0,8093
|
|
69
72
|
recce/data/_next/static/css/abdb9814a3dd18bb.css,sha256=Do5x9FJaOIe-pSBlbk_PP7PQAYFbDXTTl_0PWY4cnBM,1327
|
|
70
73
|
recce/data/_next/static/css/af2813f12c8a4fd8.css,sha256=sLhHpBJpH5zsmd5h13LU922e3dTlJWkPzZ5R67f8CzM,12260
|
|
71
|
-
recce/data/_next/static/fCULljmCJ7BP8dZt7mK5Y/_buildManifest.js,sha256=oqgeL8q3W6T625zlDuGLFDWY3H4_3tlJCNWvj42kYcs,544
|
|
72
|
-
recce/data/_next/static/fCULljmCJ7BP8dZt7mK5Y/_ssgManifest.js,sha256=Z49s4suAsf5y_GfnQSvm4qtq2ggxEbZPfEDTXjy6XgA,80
|
|
73
74
|
recce/data/_next/static/media/montserrat-cyrillic-800-normal.728ecd06.woff,sha256=XiXifLCW_CfSR4eNCIxDzi3Cg59f6L34ARTHrw-hoDA,10924
|
|
74
75
|
recce/data/_next/static/media/montserrat-cyrillic-800-normal.ffc85bb1.woff2,sha256=6XlujeGVTucujV3iPSa7j1d9S5A6uuH-EduJBCeVFu8,11180
|
|
75
76
|
recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.622429c0.woff2,sha256=9hQT0vu2HmOb21Pk5Ht7bydZU7QIsY7rCyAPtm1VxM8,12144
|
|
@@ -87,7 +88,7 @@ recce/data/imgs/feedback/thumbs-up.png,sha256=VF3BH8bmYEqcSsMDJO57xMqW4t6crCXUXa
|
|
|
87
88
|
recce/data/logo/recce-logo-white.png,sha256=y3re8iEucJnMUkAkRS2CjWHTlOydyvgWdWjuQKcXDbk,46923
|
|
88
89
|
recce/event/CONFIG,sha256=w8_AVcNu_JF-t8lNmjOqtsXbeOawMzpEhkISaMlm-iU,48
|
|
89
90
|
recce/event/SENTRY_DNS,sha256=nWXZevLC4qDScvNtjs329X13oxqvtFI_Kiu6cfCDOBA,83
|
|
90
|
-
recce/event/__init__.py,sha256=
|
|
91
|
+
recce/event/__init__.py,sha256=UKo0U0OZTkWsr7QBnCwXOHu7XtY0dhYPRTXuCsX8uIw,8847
|
|
91
92
|
recce/event/collector.py,sha256=6Vvcz9zejNNMytZ_K0HFV_kN6-S5B-SUOglBrlSPBo8,5884
|
|
92
93
|
recce/event/track.py,sha256=3wBzpB8l2gtnDzjh-wJatmLeBbtnP0q-BLQZQVQ23sM,5310
|
|
93
94
|
recce/models/__init__.py,sha256=F7cgALtdWnwv37R0eEgKZ_yBsMwxWnUfo3jAZ3u6qyU,209
|
|
@@ -96,10 +97,10 @@ recce/models/run.py,sha256=QK2gvOWvko9YYhd2NLs3BPt5l4MSCZGwpzTAiqx9zJw,1161
|
|
|
96
97
|
recce/models/types.py,sha256=9ReOyIv3rUD3_cIQfB9Rplb0L2YQuZr4kS9Zai30nB8,5234
|
|
97
98
|
recce/state/__init__.py,sha256=V1FRPTQJUz-uwI3Cn8wDa5Z9bueVs86MR_1Ti4RGfPc,650
|
|
98
99
|
recce/state/cloud.py,sha256=hCLvRgMwQwRxlcQhU33L8gXq6yoytBdAIwgWIWPTeLc,25489
|
|
99
|
-
recce/state/const.py,sha256=
|
|
100
|
+
recce/state/const.py,sha256=5XwGWEv4RhGucgl1RHWg_67gVY7fV0-jsbZPpfnOCTs,828
|
|
100
101
|
recce/state/local.py,sha256=bZIkl7cAfyYaGgYEr3uD_JLrtwlHBnu8_o1Qz2djQzw,1920
|
|
101
102
|
recce/state/state.py,sha256=D3wBd8VLtrYEqAb0ueNxzjF84OB4vFDNJL3a1fPhziQ,4177
|
|
102
|
-
recce/state/state_loader.py,sha256=
|
|
103
|
+
recce/state/state_loader.py,sha256=IiGaFDm2y_0NMsv_YUZhSuDTsvWmdMrf9SXIv9w5sKE,5966
|
|
103
104
|
recce/tasks/__init__.py,sha256=b553AtDHjYROgmMePv_Hv_X3fjh4iEn11gzzpUJz6_o,610
|
|
104
105
|
recce/tasks/core.py,sha256=JFYa1CfgOiRPQ7KVTwMuxJjhMB-pvCwB-xezVt-h3RU,4080
|
|
105
106
|
recce/tasks/dataframe.py,sha256=03UBWwt0DFTXlaEOtnV5i_mxdRKD7UbRayewEL2Ub48,3650
|
|
@@ -125,14 +126,16 @@ recce/util/pydantic_model.py,sha256=KumKuyCjbTzEMsKLE4-b-eZfp0gLhYDdmVtw1-hxiJw,
|
|
|
125
126
|
recce/util/recce_cloud.py,sha256=fM4At7-90dS_TXIAxYNjWdEOW1OUbjTOqYmwWt89eEw,17171
|
|
126
127
|
recce/util/singleton.py,sha256=1cU99I0f9tjuMQLMJyLsK1oK3fZJMsO5-TbRHAMXqds,627
|
|
127
128
|
recce/yaml/__init__.py,sha256=PAym5akbtL24Ag7VR7EW8SS2VINNaJU06esbfe-ek-U,1328
|
|
128
|
-
recce_nightly-1.23.0.
|
|
129
|
+
recce_nightly-1.23.0.20251031a0.dist-info/licenses/LICENSE,sha256=CQjjMy9aYPhfe8xG_bcpIfKtNkdxLZ5IOb8oPygtUhY,11343
|
|
129
130
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
130
|
-
tests/test_cli.py,sha256=
|
|
131
|
+
tests/test_cli.py,sha256=hT-KxF5PcVtNZ05HGsTrjec6WTDaD6vInCijJcCYiU8,9923
|
|
132
|
+
tests/test_cli_mcp_optional.py,sha256=w6Iwr22fheaOxw6vGXVZyvi0pw5uh8jDJKcdnd64-_o,1629
|
|
131
133
|
tests/test_cloud_listing_cli.py,sha256=77As9n0Sngdh53dfHkShmqDETMTOohlqw1xXO9cIago,14208
|
|
132
134
|
tests/test_config.py,sha256=ODDFe_XF6gphmSmmc422dGLBaCCmG-IjDzTkD5SJsJE,1557
|
|
133
135
|
tests/test_connect_to_cloud.py,sha256=b2fgV8L1iQBdEwh6RumMsIIyYg7GtMOMRz1dvE3WRPg,3059
|
|
134
136
|
tests/test_core.py,sha256=WgCFm8Au3YQI-V5UbZ6LA8irMNHRc7NWutIq2Mny0LE,6242
|
|
135
137
|
tests/test_dbt.py,sha256=VzXvdoJNwwEmKNhJJDNB1N_qZYwp1aoJQ1sLcoyRBmk,1316
|
|
138
|
+
tests/test_mcp_server.py,sha256=k7FxIgHNQF-DWrFJ6eLVi3uyhlDz3AO5VHCEwY56v1s,7709
|
|
136
139
|
tests/test_pull_request.py,sha256=HmZo5MoDaoKSgPwbLxJ3Ur3ajZ7IxhkzJxaOmhg6bwE,3562
|
|
137
140
|
tests/test_server.py,sha256=sq03awzG-kvLNJwHaZGDvVjd2uAs502XUKZ2nLNwvRQ,3391
|
|
138
141
|
tests/test_summary.py,sha256=D0WvAkdO-vzGcvholH2rfS1wTxUXjVHwWm59fWy45eA,2876
|
|
@@ -154,8 +157,8 @@ tests/tasks/test_row_count.py,sha256=21PaP2aq-x8-pqwzWHRT1sixhQ8g3CQNRWOZTTmbK0s
|
|
|
154
157
|
tests/tasks/test_schema.py,sha256=7ds4Vx8ixaiIWDR49Lvjem4xlPkRP1cXazDRY3roUak,3121
|
|
155
158
|
tests/tasks/test_top_k.py,sha256=YR_GS__DJsbDlQVaEEdJvNQ3fh1VmV5Nb3G7lb0r6YM,1779
|
|
156
159
|
tests/tasks/test_valuediff.py,sha256=_xQJGgxsXoy2NYk_Z6Hsw2FlVh6zk2nN_iUueyRN1e8,2046
|
|
157
|
-
recce_nightly-1.23.0.
|
|
158
|
-
recce_nightly-1.23.0.
|
|
159
|
-
recce_nightly-1.23.0.
|
|
160
|
-
recce_nightly-1.23.0.
|
|
161
|
-
recce_nightly-1.23.0.
|
|
160
|
+
recce_nightly-1.23.0.20251031a0.dist-info/METADATA,sha256=2IJFCGLp0u13Z5c2UNvRzBcNmXuvDcMLlpW-IybdZGE,9581
|
|
161
|
+
recce_nightly-1.23.0.20251031a0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
162
|
+
recce_nightly-1.23.0.20251031a0.dist-info/entry_points.txt,sha256=oqoY_IiwIqXbgrIsPnlqUqao2eiIeP2dprowkOlmeyg,40
|
|
163
|
+
recce_nightly-1.23.0.20251031a0.dist-info/top_level.txt,sha256=6PKGVpf75idP0C6KEaldDzzZUauIxNu1ZDstau1pI4I,12
|
|
164
|
+
recce_nightly-1.23.0.20251031a0.dist-info/RECORD,,
|
tests/test_cli.py
CHANGED
|
@@ -67,6 +67,82 @@ class TestCommandServer(TestCase):
|
|
|
67
67
|
mock_state_loader_class.assert_called_once()
|
|
68
68
|
mock_run.assert_called_once()
|
|
69
69
|
|
|
70
|
+
@patch.object(RecceContext, "verify_required_artifacts")
|
|
71
|
+
@patch("recce.util.recce_cloud.get_recce_cloud_onboarding_state")
|
|
72
|
+
@patch("recce.cli.uvicorn.run")
|
|
73
|
+
@patch("recce.cli.CloudStateLoader")
|
|
74
|
+
@patch("recce.cli.prepare_api_token", return_value="test_api_token")
|
|
75
|
+
def test_cmd_server_with_session_id(
|
|
76
|
+
self,
|
|
77
|
+
mock_prepare_api_token,
|
|
78
|
+
mock_state_loader_class,
|
|
79
|
+
mock_run,
|
|
80
|
+
mock_get_recce_cloud_onboarding_state,
|
|
81
|
+
mock_verify_required_artifacts,
|
|
82
|
+
):
|
|
83
|
+
"""Test that --session-id automatically enables cloud and review mode"""
|
|
84
|
+
mock_state_loader = MagicMock(spec=CloudStateLoader)
|
|
85
|
+
mock_state_loader.verify.return_value = True
|
|
86
|
+
mock_state_loader.review_mode = True
|
|
87
|
+
mock_get_recce_cloud_onboarding_state.return_value = "completed"
|
|
88
|
+
mock_verify_required_artifacts.return_value = True, None
|
|
89
|
+
|
|
90
|
+
mock_state_loader_class.return_value = mock_state_loader
|
|
91
|
+
|
|
92
|
+
# Test with --session-id (should automatically enable cloud and review)
|
|
93
|
+
result = self.runner.invoke(cli_command_server, ["--session-id", "test-session-123", "--single-env"])
|
|
94
|
+
|
|
95
|
+
# Should succeed
|
|
96
|
+
assert result.exit_code == 0
|
|
97
|
+
|
|
98
|
+
# Should create CloudStateLoader with session_id in cloud_options
|
|
99
|
+
mock_state_loader_class.assert_called_once()
|
|
100
|
+
call_args = mock_state_loader_class.call_args
|
|
101
|
+
assert call_args.kwargs["review_mode"] is True
|
|
102
|
+
assert "session_id" in call_args.kwargs["cloud_options"]
|
|
103
|
+
assert call_args.kwargs["cloud_options"]["session_id"] == "test-session-123"
|
|
104
|
+
|
|
105
|
+
mock_run.assert_called_once()
|
|
106
|
+
|
|
107
|
+
@patch.object(RecceContext, "verify_required_artifacts")
|
|
108
|
+
@patch("recce.util.recce_cloud.get_recce_cloud_onboarding_state")
|
|
109
|
+
@patch("recce.cli.uvicorn.run")
|
|
110
|
+
@patch("recce.cli.CloudStateLoader")
|
|
111
|
+
@patch("recce.cli.prepare_api_token", return_value="test_api_token")
|
|
112
|
+
def test_cmd_server_with_share_url(
|
|
113
|
+
self,
|
|
114
|
+
mock_prepare_api_token,
|
|
115
|
+
mock_state_loader_class,
|
|
116
|
+
mock_run,
|
|
117
|
+
mock_get_recce_cloud_onboarding_state,
|
|
118
|
+
mock_verify_required_artifacts,
|
|
119
|
+
):
|
|
120
|
+
"""Test that --share-url automatically enables cloud and review mode"""
|
|
121
|
+
mock_state_loader = MagicMock(spec=CloudStateLoader)
|
|
122
|
+
mock_state_loader.verify.return_value = True
|
|
123
|
+
mock_state_loader.review_mode = True
|
|
124
|
+
mock_get_recce_cloud_onboarding_state.return_value = "completed"
|
|
125
|
+
mock_verify_required_artifacts.return_value = True, None
|
|
126
|
+
|
|
127
|
+
mock_state_loader_class.return_value = mock_state_loader
|
|
128
|
+
|
|
129
|
+
# Test with --share-url (should automatically enable cloud and review)
|
|
130
|
+
result = self.runner.invoke(
|
|
131
|
+
cli_command_server, ["--share-url", "https://cloud.recce.io/share/abc123", "--single-env"]
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Should succeed
|
|
135
|
+
assert result.exit_code == 0
|
|
136
|
+
|
|
137
|
+
# Should create CloudStateLoader with share_id in cloud_options
|
|
138
|
+
mock_state_loader_class.assert_called_once()
|
|
139
|
+
call_args = mock_state_loader_class.call_args
|
|
140
|
+
assert call_args.kwargs["review_mode"] is True
|
|
141
|
+
assert "share_id" in call_args.kwargs["cloud_options"]
|
|
142
|
+
assert call_args.kwargs["cloud_options"]["share_id"] == "abc123"
|
|
143
|
+
|
|
144
|
+
mock_run.assert_called_once()
|
|
145
|
+
|
|
70
146
|
@patch.object(RecceContext, "verify_required_artifacts")
|
|
71
147
|
@patch("os.path.isdir", side_effect=lambda path: True if path == "existed_folder" else False)
|
|
72
148
|
@patch("recce.cli.uvicorn.run")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test that CLI can be imported even when mcp is not available.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_cli_can_be_imported_without_mcp():
|
|
10
|
+
"""Test that recce.cli can be imported even if mcp package is not available"""
|
|
11
|
+
# This test verifies that the CLI module doesn't fail to import
|
|
12
|
+
# when mcp is not installed, since mcp is an optional dependency
|
|
13
|
+
from recce import cli
|
|
14
|
+
|
|
15
|
+
assert cli is not None
|
|
16
|
+
assert hasattr(cli, "cli")
|
|
17
|
+
assert hasattr(cli, "mcp_server")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_mcp_server_command_fails_gracefully_without_mcp():
|
|
21
|
+
"""Test that mcp-server command shows helpful error when mcp is not available"""
|
|
22
|
+
# Mock sys.modules to simulate mcp not being installed
|
|
23
|
+
with patch.dict(sys.modules, {"mcp": None, "mcp.server": None, "mcp.server.stdio": None, "mcp.types": None}):
|
|
24
|
+
# Remove mcp_server from modules to force reimport
|
|
25
|
+
if "recce.mcp_server" in sys.modules:
|
|
26
|
+
del sys.modules["recce.mcp_server"]
|
|
27
|
+
|
|
28
|
+
from recce.cli import mcp_server
|
|
29
|
+
|
|
30
|
+
# The function should exist
|
|
31
|
+
assert mcp_server is not None
|
|
32
|
+
|
|
33
|
+
# When called, it should handle ImportError gracefully
|
|
34
|
+
# (We can't easily test the actual execution without more mocking,
|
|
35
|
+
# but we've verified the function exists and can be imported)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_cli_command_exists():
|
|
39
|
+
"""Test that both server and mcp-server commands are registered"""
|
|
40
|
+
from recce.cli import cli
|
|
41
|
+
|
|
42
|
+
# Check that both commands exist
|
|
43
|
+
commands = {cmd.name for cmd in cli.commands.values()}
|
|
44
|
+
assert "server" in commands
|
|
45
|
+
assert "mcp_server" in commands or "mcp-server" in commands
|