main
py 253 lines 9.98 KB
Raw
1 from typing import Optional
2
3 import httpx
4 from fastapi import HTTPException
5 from loguru import logger
6
7 from app.integrations.copilot_action.schema.copilot_action import ActionDetailResponse
8 from app.integrations.copilot_action.schema.copilot_action import (
9 InventoryMetricsResponse,
10 )
11 from app.integrations.copilot_action.schema.copilot_action import InventoryResponse
12 from app.integrations.copilot_action.schema.copilot_action import Technology
13
14
15 class CopilotActionService:
16 """Service for interacting with the Copilot Action inventory API"""
17
18 BASE_URL = "https://copilot-action.socfortress.co"
19 MODULE_VERSION = "1.0.0"
20
21 @classmethod
22 async def get_inventory(
23 cls,
24 license_key: str,
25 technology: Optional[Technology] = None,
26 category: Optional[str] = None,
27 tag: Optional[str] = None,
28 q: Optional[str] = None,
29 limit: int = 100,
30 offset: int = 0,
31 refresh: bool = False,
32 include: Optional[str] = None,
33 ) -> InventoryResponse:
34 """
35 Fetch inventory from the Copilot Action service.
36
37 Args:
38 license_key: API key for authentication
39 technology: Filter by technology type
40 category: Filter by category
41 tag: Filter by tag
42 q: Free-text search query
43 limit: Maximum number of results
44 offset: Offset for pagination
45 refresh: Force refresh cache
46 include: Comma-separated extra fields to include
47
48 Returns:
49 InventoryResponse: The inventory data
50 """
51 try:
52 url = f"{cls.BASE_URL}/inventory"
53
54 headers = {"x-api-key": license_key, "module-version": cls.MODULE_VERSION, "Accept": "application/json"}
55
56 params = {}
57 if technology:
58 params["technology"] = technology.value
59 if category:
60 params["category"] = category
61 if tag:
62 params["tag"] = tag
63 if q:
64 params["q"] = q
65 if limit != 100:
66 params["limit"] = limit
67 if offset != 0:
68 params["offset"] = offset
69 if refresh:
70 params["refresh"] = "true"
71 if include:
72 params["include"] = include
73
74 logger.info(f"Fetching inventory from {url} with params: {params}")
75
76 async with httpx.AsyncClient() as client:
77 response = await client.get(url, headers=headers, params=params, timeout=30.0)
78
79 response.raise_for_status()
80
81 try:
82 data = response.json()
83 logger.debug(f"Raw API response: {data}")
84 except ValueError:
85 logger.error(f"Non-JSON response from inventory API: {response.text[:200]}")
86 return InventoryResponse(copilot_actions=[], message="Invalid response format from inventory API", success=False)
87
88 logger.info(f"Successfully fetched inventory: {len(data.get('copilot_actions', []))} actions")
89
90 # Calculate pagination metadata
91 copilot_actions = data.get("copilot_actions", [])
92 count = len(copilot_actions)
93
94 # Try to get total from API response, with fallback parsing from message
95 total = data.get("total")
96 if total is None:
97 # Try to parse total from message like "Returned 1 of 44 matching items"
98 message = data.get("message", "")
99 import re
100
101 # Try multiple patterns to be more robust
102 patterns = [
103 r"(\d+) of (\d+) matching items",
104 r"Returned (\d+) of (\d+)",
105 r"(\d+)/(\d+) items",
106 r"showing (\d+) of (\d+)",
107 ]
108
109 for pattern in patterns:
110 match = re.search(pattern, message, re.IGNORECASE)
111 if match:
112 total = int(match.group(2))
113 logger.info(f"Parsed total from message using pattern '{pattern}': {total}")
114 break
115 else:
116 # Fallback to count if we can't parse
117 total = count
118 logger.warning(f"Could not determine total count from message '{message}', using current count: {count}")
119
120 has_more = (offset + count) < total
121 next_offset = offset + limit if has_more else None
122 prev_offset = max(0, offset - limit) if offset > 0 else None
123
124 return InventoryResponse(
125 copilot_actions=copilot_actions,
126 message=data.get("message", "Successfully fetched inventory"),
127 success=data.get("success", True),
128 total=total,
129 count=count,
130 limit=limit,
131 offset=offset,
132 has_more=has_more,
133 next_offset=next_offset,
134 prev_offset=prev_offset,
135 )
136
137 except httpx.HTTPError as e:
138 logger.error(f"HTTP error fetching inventory: {str(e)}")
139 return InventoryResponse(
140 copilot_actions=[],
141 message=f"HTTP error fetching inventory: {str(e)}",
142 success=False,
143 total=0,
144 count=0,
145 limit=limit,
146 offset=offset,
147 has_more=False,
148 )
149 except Exception as e:
150 logger.error(f"Unexpected error fetching inventory: {str(e)}")
151 return InventoryResponse(
152 copilot_actions=[],
153 message=f"Unexpected error: {str(e)}",
154 success=False,
155 total=0,
156 count=0,
157 limit=limit,
158 offset=offset,
159 has_more=False,
160 )
161
162 @classmethod
163 async def get_action_by_name(cls, license_key: str, copilot_action_name: str) -> ActionDetailResponse:
164 """
165 Fetch details for a specific action by name.
166
167 Args:
168 license_key: API key for authentication
169 copilot_action_name: Name of the action to fetch
170
171 Returns:
172 ActionDetailResponse: The action details
173 """
174 try:
175 url = f"{cls.BASE_URL}/inventory/{copilot_action_name}"
176
177 headers = {"x-api-key": license_key, "module-version": cls.MODULE_VERSION, "Accept": "application/json"}
178
179 logger.info(f"Fetching action details for: {copilot_action_name}")
180
181 async with httpx.AsyncClient() as client:
182 response = await client.get(url, headers=headers, timeout=30.0)
183
184 response.raise_for_status()
185
186 try:
187 data = response.json()
188 logger.debug(f"Action detail response data: {data}")
189 except ValueError:
190 logger.error(f"Non-JSON response from action API: {response.text[:200]}")
191 return ActionDetailResponse(copilot_action=None, message="Invalid response format from action API", success=False)
192
193 logger.info(f"Successfully fetched action details for: {copilot_action_name}")
194 return ActionDetailResponse(**data)
195
196 except httpx.HTTPStatusError as e:
197 if e.response.status_code == 404:
198 logger.warning(f"Action not found: {copilot_action_name}")
199 raise HTTPException(status_code=404, detail=f"Action '{copilot_action_name}' not found")
200 logger.error(f"HTTP error fetching action details: {str(e)}")
201 return ActionDetailResponse(copilot_action=None, message=f"HTTP error fetching action details: {str(e)}", success=False)
202 except Exception as e:
203 logger.error(f"Unexpected error fetching action details: {str(e)}")
204 return ActionDetailResponse(copilot_action=None, message=f"Unexpected error: {str(e)}", success=False)
205
206 @classmethod
207 async def get_metrics(cls, license_key: str) -> InventoryMetricsResponse:
208 """
209 Fetch inventory metrics.
210
211 Args:
212 license_key: API key for authentication
213
214 Returns:
215 InventoryMetricsResponse: The metrics data
216 """
217 try:
218 url = f"{cls.BASE_URL}/metrics"
219
220 headers = {"x-api-key": license_key, "module-version": cls.MODULE_VERSION, "Accept": "application/json"}
221
222 logger.info("Fetching inventory metrics")
223
224 async with httpx.AsyncClient() as client:
225 response = await client.get(url, headers=headers, timeout=30.0)
226
227 response.raise_for_status()
228
229 try:
230 data = response.json()
231 except ValueError:
232 logger.error(f"Non-JSON response from metrics API: {response.text[:200]}")
233 return InventoryMetricsResponse(
234 status="error",
235 metrics={},
236 message="Invalid response format from metrics API",
237 success=False,
238 )
239
240 logger.info("Successfully fetched inventory metrics")
241 return InventoryMetricsResponse(
242 status=data.get("status", "unknown"),
243 metrics=data.get("metrics", {}),
244 message="Successfully retrieved inventory metrics",
245 success=True,
246 )
247
248 except httpx.HTTPError as e:
249 logger.error(f"HTTP error fetching metrics: {str(e)}")
250 return InventoryMetricsResponse(status="error", metrics={}, message=f"HTTP error fetching metrics: {str(e)}", success=False)
251 except Exception as e:
252 logger.error(f"Unexpected error fetching metrics: {str(e)}")
253 return InventoryMetricsResponse(status="error", metrics={}, message=f"Unexpected error: {str(e)}", success=False)