Skip to content

Commit 39aec7c

Browse files
authored
Merge pull request #26 from eifrach/add_linter
NO-ISSUE: adding linter and test linter to build flow
2 parents 45c1e13 + 5b34a74 commit 39aec7c

File tree

8 files changed

+205
-123
lines changed

8 files changed

+205
-123
lines changed

.github/workflows/python-lint.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: python-lint
2+
on:
3+
pull_request:
4+
branches:
5+
- "*"
6+
jobs:
7+
build:
8+
name: python Lint
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout code
12+
uses: actions/checkout@v4
13+
14+
- name: Install uv
15+
uses: astral-sh/setup-uv@v6
16+
17+
- name: Run flake8
18+
run: uv sync && uv run flake8 src/ --max-line-length=120 --ignore=E501,W503
19+
with:
20+
python-version: "3.11"

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ build-backend = "hatchling.build"
2121
packages = ["src"]
2222

2323
[tool.uv]
24-
dev-dependencies = []
24+
dev-dependencies = [
25+
"flake8>=7.3.0",
26+
]

src/config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030

3131
# Check if Prometheus is available
3232
try:
33-
from prometheus_client import Counter, Histogram, Gauge, CONTENT_TYPE_LATEST, generate_latest
33+
# These imports are used in metrics.py
34+
from prometheus_client import Counter, Histogram, Gauge, CONTENT_TYPE_LATEST, generate_latest # noqa: F401
3435
PROMETHEUS_AVAILABLE = True
3536
except ImportError:
36-
PROMETHEUS_AVAILABLE = False
37+
PROMETHEUS_AVAILABLE = False

src/database.py

Lines changed: 50 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
1-
import os
21
import json
3-
import re
42
import time
53
import logging
64
from typing import Any, List, Dict, Optional
75

86
import httpx
97

108
from config import (
11-
MCP_TRANSPORT,
12-
SNOWFLAKE_BASE_URL,
13-
SNOWFLAKE_DATABASE,
9+
SNOWFLAKE_BASE_URL,
10+
SNOWFLAKE_DATABASE,
1411
SNOWFLAKE_SCHEMA,
1512
SNOWFLAKE_TOKEN
1613
)
1714
from metrics import track_snowflake_query
1815

1916
logger = logging.getLogger(__name__)
2017

18+
2119
def sanitize_sql_value(value: str) -> str:
2220
"""Sanitize a SQL value to prevent injection attacks"""
2321
if not isinstance(value, str):
@@ -26,37 +24,38 @@ def sanitize_sql_value(value: str) -> str:
2624
# For string values, we'll escape single quotes by doubling them
2725
return value.replace("'", "''")
2826

27+
2928
async def make_snowflake_request(
30-
endpoint: str,
31-
method: str = "POST",
29+
endpoint: str,
30+
method: str = "POST",
3231
data: dict[str, Any] = None,
3332
snowflake_token: Optional[str] = None
3433
) -> dict[str, Any] | None:
3534
"""Make a request to Snowflake API"""
3635
# Use provided token or fall back to config
3736
token = snowflake_token or SNOWFLAKE_TOKEN
38-
37+
3938
if not token:
4039
logger.error("SNOWFLAKE_TOKEN environment variable is required but not set")
4140
return None
42-
41+
4342
headers = {
4443
"Authorization": f"Bearer {token}",
4544
"Accept": "application/json",
4645
"Content-Type": "application/json"
4746
}
48-
47+
4948
url = f"{SNOWFLAKE_BASE_URL}/{endpoint}"
50-
49+
5150
try:
5251
async with httpx.AsyncClient(timeout=30.0) as client:
5352
if method.upper() == "GET":
5453
response = await client.request(method, url, headers=headers, params=data)
5554
else:
5655
response = await client.request(method, url, headers=headers, json=data)
57-
56+
5857
response.raise_for_status()
59-
58+
6059
# Try to parse JSON, but handle cases where response is not valid JSON
6160
try:
6261
return response.json()
@@ -65,19 +64,20 @@ async def make_snowflake_request(
6564
logger.error(f"Response content: {response.text[:500]}...") # Log first 500 chars
6665
# Return None to indicate error, which will be handled by calling functions
6766
return None
68-
67+
6968
except httpx.HTTPStatusError as http_error:
7069
logger.error(f"HTTP error from Snowflake API: {http_error.response.status_code} - {http_error.response.text}")
7170
return None
7271
except Exception as e:
7372
logger.error(f"Unexpected error in Snowflake API request: {str(e)}")
7473
return None
7574

75+
7676
async def execute_snowflake_query(sql: str, snowflake_token: Optional[str] = None) -> List[Dict[str, Any]]:
7777
"""Execute a SQL query against Snowflake and return results"""
7878
start_time = time.time()
7979
success = False
80-
80+
8181
try:
8282
# Use the statements endpoint to execute SQL
8383
endpoint = "statements"
@@ -87,16 +87,16 @@ async def execute_snowflake_query(sql: str, snowflake_token: Optional[str] = Non
8787
"database": SNOWFLAKE_DATABASE,
8888
"schema": SNOWFLAKE_SCHEMA
8989
}
90-
90+
9191
logger.info(f"Executing Snowflake query: {sql[:100]}...") # Log first 100 chars of query
92-
92+
9393
response = await make_snowflake_request(endpoint, "POST", payload, snowflake_token)
94-
94+
9595
# Check if response is None (indicating an error in API request or JSON parsing)
9696
if response is None:
9797
logger.error("Failed to get valid response from Snowflake API")
9898
return []
99-
99+
100100
# Parse the response to extract data
101101
if response and "data" in response:
102102
logger.info(f"Successfully got {len(response['data'])} rows from Snowflake")
@@ -109,109 +109,112 @@ async def execute_snowflake_query(sql: str, snowflake_token: Optional[str] = Non
109109
logger.info(f"Successfully got {len(result_set['data'])} rows from Snowflake (resultSet format)")
110110
success = True
111111
return result_set["data"]
112-
112+
113113
logger.warning("No data found in Snowflake response")
114114
success = True # No data is still a successful query
115115
return []
116-
116+
117117
except Exception as e:
118118
logger.error(f"Error executing Snowflake query: {str(e)}")
119119
logger.error(f"Query that failed: {sql}")
120120
return []
121121
finally:
122122
track_snowflake_query(start_time, success)
123123

124+
124125
def format_snowflake_row(row_data: List[Any], columns: List[str]) -> Dict[str, Any]:
125126
"""Convert Snowflake row data to dictionary using column names"""
126127
if len(row_data) != len(columns):
127128
return {}
128-
129+
129130
return {columns[i]: row_data[i] for i in range(len(columns))}
130131

132+
131133
async def get_issue_labels(issue_ids: List[str], snowflake_token: Optional[str] = None) -> Dict[str, List[str]]:
132134
"""Get labels for given issue IDs from Snowflake"""
133135
if not issue_ids:
134136
return {}
135-
137+
136138
labels_data = {}
137-
139+
138140
try:
139141
# Sanitize and validate issue IDs (should be numeric)
140142
sanitized_ids = []
141143
for issue_id in issue_ids:
142144
# Ensure issue IDs are numeric to prevent injection
143145
if isinstance(issue_id, (str, int)) and str(issue_id).isdigit():
144146
sanitized_ids.append(str(issue_id))
145-
147+
146148
if not sanitized_ids:
147149
return {}
148-
150+
149151
# Create comma-separated list for IN clause
150152
ids_str = "'" + "','".join(sanitized_ids) + "'"
151-
153+
152154
sql = f"""
153-
SELECT ISSUE, LABEL
154-
FROM JIRA_LABEL_RHAI
155+
SELECT ISSUE, LABEL
156+
FROM JIRA_LABEL_RHAI
155157
WHERE ISSUE IN ({ids_str}) AND LABEL IS NOT NULL
156158
"""
157-
159+
158160
rows = await execute_snowflake_query(sql, snowflake_token)
159161
columns = ["ISSUE", "LABEL"]
160-
162+
161163
for row in rows:
162164
row_dict = format_snowflake_row(row, columns)
163165
issue_id = str(row_dict.get("ISSUE"))
164166
label = row_dict.get("LABEL")
165-
167+
166168
if issue_id and label:
167169
if issue_id not in labels_data:
168170
labels_data[issue_id] = []
169171
labels_data[issue_id].append(label)
170-
172+
171173
except Exception as e:
172174
logger.error(f"Error fetching labels: {str(e)}")
173-
175+
174176
return labels_data
175177

178+
176179
async def get_issue_comments(issue_ids: List[str], snowflake_token: Optional[str] = None) -> Dict[str, List[Dict[str, Any]]]:
177180
"""Get comments for given issue IDs from Snowflake"""
178181
if not issue_ids:
179182
return {}
180-
183+
181184
comments_data = {}
182-
185+
183186
try:
184187
# Sanitize and validate issue IDs (should be numeric)
185188
sanitized_ids = []
186189
for issue_id in issue_ids:
187190
# Ensure issue IDs are numeric to prevent injection
188191
if isinstance(issue_id, (str, int)) and str(issue_id).isdigit():
189192
sanitized_ids.append(str(issue_id))
190-
193+
191194
if not sanitized_ids:
192195
return {}
193-
196+
194197
# Create comma-separated list for IN clause
195198
ids_str = "'" + "','".join(sanitized_ids) + "'"
196-
199+
197200
sql = f"""
198201
SELECT ID, ISSUEID, ROLELEVEL, BODY, CREATED, UPDATED
199-
FROM JIRA_COMMENT_NON_PII
202+
FROM JIRA_COMMENT_NON_PII
200203
WHERE ISSUEID IN ({ids_str}) AND BODY IS NOT NULL
201204
ORDER BY ISSUEID, CREATED ASC
202205
"""
203-
206+
204207
rows = await execute_snowflake_query(sql, snowflake_token)
205208
columns = ["ID", "ISSUEID", "ROLELEVEL", "BODY", "CREATED", "UPDATED"]
206-
209+
207210
for row in rows:
208211
row_dict = format_snowflake_row(row, columns)
209212
issue_id = str(row_dict.get("ISSUEID"))
210-
213+
211214
if issue_id:
212215
if issue_id not in comments_data:
213216
comments_data[issue_id] = []
214-
217+
215218
comment = {
216219
"id": row_dict.get("ID"),
217220
"role_level": row_dict.get("ROLELEVEL"),
@@ -220,8 +223,8 @@ async def get_issue_comments(issue_ids: List[str], snowflake_token: Optional[str
220223
"updated": row_dict.get("UPDATED")
221224
}
222225
comments_data[issue_id].append(comment)
223-
226+
224227
except Exception as e:
225228
logger.error(f"Error fetching comments: {str(e)}")
226-
227-
return comments_data
229+
230+
return comments_data

src/mcp_server.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,18 @@
1717
# Get logger
1818
logger = logging.getLogger(__name__)
1919

20+
2021
def main():
2122
"""Main entry point for the MCP server"""
2223
# Initialize FastMCP server
2324
mcp = FastMCP("jira-mcp-snowflake")
24-
25+
2526
# Register all tools
2627
register_tools(mcp)
27-
28+
2829
# Start metrics server in background thread if enabled
2930
start_metrics_thread()
30-
31+
3132
# Run the MCP server
3233
try:
3334
logger.info("Starting JIRA MCP Server for Snowflake")
@@ -42,5 +43,6 @@ def main():
4243
set_active_connections(0)
4344
logger.info("MCP server shutdown complete")
4445

46+
4547
if __name__ == "__main__":
4648
main()

0 commit comments

Comments
 (0)