@cryptotaxi247 / CoPilot / commits / 677c0e54

Implement organizations service and API routes; refactor Singul integration to use dynamic client creation (#458)

taylor_socfortress committed Jun 18, 2025 at 14:17 UTC 677c0e5442dff92c67368fdaecb15bb3d885ff67
6 files changed +392 -18
backend/app/connectors/shuffle/routes/organizations.py new
+128
@@ -0,0 +1,128 @@
1 +from fastapi import APIRouter
2 +from fastapi import Depends
3 +from fastapi import HTTPException
4 +from fastapi import Query
5 +from loguru import logger
6 +
7 +from app.auth.utils import AuthHandler
8 +from app.connectors.shuffle.schema.organizations import OrganizationResponse
9 +from app.connectors.shuffle.schema.organizations import OrganizationsListResponse
10 +from app.connectors.shuffle.services.organizations import OrganizationsService
11 +
12 +# Router Configuration
13 +shuffle_organizations_router = APIRouter()
14 +
15 +# Auth handler
16 +auth_handler = AuthHandler()
17 +
18 +
19 +@shuffle_organizations_router.get(
20 + "/organizations",
21 + response_model=OrganizationsListResponse,
22 + description="Retrieve all organizations from Shuffle",
23 + dependencies=[Depends(auth_handler.require_any_scope("admin", "analyst"))],
24 +)
25 +async def list_organizations(
26 + connector_name: str = Query("Shuffle", description="Name of the Shuffle connector to use")
27 +):
28 + """
29 + Retrieve all organizations from Shuffle.
30 +
31 + Args:
32 + connector_name (str): Name of the Shuffle connector to use.
33 +
34 + Returns:
35 + OrganizationsListResponse: List of all organizations.
36 + """
37 + logger.info(f"Request to list all organizations using connector: {connector_name}")
38 +
39 + try:
40 + organizations = await OrganizationsService.list_organizations(connector_name)
41 + logger.info(f"Successfully retrieved {organizations.total_count} organizations")
42 + return organizations
43 + except HTTPException:
44 + raise
45 + except Exception as e:
46 + logger.error(f"Unexpected error in list_organizations endpoint: {e}")
47 + raise HTTPException(
48 + status_code=500,
49 + detail=f"Internal server error: {str(e)}"
50 + )
51 +
52 +
53 +@shuffle_organizations_router.get(
54 + "/organizations/{org_id}",
55 + response_model=OrganizationResponse,
56 + description="Retrieve a specific organization by ID",
57 + dependencies=[Depends(auth_handler.require_any_scope("admin", "analyst"))],
58 +)
59 +async def get_organization_by_id(
60 + org_id: str,
61 + connector_name: str = Query("Shuffle", description="Name of the Shuffle connector to use")
62 +):
63 + """
64 + Retrieve a specific organization by ID.
65 +
66 + Args:
67 + org_id (str): The organization ID to retrieve.
68 + connector_name (str): Name of the Shuffle connector to use.
69 +
70 + Returns:
71 + OrganizationResponse: The organization data.
72 + """
73 + logger.info(f"Request to get organization with ID: {org_id} using connector: {connector_name}")
74 +
75 + try:
76 + organization = await OrganizationsService.get_organization_by_id(org_id, connector_name)
77 + return OrganizationResponse(
78 + success=True,
79 + message=f"Successfully retrieved organization: {organization.name}",
80 + data=organization
81 + )
82 + except HTTPException:
83 + raise
84 + except Exception as e:
85 + logger.error(f"Unexpected error in get_organization_by_id endpoint: {e}")
86 + raise HTTPException(
87 + status_code=500,
88 + detail=f"Internal server error: {str(e)}"
89 + )
90 +
91 +
92 +@shuffle_organizations_router.get(
93 + "/organizations/name/{org_name}",
94 + response_model=OrganizationResponse,
95 + description="Retrieve a specific organization by name",
96 + dependencies=[Depends(auth_handler.require_any_scope("admin", "analyst"))],
97 +)
98 +async def get_organization_by_name(
99 + org_name: str,
100 + connector_name: str = Query("Shuffle", description="Name of the Shuffle connector to use")
101 +):
102 + """
103 + Retrieve a specific organization by name.
104 +
105 + Args:
106 + org_name (str): The organization name to retrieve.
107 + connector_name (str): Name of the Shuffle connector to use.
108 +
109 + Returns:
110 + OrganizationResponse: The organization data.
111 + """
112 + logger.info(f"Request to get organization with name: {org_name} using connector: {connector_name}")
113 +
114 + try:
115 + organization = await OrganizationsService.get_organization_by_name(org_name, connector_name)
116 + return OrganizationResponse(
117 + success=True,
118 + message=f"Successfully retrieved organization: {organization.name}",
119 + data=organization
120 + )
121 + except HTTPException:
122 + raise
123 + except Exception as e:
124 + logger.error(f"Unexpected error in get_organization_by_name endpoint: {e}")
125 + raise HTTPException(
126 + status_code=500,
127 + detail=f"Internal server error: {str(e)}"
128 + )
backend/app/connectors/shuffle/schema/organizations.py new
+68
@@ -0,0 +1,68 @@
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +
6 +from pydantic import BaseModel
7 +from pydantic import Field
8 +
9 +
10 +class SyncConfig(BaseModel):
11 + interval: int = 0
12 + api_key: str = ""
13 + source: str = ""
14 +
15 +
16 +class PartnerInfo(BaseModel):
17 + reseller: bool = False
18 + reseller_level: str = ""
19 +
20 +
21 +class SSOConfig(BaseModel):
22 + sso_entrypoint: str = ""
23 + sso_certificate: str = ""
24 + client_id: str = ""
25 + client_secret: str = ""
26 + openid_authorization: str = ""
27 + openid_token: str = ""
28 +
29 +
30 +class Organization(BaseModel):
31 + name: str
32 + description: str
33 + company_type: str = ""
34 + image: str = ""
35 + id: str
36 + org: str
37 + users: List[Any] = []
38 + role: str = ""
39 + roles: List[str] = []
40 + active_apps: List[str] = []
41 + cloud_sync: bool = False
42 + cloud_sync_active: bool = True
43 + sync_config: SyncConfig = Field(default_factory=SyncConfig)
44 + sync_features: Dict[str, Any] = Field(default_factory=dict)
45 + invites: Optional[Any] = None
46 + child_orgs: List[str] = []
47 + manager_orgs: Optional[List[str]] = None
48 + creator_org: Optional[str] = None
49 + disabled: bool = False
50 + partner_info: PartnerInfo = Field(default_factory=PartnerInfo)
51 + sso_config: SSOConfig = Field(default_factory=SSOConfig)
52 + main_priority: str = ""
53 + region: str = ""
54 + region_url: str = ""
55 + tutorials: List[Any] = []
56 +
57 +
58 +class OrganizationsListResponse(BaseModel):
59 + success: bool
60 + message: str
61 + data: List[Organization]
62 + total_count: int = Field(description="Total number of organizations")
63 +
64 +
65 +class OrganizationResponse(BaseModel):
66 + success: bool
67 + message: str
68 + data: Optional[Organization] = None
backend/app/connectors/shuffle/services/organizations.py new
+135
@@ -0,0 +1,135 @@
1 +from typing import Dict
2 +from typing import List
3 +
4 +from fastapi import HTTPException
5 +from loguru import logger
6 +
7 +from app.connectors.shuffle.schema.organizations import Organization
8 +from app.connectors.shuffle.schema.organizations import OrganizationsListResponse
9 +from app.connectors.shuffle.utils.universal import send_get_request
10 +
11 +
12 +class OrganizationsService:
13 + """Service class for handling Shuffle Organizations operations."""
14 +
15 + @staticmethod
16 + async def list_organizations(connector_name: str = "Shuffle") -> OrganizationsListResponse:
17 + """
18 + List all organizations from Shuffle.
19 +
20 + Args:
21 + connector_name (str, optional): The name of the connector to use. Defaults to "Shuffle".
22 +
23 + Returns:
24 + OrganizationsListResponse: Response containing list of organizations.
25 + """
26 + logger.info("Fetching organizations from Shuffle")
27 +
28 + try:
29 + # Send GET request to Shuffle API
30 + response = await send_get_request(
31 + endpoint="/api/v1/orgs",
32 + connector_name=connector_name
33 + )
34 +
35 + if not response.get("success", False):
36 + logger.error(f"Failed to fetch organizations: {response.get('message', 'Unknown error')}")
37 + raise HTTPException(
38 + status_code=500,
39 + detail=f"Failed to fetch organizations: {response.get('message', 'Unknown error')}"
40 + )
41 +
42 + # Parse the response data
43 + organizations_data = response.get("data", [])
44 +
45 + if not isinstance(organizations_data, list):
46 + logger.error("Invalid response format: expected list of organizations")
47 + raise HTTPException(
48 + status_code=500,
49 + detail="Invalid response format from Shuffle API"
50 + )
51 +
52 + # Convert to Organization models
53 + organizations = []
54 + for org_data in organizations_data:
55 + try:
56 + organization = Organization(**org_data)
57 + organizations.append(organization)
58 + except Exception as e:
59 + logger.warning(f"Failed to parse organization data: {org_data}. Error: {e}")
60 + continue
61 +
62 + logger.info(f"Successfully fetched {len(organizations)} organizations")
63 +
64 + return OrganizationsListResponse(
65 + success=True,
66 + message=f"Successfully retrieved {len(organizations)} organizations",
67 + data=organizations,
68 + total_count=len(organizations)
69 + )
70 +
71 + except HTTPException:
72 + # Re-raise HTTPExceptions as-is
73 + raise
74 + except Exception as e:
75 + logger.error(f"Unexpected error while fetching organizations: {e}")
76 + raise HTTPException(
77 + status_code=500,
78 + detail=f"Unexpected error while fetching organizations: {str(e)}"
79 + )
80 +
81 + @staticmethod
82 + async def get_organization_by_id(org_id: str, connector_name: str = "Shuffle") -> Organization:
83 + """
84 + Get a specific organization by ID.
85 +
86 + Args:
87 + org_id (str): The organization ID to retrieve.
88 + connector_name (str, optional): The name of the connector to use. Defaults to "Shuffle".
89 +
90 + Returns:
91 + Organization: The organization data.
92 + """
93 + logger.info(f"Fetching organization with ID: {org_id}")
94 +
95 + # Get all organizations and filter by ID
96 + organizations_response = await OrganizationsService.list_organizations(connector_name)
97 +
98 + for org in organizations_response.data:
99 + if org.id == org_id:
100 + logger.info(f"Found organization: {org.name}")
101 + return org
102 +
103 + logger.error(f"Organization with ID {org_id} not found")
104 + raise HTTPException(
105 + status_code=404,
106 + detail=f"Organization with ID {org_id} not found"
107 + )
108 +
109 + @staticmethod
110 + async def get_organization_by_name(org_name: str, connector_name: str = "Shuffle") -> Organization:
111 + """
112 + Get a specific organization by name.
113 +
114 + Args:
115 + org_name (str): The organization name to retrieve.
116 + connector_name (str, optional): The name of the connector to use. Defaults to "Shuffle".
117 +
118 + Returns:
119 + Organization: The organization data.
120 + """
121 + logger.info(f"Fetching organization with name: {org_name}")
122 +
123 + # Get all organizations and filter by name
124 + organizations_response = await OrganizationsService.list_organizations(connector_name)
125 +
126 + for org in organizations_response.data:
127 + if org.name.lower() == org_name.lower():
128 + logger.info(f"Found organization: {org.name} (ID: {org.id})")
129 + return org
130 +
131 + logger.error(f"Organization with name '{org_name}' not found")
132 + raise HTTPException(
133 + status_code=404,
134 + detail=f"Organization with name '{org_name}' not found"
135 + )
backend/app/connectors/shuffle/services/singul.py
+26 -18
@@ -1,9 +1,7 @@
1 from loguru import logger
2 -from shufflepy import Singul
2
3 from app.connectors.shuffle.schema.singul import SingulRequest
5 -
6 -singul = Singul(auth="REPLACE_ME", url="https://california.shuffler.io")
4 +from app.connectors.shuffle.utils.universal import get_singul_client
5
6
7 async def execute_singul(
@@ -13,23 +11,33 @@ async def execute_singul(
11 Execute a Singul integration.
12
13 Args:
16 - request (IntegrationRequest): The request object containing the workflow ID.
14 + request (SingulRequest): The request object containing the workflow ID.
15
16 Returns:
17 dict: The response containing the execution ID.
18 """
19 logger.info("Executing Singul integration")
22 - response = singul.communication.send_message(
23 - app=request.app,
24 - fields=[
25 - {"key": "to", "value": "walton.taylor23@gmail.com"},
26 - {"key": "subject", "value": "Test Email from Singul"},
27 - {"key": "body", "value": "This is a test email sent from Singul."},
28 - ],
29 - )
30 - logger.info(f"Singul response: {response}")
31 - logger.info(f"Singul response: {response.get('success', 'unknown')}")
32 - return {
33 - "executionId": response.get("id", "unknown"),
34 - "message": "Singul integration executed successfully",
35 - }
20 +
21 + # Get Singul client from database credentials
22 + singul = await get_singul_client()
23 +
24 + try:
25 + response = singul.communication.send_message(
26 + app=request.app,
27 + org_id=request.org_id,
28 + fields=[
29 + {"key": "to", "value": "walton.taylor23@gmail.com"},
30 + {"key": "subject", "value": "Test Email from Singul"},
31 + {"key": "body", "value": "This is a test email sent from Singul."},
32 + ],
33 + )
34 + logger.info(f"Singul response: {response}")
35 + logger.info(f"Singul response success: {response.get('success', 'unknown')}")
36 +
37 + return {
38 + "executionId": response.get("id", "unknown"),
39 + "message": "Singul integration executed successfully",
40 + }
41 + except Exception as e:
42 + logger.error(f"Failed to execute Singul integration: {e}")
43 + return {"executionId": "unknown", "message": f"Singul integration failed: {e}", "success": False}
backend/app/connectors/shuffle/utils/universal.py
+28
@@ -5,6 +5,7 @@ from typing import Optional
5 import requests
6 from fastapi import HTTPException
7 from loguru import logger
8 +from shufflepy import Singul
9
10 from app.connectors.utils import get_connector_info_from_db
11 from app.db.db_session import get_db_session
@@ -277,3 +278,30 @@ def send_put_request(
278 status_code=500,
279 detail=f"Failed to send PUT request to {endpoint} with error: {e}",
280 )
281 +
282 +
283 +async def get_singul_client(connector_name: str = "Shuffle") -> Singul:
284 + """
285 + Create and return a Singul client using database credentials.
286 +
287 + Args:
288 + connector_name (str, optional): The name of the connector to use. Defaults to "Shuffle".
289 +
290 + Returns:
291 + Singul: An initialized Singul client instance.
292 + """
293 + logger.info("Creating Singul client from database credentials")
294 + async with get_db_session() as session:
295 + attributes = await get_connector_info_from_db(connector_name, session)
296 +
297 + if attributes is None:
298 + logger.error("No Shuffle connector found in the database")
299 + raise HTTPException(status_code=404, detail="Shuffle connector not found in database")
300 +
301 + try:
302 + singul_client = Singul(auth=attributes["connector_api_key"], url=attributes["connector_url"])
303 + logger.info(f"Singul client created successfully for {attributes['connector_url']}")
304 + return singul_client
305 + except Exception as e:
306 + logger.error(f"Failed to create Singul client: {e}")
307 + raise HTTPException(status_code=500, detail=f"Failed to create Singul client: {e}")
backend/app/routers/shuffle.py
+7
@@ -1,6 +1,7 @@
1 from fastapi import APIRouter
2
3 from app.connectors.shuffle.routes.integrations import shuffle_integrations_router
4 +from app.connectors.shuffle.routes.organizations import shuffle_organizations_router
5 from app.connectors.shuffle.routes.singul import shuffle_singul_router
6 from app.connectors.shuffle.routes.workflows import shuffle_workflows_router
7
@@ -25,3 +26,9 @@ router.include_router(
26 prefix="/shuffle/singul",
27 tags=["shuffle-singul"],
28 )
29 +
30 +router.include_router(
31 + shuffle_organizations_router,
32 + prefix="/shuffle/organizations",
33 + tags=["shuffle-organizations"],
34 +)