dhisana 0.0.1.dev218__py3-none-any.whl → 0.0.1.dev219__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.
dhisana/schemas/sales.py CHANGED
@@ -1,3 +1,5 @@
1
+ import json
2
+
1
3
  from uuid import UUID
2
4
  from pydantic import BaseModel, Field, field_validator
3
5
  from typing import List, Optional, Dict, Any
@@ -10,14 +12,20 @@ from typing import Optional, List, Dict, Literal
10
12
  # -----------------------------
11
13
 
12
14
  class Lead(BaseModel):
13
- id: Optional[str] = None
15
+ id: Optional[UUID] = None
14
16
  full_name: Optional[str] = None
15
17
  first_name: Optional[str] = None
16
18
  last_name: Optional[str] = None
17
19
  email: Optional[str] = None
18
20
  user_linkedin_url: Optional[str] = None
19
21
  user_linkedin_salesnav_url: Optional[str] = None
22
+ organization_linkedin_url: Optional[str] = None
23
+ organization_linkedin_salesnav_url: Optional[str] = None
24
+ linkedin_follower_count: Optional[int] = None
20
25
  primary_domain_of_organization: Optional[str] = None
26
+ twitter_handle: Optional[str] = None
27
+ twitch_handle: Optional[str] = None
28
+ github_handle: Optional[str] = None
21
29
  job_title: Optional[str] = None
22
30
  phone: Optional[str] = None
23
31
  headline: Optional[str] = None
@@ -25,36 +33,68 @@ class Lead(BaseModel):
25
33
  organization_name: Optional[str] = None
26
34
  organization_website: Optional[str] = None
27
35
  summary_about_lead: Optional[str] = None
36
+
37
+ qualification_score: Optional[float] = None
38
+ qualification_reason: Optional[str] = None
39
+ revenue: Optional[str] = None
40
+ company_size: Optional[str] = None
41
+ industry: Optional[str] = None
42
+
43
+ keywords: Optional[Any] = None
44
+ tags: List[str] = []
45
+ notes: List[str] = []
46
+ additional_properties: Optional[Dict[str, Any]] = {}
28
47
  workflow_stage: Optional[str] = None
29
- assigned_to: Optional[str] = None
30
- engaged: Optional[bool] = None
48
+
49
+ engaged: bool = False
31
50
  last_contact: Optional[int] = None
32
- additional_properties: Optional[Dict[str, Any]] = None
33
51
  research_summary: Optional[str] = None
34
- task_ids: Optional[List[str]] = None
35
- email_validation_status: Optional[
36
- Literal["not_started", "in_progress", "valid", "invalid"]
37
- ] = None
38
- linkedin_validation_status: Optional[
39
- Literal["not_started", "in_progress", "valid", "invalid"]
40
- ] = None
41
- research_status: Optional[
42
- Literal["not_started", "in_progress", "done", "failed"]
43
- ] = None
44
- enchrichment_status: Optional[
45
- Literal["not_started", "in_progress", "done", "failed"]
46
- ] = None
47
-
48
- @field_validator(
49
- "linkedin_validation_status",
50
- "email_validation_status",
51
- "research_status",
52
- "enchrichment_status",
53
- mode="before"
54
- )
52
+ email_validation_status: Optional[str] = None
53
+ linkedin_validation_status: Optional[str] = None
54
+ research_status: Optional[str] = None
55
+ enchrichment_status: Optional[str] = None
56
+
57
+
58
+ @field_validator("linkedin_follower_count", mode="before")
59
+ @classmethod
60
+ def parse_linkedin_follower_count(cls, v):
61
+ if v is None or v == "":
62
+ return None
63
+ if isinstance(v, str):
64
+ v = v.strip()
65
+ if v == "":
66
+ return None
67
+ try:
68
+ return int(v)
69
+ except ValueError:
70
+ raise ValueError("linkedin_follower_count must be an integer")
71
+ return v
72
+
73
+ @field_validator("notes", mode="before")
55
74
  @classmethod
56
- def empty_string_to_none(cls, v):
57
- return None if v == "" else v
75
+ def ensure_notes_list(cls, v):
76
+ """Coerce notes to a list of strings.
77
+ Handles legacy cases where the DB may contain a scalar or JSON string.
78
+ """
79
+ if v is None:
80
+ return []
81
+ if isinstance(v, list):
82
+ # Ensure all elements are strings
83
+ return [str(item) if not isinstance(item, str) else item for item in v]
84
+ if isinstance(v, str):
85
+ # Try to parse JSON array; if not, wrap as single-note list
86
+ try:
87
+ parsed = json.loads(v)
88
+ if isinstance(parsed, list):
89
+ return [str(item) if not isinstance(item, str) else item for item in parsed]
90
+ except Exception:
91
+ pass
92
+ return [v]
93
+ # Fallback: wrap any other scalar/object as a single string entry
94
+ try:
95
+ return [json.dumps(v)]
96
+ except Exception:
97
+ return [str(v)]
58
98
 
59
99
 
60
100
  class LeadList(BaseModel):
@@ -2,6 +2,7 @@ import json
2
2
  import logging
3
3
  import asyncio
4
4
  import os
5
+ from datetime import datetime, timedelta, timezone
5
6
  from typing import Awaitable, Callable, Dict, List, Any, Optional
6
7
  import requests
7
8
 
@@ -420,6 +421,112 @@ async def test_sendgrid(api_key: str) -> Dict[str, Any]:
420
421
  return {"success": False, "status_code": 0, "error_message": str(e)}
421
422
 
422
423
 
424
+ async def test_samgov(api_key: str) -> Dict[str, Any]:
425
+ """Test SAM.gov connectivity by fetching a single opportunity."""
426
+
427
+ url = "https://api.sam.gov/opportunities/v2/search"
428
+ now = datetime.now(timezone.utc)
429
+ posted_to = now.strftime("%Y-%m-%dT%H:%M:%SZ")
430
+ posted_from = (now - timedelta(days=7)).strftime("%Y-%m-%dT%H:%M:%SZ")
431
+
432
+ params = {
433
+ "limit": 1,
434
+ "offset": 0,
435
+ "keyword": "software",
436
+ "status": "active",
437
+ "includeCount": "true",
438
+ "postedFrom": posted_from,
439
+ "postedTo": posted_to,
440
+ "api_key": api_key,
441
+ }
442
+
443
+ try:
444
+ async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
445
+
446
+ async def perform(request_params: Dict[str, Any]):
447
+ async with session.get(url, params=request_params) as response:
448
+ status = response.status
449
+ body_text = await response.text()
450
+ data: Optional[Dict[str, Any]] = None
451
+
452
+ try:
453
+ parsed = json.loads(body_text)
454
+ if isinstance(parsed, dict):
455
+ data = parsed
456
+ except json.JSONDecodeError:
457
+ data = None
458
+
459
+ return status, data, body_text
460
+
461
+ status, data, body_text = await perform(params)
462
+
463
+ def extract_error_message(payload: Optional[Dict[str, Any]], fallback_text: str) -> Optional[str]:
464
+ if not payload:
465
+ return fallback_text[:200] if fallback_text else None
466
+
467
+ errors = payload.get("errors") or payload.get("error")
468
+ if isinstance(errors, list):
469
+ parts = [
470
+ err.get("message") if isinstance(err, dict) else str(err)
471
+ for err in errors
472
+ if err
473
+ ]
474
+ return "; ".join(parts) if parts else fallback_text[:200]
475
+ if isinstance(errors, dict):
476
+ return errors.get("message") or str(errors)
477
+ if errors:
478
+ return str(errors)
479
+
480
+ for key in ("message", "errorMessage", "detail", "description"):
481
+ if key in payload and payload[key]:
482
+ return str(payload[key])
483
+
484
+ return fallback_text[:200] if fallback_text else None
485
+
486
+ error_message = extract_error_message(data, body_text)
487
+
488
+ if status == 400 and error_message and "Invalid Date Entered" in error_message:
489
+ fallback_params = dict(params)
490
+ fallback_params["postedFrom"] = (now - timedelta(days=7)).strftime("%m/%d/%Y")
491
+ fallback_params["postedTo"] = now.strftime("%m/%d/%Y")
492
+ status, data, body_text = await perform(fallback_params)
493
+ error_message = extract_error_message(data, body_text)
494
+
495
+ if status != 200:
496
+ return {
497
+ "success": False,
498
+ "status_code": status,
499
+ "error_message": error_message or f"SAM.gov non-200: {status}",
500
+ }
501
+
502
+ if not data:
503
+ return {
504
+ "success": False,
505
+ "status_code": status,
506
+ "error_message": "SAM.gov returned invalid JSON response.",
507
+ }
508
+
509
+ if data.get("errors"):
510
+ return {
511
+ "success": False,
512
+ "status_code": status,
513
+ "error_message": extract_error_message(data, body_text) or "SAM.gov reported errors.",
514
+ }
515
+
516
+ if data.get("opportunitiesData") or data.get("totalRecords") is not None:
517
+ return {"success": True, "status_code": status, "error_message": None}
518
+
519
+ return {
520
+ "success": False,
521
+ "status_code": status,
522
+ "error_message": "Unexpected SAM.gov response payload.",
523
+ }
524
+
525
+ except Exception as e:
526
+ logger.error(f"SAM.gov test failed: {e}")
527
+ return {"success": False, "status_code": 0, "error_message": str(e)}
528
+
529
+
423
530
  async def test_salesforce(
424
531
  username: str,
425
532
  password: str,
@@ -1086,8 +1193,9 @@ async def test_connectivity(tool_config: List[Dict[str, Any]]) -> Dict[str, Dict
1086
1193
  "clay": test_clay,
1087
1194
  "mcpServer": test_mcp_server,
1088
1195
  "slack": test_slack,
1089
- "mailgun": test_mailgun,
1196
+ "mailgun": test_mailgun,
1090
1197
  "sendgrid": test_sendgrid,
1198
+ "samgov": test_samgov,
1091
1199
  }
1092
1200
 
1093
1201
  results: Dict[str, Dict[str, Any]] = {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dhisana
3
- Version: 0.0.1.dev218
3
+ Version: 0.0.1.dev219
4
4
  Summary: A Python SDK for Dhisana AI Platform
5
5
  Home-page: https://github.com/dhisana-ai/dhisana-python-sdk
6
6
  Author: Admin
@@ -6,7 +6,7 @@ dhisana/cli/models.py,sha256=IzUFZW_X32mL3fpM1_j4q8AF7v5nrxJcxBoqvG-TTgA,706
6
6
  dhisana/cli/predictions.py,sha256=VYgoLK1Ksv6MFImoYZqjQJkds7e5Hso65dHwbxTNNzE,646
7
7
  dhisana/schemas/__init__.py,sha256=jv2YF__bseklT3OWEzlqJ5qE24c4aWd5F4r0TTjOrWQ,65
8
8
  dhisana/schemas/common.py,sha256=WErBbyZAQtkmSOXrMkamJkMptj3uCY1S2CGHJWkB_Ps,9103
9
- dhisana/schemas/sales.py,sha256=CnBi6_YZwqfcd_vDERoMoW8QAa1yfkcOWbFHVr1UcPA,31936
9
+ dhisana/schemas/sales.py,sha256=2fDcjGJEAXi7bOkOmdJ14lqt3pdy3GWA28PFn6yMYQE,33354
10
10
  dhisana/ui/__init__.py,sha256=jv2YF__bseklT3OWEzlqJ5qE24c4aWd5F4r0TTjOrWQ,65
11
11
  dhisana/ui/components.py,sha256=4NXrAyl9tx2wWwoVYyABO-EOGnreGMvql1AkXWajIIo,14316
12
12
  dhisana/utils/__init__.py,sha256=jv2YF__bseklT3OWEzlqJ5qE24c4aWd5F4r0TTjOrWQ,65
@@ -78,7 +78,7 @@ dhisana/utils/serperdev_google_jobs.py,sha256=m5_2f_5y79FOFZz1A_go6m0hIUfbbAoZ0Y
78
78
  dhisana/utils/serperdev_local_business.py,sha256=JoZfTg58Hojv61cyuwA2lcnPdLT1lawnWaBNrUYWnuQ,6447
79
79
  dhisana/utils/serperdev_search.py,sha256=_iBKIfHMq4gFv5StYz58eArriygoi1zW6VnLlux8vto,9363
80
80
  dhisana/utils/smtp_email_tools.py,sha256=kx0VSa0JQyVLD020oARCHhOBZJxDwMIri1Z7jPN0nP4,15843
81
- dhisana/utils/test_connect.py,sha256=-BL0BfHsKuD9JMAfKzyWyLolcZomYlKQWYdkQC8TOn8,53970
81
+ dhisana/utils/test_connect.py,sha256=1rupW6dIfRzBKUH1AcWqfhYWhW-wETD475AJlCbgvFw,58254
82
82
  dhisana/utils/trasform_json.py,sha256=s48DoyzVVCI4dTvSUwF5X-exX6VH6nPWrjbUENkYuNE,6979
83
83
  dhisana/utils/web_download_parse_tools.py,sha256=ouXwH7CmjcRjoBfP5BWat86MvcGO-8rLCmWQe_eZKjc,7810
84
84
  dhisana/utils/workflow_code_model.py,sha256=YPWse5vBb3O6Km2PvKh1Q3AB8qBkzLt1CrR5xOL9Mro,99
@@ -92,8 +92,8 @@ dhisana/workflow/agent.py,sha256=esv7_i_XuMkV2j1nz_UlsHov_m6X5WZZiZm_tG4OBHU,565
92
92
  dhisana/workflow/flow.py,sha256=xWE3qQbM7j2B3FH8XnY3zOL_QXX4LbTW4ArndnEYJE0,1638
93
93
  dhisana/workflow/task.py,sha256=HlWz9mtrwLYByoSnePOemBUBrMEcj7KbgNjEE1oF5wo,1830
94
94
  dhisana/workflow/test.py,sha256=kwW8jWqSBNcRmoyaxlTuZCMOpGJpTbJQgHI7gSjwdzM,3399
95
- dhisana-0.0.1.dev218.dist-info/METADATA,sha256=mF4K1TD6zfcNFZCXN1-YvFesMEKFO4txQyBOmmRb_v4,1190
96
- dhisana-0.0.1.dev218.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
97
- dhisana-0.0.1.dev218.dist-info/entry_points.txt,sha256=jujxteZmNI9EkEaK-pOCoWuBujU8TCevdkfl9ZcKHek,49
98
- dhisana-0.0.1.dev218.dist-info/top_level.txt,sha256=NETTHt6YifG_P7XtRHbQiXZlgSFk9Qh9aR-ng1XTf4s,8
99
- dhisana-0.0.1.dev218.dist-info/RECORD,,
95
+ dhisana-0.0.1.dev219.dist-info/METADATA,sha256=T-dWWhSYokCE-P7SB5slEkbuuHfXlpm7lrWGRXiSaV0,1190
96
+ dhisana-0.0.1.dev219.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
97
+ dhisana-0.0.1.dev219.dist-info/entry_points.txt,sha256=jujxteZmNI9EkEaK-pOCoWuBujU8TCevdkfl9ZcKHek,49
98
+ dhisana-0.0.1.dev219.dist-info/top_level.txt,sha256=NETTHt6YifG_P7XtRHbQiXZlgSFk9Qh9aR-ng1XTf4s,8
99
+ dhisana-0.0.1.dev219.dist-info/RECORD,,