pycdc 0.1.0__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.
- pycdc/__init__.py +8 -0
- pycdc/catalog.py +95 -0
- pycdc/cli.py +88 -0
- pycdc/client.py +148 -0
- pycdc/models.py +91 -0
- pycdc-0.1.0.dist-info/METADATA +67 -0
- pycdc-0.1.0.dist-info/RECORD +10 -0
- pycdc-0.1.0.dist-info/WHEEL +4 -0
- pycdc-0.1.0.dist-info/entry_points.txt +2 -0
- pycdc-0.1.0.dist-info/licenses/LICENSE +21 -0
pycdc/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""pycdc: Please Your Customer Direct Connect — CRM gift idea finder."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .client import GiftFinder
|
|
6
|
+
from .models import GiftIdea, Occasion, Budget, CustomerProfile
|
|
7
|
+
|
|
8
|
+
__all__ = ["GiftFinder", "GiftIdea", "Occasion", "Budget", "CustomerProfile"]
|
pycdc/catalog.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Built-in gift catalog with curated ideas by occasion and budget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .models import GiftIdea, Occasion, Budget
|
|
6
|
+
|
|
7
|
+
# Curated gift catalog
|
|
8
|
+
CATALOG: list[GiftIdea] = [
|
|
9
|
+
# Token gifts
|
|
10
|
+
GiftIdea("Handwritten Thank You Card", "Premium card with personal note", "$5-10",
|
|
11
|
+
Budget.TOKEN, Occasion.THANK_YOU, category="stationery",
|
|
12
|
+
personalization_tip="Reference a specific project or win you shared"),
|
|
13
|
+
GiftIdea("Custom Sticker Pack", "Branded fun stickers for their laptop", "$8-12",
|
|
14
|
+
Budget.TOKEN, Occasion.ONBOARDING, category="swag",
|
|
15
|
+
personalization_tip="Include their company logo alongside yours"),
|
|
16
|
+
GiftIdea("Artisan Coffee Sampler", "4-pack of single-origin coffee", "$12-15",
|
|
17
|
+
Budget.TOKEN, Occasion.REFERRAL, category="food",
|
|
18
|
+
personalization_tip="Add a note: 'Thanks a latte for the referral'"),
|
|
19
|
+
|
|
20
|
+
# Modest gifts
|
|
21
|
+
GiftIdea("Desk Plant Kit", "Low-maintenance succulent in branded pot", "$20-35",
|
|
22
|
+
Budget.MODEST, Occasion.ONBOARDING, category="office",
|
|
23
|
+
personalization_tip="Choose a plant that matches their office aesthetic"),
|
|
24
|
+
GiftIdea("Book — Their Industry", "Bestseller relevant to their field", "$15-30",
|
|
25
|
+
Budget.MODEST, Occasion.MILESTONE, category="books",
|
|
26
|
+
personalization_tip="Write a note inside the cover about why you picked it"),
|
|
27
|
+
GiftIdea("Gourmet Cookie Box", "Assorted artisan cookies, 12-pack", "$25-40",
|
|
28
|
+
Budget.MODEST, Occasion.HOLIDAY, category="food",
|
|
29
|
+
personalization_tip="Check for dietary restrictions first"),
|
|
30
|
+
GiftIdea("Charity Donation in Their Name", "Donation to a cause they care about", "$25-50",
|
|
31
|
+
Budget.MODEST, Occasion.THANK_YOU, category="charity",
|
|
32
|
+
personalization_tip="Pick a cause related to their stated values or interests"),
|
|
33
|
+
|
|
34
|
+
# Standard gifts
|
|
35
|
+
GiftIdea("Premium Bluetooth Speaker", "Compact high-quality speaker", "$60-100",
|
|
36
|
+
Budget.STANDARD, Occasion.CLOSING_DEAL, category="tech",
|
|
37
|
+
personalization_tip="Engrave their initials or a short message"),
|
|
38
|
+
GiftIdea("Wine & Cheese Hamper", "Curated selection with tasting notes", "$75-120",
|
|
39
|
+
Budget.STANDARD, Occasion.ANNIVERSARY, category="food",
|
|
40
|
+
personalization_tip="Include wines from their home region if possible"),
|
|
41
|
+
GiftIdea("Spa Gift Card", "Relaxation package at a local spa", "$80-150",
|
|
42
|
+
Budget.STANDARD, Occasion.RENEWAL, category="experience",
|
|
43
|
+
personalization_tip="'Celebrating another year of partnership — you deserve a break'"),
|
|
44
|
+
GiftIdea("Custom Illustration", "Commissioned portrait or office artwork", "$50-120",
|
|
45
|
+
Budget.STANDARD, Occasion.MILESTONE, category="art",
|
|
46
|
+
personalization_tip="Commission art of their office building or team mascot"),
|
|
47
|
+
|
|
48
|
+
# Premium gifts
|
|
49
|
+
GiftIdea("Weekend Getaway Voucher", "Hotel voucher for a weekend escape", "$200-400",
|
|
50
|
+
Budget.PREMIUM, Occasion.CLOSING_DEAL, category="experience",
|
|
51
|
+
personalization_tip="Pick a destination near their city or a place they've mentioned"),
|
|
52
|
+
GiftIdea("Noise-Cancelling Headphones", "Sony or Bose premium headphones", "$250-350",
|
|
53
|
+
Budget.PREMIUM, Occasion.RENEWAL, category="tech",
|
|
54
|
+
personalization_tip="Great for clients who travel frequently"),
|
|
55
|
+
GiftIdea("Leather Portfolio Set", "Handcrafted leather notebook + pen", "$150-300",
|
|
56
|
+
Budget.PREMIUM, Occasion.PROMOTION, category="luxury",
|
|
57
|
+
personalization_tip="Monogram with their initials"),
|
|
58
|
+
|
|
59
|
+
# VIP gifts
|
|
60
|
+
GiftIdea("Private Dining Experience", "Chef's table dinner for two", "$500-800",
|
|
61
|
+
Budget.VIP, Occasion.ANNIVERSARY, category="experience",
|
|
62
|
+
personalization_tip="Invite them personally — make it a relationship-building event"),
|
|
63
|
+
GiftIdea("Custom Watch Engraving", "Premium watch with personalized engraving", "$500-1500",
|
|
64
|
+
Budget.VIP, Occasion.MILESTONE, category="luxury",
|
|
65
|
+
personalization_tip="Engrave the date of your first deal together"),
|
|
66
|
+
GiftIdea("Team Offsite Sponsorship", "Fund a team activity for their company", "$1000+",
|
|
67
|
+
Budget.VIP, Occasion.RENEWAL, category="experience",
|
|
68
|
+
personalization_tip="Offer to cover an escape room, cooking class, or wine tasting"),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def search_catalog(*, occasion: Occasion | None = None, budget: Budget | None = None,
|
|
73
|
+
query: str = "", interests: list[str] | None = None) -> list[GiftIdea]:
|
|
74
|
+
"""Search the built-in catalog."""
|
|
75
|
+
results = list(CATALOG)
|
|
76
|
+
|
|
77
|
+
if occasion:
|
|
78
|
+
results = [g for g in results if g.occasion == occasion]
|
|
79
|
+
if budget:
|
|
80
|
+
results = [g for g in results if g.budget == budget]
|
|
81
|
+
if query:
|
|
82
|
+
q = query.lower()
|
|
83
|
+
results = [g for g in results if q in g.name.lower() or q in g.description.lower()
|
|
84
|
+
or q in g.category.lower()]
|
|
85
|
+
if interests:
|
|
86
|
+
interest_set = {i.lower() for i in interests}
|
|
87
|
+
scored = []
|
|
88
|
+
for g in results:
|
|
89
|
+
text = f"{g.name} {g.description} {g.category}".lower()
|
|
90
|
+
matches = sum(1 for i in interest_set if i in text)
|
|
91
|
+
g.score = matches / len(interest_set) if interest_set else 0
|
|
92
|
+
scored.append(g)
|
|
93
|
+
results = sorted(scored, key=lambda g: g.score, reverse=True)
|
|
94
|
+
|
|
95
|
+
return results
|
pycdc/cli.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""CLI for pycdc."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main(argv: list[str] | None = None) -> None:
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
prog="pycdc",
|
|
13
|
+
description="Please Your Customer Direct Connect — find CRM gift ideas",
|
|
14
|
+
)
|
|
15
|
+
sub = parser.add_subparsers(dest="command")
|
|
16
|
+
|
|
17
|
+
# Search
|
|
18
|
+
search_p = sub.add_parser("search", help="Search for gift ideas")
|
|
19
|
+
search_p.add_argument("query", nargs="?", default="", help="Search keyword")
|
|
20
|
+
search_p.add_argument("--occasion", choices=[o.value for o in _occasions()])
|
|
21
|
+
search_p.add_argument("--budget", choices=[b.value for b in _budgets()])
|
|
22
|
+
search_p.add_argument("--no-web", action="store_true", help="Skip web search")
|
|
23
|
+
search_p.add_argument("--json", action="store_true")
|
|
24
|
+
|
|
25
|
+
# Recommend
|
|
26
|
+
rec_p = sub.add_parser("recommend", help="Get recommendations for a customer")
|
|
27
|
+
rec_p.add_argument("--name", required=True)
|
|
28
|
+
rec_p.add_argument("--company", default="")
|
|
29
|
+
rec_p.add_argument("--deal-value", type=float, default=0)
|
|
30
|
+
rec_p.add_argument("--interests", nargs="*", default=[])
|
|
31
|
+
rec_p.add_argument("--occasion", default="thank_you")
|
|
32
|
+
|
|
33
|
+
# List occasions
|
|
34
|
+
sub.add_parser("occasions", help="List all occasions")
|
|
35
|
+
sub.add_parser("budgets", help="List all budget tiers")
|
|
36
|
+
|
|
37
|
+
args = parser.parse_args(argv)
|
|
38
|
+
|
|
39
|
+
if args.command == "search":
|
|
40
|
+
from .client import GiftFinder
|
|
41
|
+
from .models import Occasion, Budget
|
|
42
|
+
finder = GiftFinder(enable_web=not args.no_web)
|
|
43
|
+
occasion = Occasion(args.occasion) if args.occasion else None
|
|
44
|
+
budget = Budget(args.budget) if args.budget else None
|
|
45
|
+
ideas = finder.find(occasion=occasion, budget=budget, query=args.query)
|
|
46
|
+
if args.json:
|
|
47
|
+
print(json.dumps([i.to_dict() for i in ideas], indent=2))
|
|
48
|
+
else:
|
|
49
|
+
for i in ideas:
|
|
50
|
+
print(f" [{i.budget.value:>8}] {i.name}")
|
|
51
|
+
print(f" {i.price_range} — {i.description[:60]}")
|
|
52
|
+
if i.personalization_tip:
|
|
53
|
+
print(f" Tip: {i.personalization_tip}")
|
|
54
|
+
print()
|
|
55
|
+
|
|
56
|
+
elif args.command == "recommend":
|
|
57
|
+
from .client import GiftFinder
|
|
58
|
+
from .models import CustomerProfile, Occasion
|
|
59
|
+
customer = CustomerProfile(
|
|
60
|
+
name=args.name, company=args.company,
|
|
61
|
+
deal_value=args.deal_value, interests=args.interests)
|
|
62
|
+
finder = GiftFinder()
|
|
63
|
+
print(finder.summary_for(customer))
|
|
64
|
+
|
|
65
|
+
elif args.command == "occasions":
|
|
66
|
+
for o in _occasions():
|
|
67
|
+
print(f" {o.value}")
|
|
68
|
+
|
|
69
|
+
elif args.command == "budgets":
|
|
70
|
+
for b in _budgets():
|
|
71
|
+
low, high = b.range
|
|
72
|
+
print(f" {b.value:>8} ${low}-${high}")
|
|
73
|
+
|
|
74
|
+
else:
|
|
75
|
+
parser.print_help()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _occasions():
|
|
79
|
+
from .models import Occasion
|
|
80
|
+
return list(Occasion)
|
|
81
|
+
|
|
82
|
+
def _budgets():
|
|
83
|
+
from .models import Budget
|
|
84
|
+
return list(Budget)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
main()
|
pycdc/client.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""GiftFinder — main client that combines catalog search with web search."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .models import GiftIdea, Occasion, Budget, CustomerProfile
|
|
11
|
+
from .catalog import search_catalog, CATALOG
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class GiftFinder:
|
|
16
|
+
"""Search for CRM gift ideas from built-in catalog and the web.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
enable_web : bool
|
|
21
|
+
Whether to also search the web for ideas (default True).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
enable_web: bool = True
|
|
25
|
+
|
|
26
|
+
def find(self, *, occasion: Occasion | None = None, budget: Budget | None = None,
|
|
27
|
+
query: str = "", customer: CustomerProfile | None = None) -> list[GiftIdea]:
|
|
28
|
+
"""Find gift ideas matching criteria.
|
|
29
|
+
|
|
30
|
+
Searches the built-in catalog and optionally the web.
|
|
31
|
+
"""
|
|
32
|
+
interests = customer.interests if customer else None
|
|
33
|
+
|
|
34
|
+
# Catalog results
|
|
35
|
+
results = search_catalog(occasion=occasion, budget=budget,
|
|
36
|
+
query=query, interests=interests)
|
|
37
|
+
|
|
38
|
+
# Boost score for customer tier match
|
|
39
|
+
if customer and budget is None:
|
|
40
|
+
tier_budget = {
|
|
41
|
+
"enterprise": Budget.VIP,
|
|
42
|
+
"business": Budget.PREMIUM,
|
|
43
|
+
"professional": Budget.STANDARD,
|
|
44
|
+
"starter": Budget.MODEST,
|
|
45
|
+
}
|
|
46
|
+
preferred = tier_budget.get(customer.tier, Budget.STANDARD)
|
|
47
|
+
for g in results:
|
|
48
|
+
if g.budget == preferred:
|
|
49
|
+
g.score += 0.3
|
|
50
|
+
|
|
51
|
+
# Web search (if enabled)
|
|
52
|
+
if self.enable_web and (query or (customer and customer.interests)):
|
|
53
|
+
web_results = asyncio.run(self._web_search(
|
|
54
|
+
query=query, occasion=occasion, budget=budget, customer=customer))
|
|
55
|
+
results.extend(web_results)
|
|
56
|
+
|
|
57
|
+
# Sort by score descending
|
|
58
|
+
results.sort(key=lambda g: g.score, reverse=True)
|
|
59
|
+
return results
|
|
60
|
+
|
|
61
|
+
def recommend(self, customer: CustomerProfile, occasion: Occasion) -> list[GiftIdea]:
|
|
62
|
+
"""Smart recommendations based on customer profile and occasion."""
|
|
63
|
+
# Determine budget from customer tier
|
|
64
|
+
tier_budget = {
|
|
65
|
+
"enterprise": Budget.VIP,
|
|
66
|
+
"business": Budget.PREMIUM,
|
|
67
|
+
"professional": Budget.STANDARD,
|
|
68
|
+
"starter": Budget.MODEST,
|
|
69
|
+
}
|
|
70
|
+
budget = tier_budget.get(customer.tier, Budget.STANDARD)
|
|
71
|
+
|
|
72
|
+
ideas = self.find(occasion=occasion, budget=budget, customer=customer)
|
|
73
|
+
|
|
74
|
+
# Further personalize tips
|
|
75
|
+
for idea in ideas:
|
|
76
|
+
if customer.name and idea.personalization_tip:
|
|
77
|
+
idea.personalization_tip = (
|
|
78
|
+
f"For {customer.name}: {idea.personalization_tip}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return ideas[:5]
|
|
82
|
+
|
|
83
|
+
async def _web_search(self, *, query: str, occasion: Occasion | None,
|
|
84
|
+
budget: Budget | None, customer: CustomerProfile | None) -> list[GiftIdea]:
|
|
85
|
+
"""Search the web for gift ideas (uses DuckDuckGo instant answers)."""
|
|
86
|
+
search_terms = []
|
|
87
|
+
if query:
|
|
88
|
+
search_terms.append(query)
|
|
89
|
+
if occasion:
|
|
90
|
+
search_terms.append(f"{occasion.value} gift")
|
|
91
|
+
if budget:
|
|
92
|
+
low, high = budget.range
|
|
93
|
+
search_terms.append(f"${low}-${high}")
|
|
94
|
+
if customer and customer.interests:
|
|
95
|
+
search_terms.extend(customer.interests[:2])
|
|
96
|
+
search_terms.append("corporate gift idea")
|
|
97
|
+
|
|
98
|
+
search_query = " ".join(search_terms)
|
|
99
|
+
|
|
100
|
+
async with httpx.AsyncClient() as client:
|
|
101
|
+
try:
|
|
102
|
+
resp = await client.get(
|
|
103
|
+
"https://api.duckduckgo.com/",
|
|
104
|
+
params={"q": search_query, "format": "json", "no_html": 1},
|
|
105
|
+
timeout=10,
|
|
106
|
+
)
|
|
107
|
+
resp.raise_for_status()
|
|
108
|
+
data = resp.json()
|
|
109
|
+
|
|
110
|
+
results = []
|
|
111
|
+
for topic in data.get("RelatedTopics", [])[:5]:
|
|
112
|
+
text = topic.get("Text", "")
|
|
113
|
+
url = topic.get("FirstURL", "")
|
|
114
|
+
if text:
|
|
115
|
+
results.append(GiftIdea(
|
|
116
|
+
name=text[:60],
|
|
117
|
+
description=text,
|
|
118
|
+
price_range="varies",
|
|
119
|
+
budget=budget or Budget.STANDARD,
|
|
120
|
+
occasion=occasion or Occasion.CUSTOM,
|
|
121
|
+
source_url=url,
|
|
122
|
+
category="web-suggestion",
|
|
123
|
+
score=0.3,
|
|
124
|
+
))
|
|
125
|
+
return results
|
|
126
|
+
except Exception:
|
|
127
|
+
return []
|
|
128
|
+
|
|
129
|
+
def summary_for(self, customer: CustomerProfile) -> str:
|
|
130
|
+
"""Print a customer gift strategy summary."""
|
|
131
|
+
lines = [
|
|
132
|
+
f"Gift Strategy for {customer.name}",
|
|
133
|
+
f" Company: {customer.company} ({customer.industry})",
|
|
134
|
+
f" Tier: {customer.tier} (deal value: ${customer.deal_value:,.0f})",
|
|
135
|
+
f" Interests: {', '.join(customer.interests) or 'unknown'}",
|
|
136
|
+
f" Relationship: {customer.relationship_length_months} months",
|
|
137
|
+
"",
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
for occasion in [Occasion.RENEWAL, Occasion.HOLIDAY, Occasion.THANK_YOU]:
|
|
141
|
+
recs = self.recommend(customer, occasion)
|
|
142
|
+
if recs:
|
|
143
|
+
lines.append(f" [{occasion.value.upper()}]")
|
|
144
|
+
for r in recs[:2]:
|
|
145
|
+
lines.append(f" • {r.name} ({r.price_range}) — {r.personalization_tip}")
|
|
146
|
+
lines.append("")
|
|
147
|
+
|
|
148
|
+
return "\n".join(lines)
|
pycdc/models.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Core models for customer profiles, occasions, and gift ideas."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Occasion(str, Enum):
|
|
11
|
+
ONBOARDING = "onboarding"
|
|
12
|
+
RENEWAL = "renewal"
|
|
13
|
+
BIRTHDAY = "birthday"
|
|
14
|
+
HOLIDAY = "holiday"
|
|
15
|
+
MILESTONE = "milestone"
|
|
16
|
+
THANK_YOU = "thank_you"
|
|
17
|
+
APOLOGY = "apology"
|
|
18
|
+
REFERRAL = "referral"
|
|
19
|
+
ANNIVERSARY = "anniversary"
|
|
20
|
+
CLOSING_DEAL = "closing_deal"
|
|
21
|
+
PROMOTION = "promotion"
|
|
22
|
+
CUSTOM = "custom"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Budget(str, Enum):
|
|
26
|
+
TOKEN = "token" # $0-15
|
|
27
|
+
MODEST = "modest" # $15-50
|
|
28
|
+
STANDARD = "standard" # $50-150
|
|
29
|
+
PREMIUM = "premium" # $150-500
|
|
30
|
+
VIP = "vip" # $500+
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def range(self) -> tuple[int, int]:
|
|
34
|
+
return {
|
|
35
|
+
"token": (0, 15),
|
|
36
|
+
"modest": (15, 50),
|
|
37
|
+
"standard": (50, 150),
|
|
38
|
+
"premium": (150, 500),
|
|
39
|
+
"vip": (500, 10000),
|
|
40
|
+
}[self.value]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class CustomerProfile:
|
|
45
|
+
"""Profile of a customer for personalized gift recommendations."""
|
|
46
|
+
name: str
|
|
47
|
+
company: str = ""
|
|
48
|
+
role: str = ""
|
|
49
|
+
industry: str = ""
|
|
50
|
+
interests: list[str] = field(default_factory=list)
|
|
51
|
+
location: str = ""
|
|
52
|
+
relationship_length_months: int = 0
|
|
53
|
+
deal_value: float = 0.0
|
|
54
|
+
notes: str = ""
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def tier(self) -> str:
|
|
58
|
+
if self.deal_value >= 100_000:
|
|
59
|
+
return "enterprise"
|
|
60
|
+
elif self.deal_value >= 10_000:
|
|
61
|
+
return "business"
|
|
62
|
+
elif self.deal_value >= 1_000:
|
|
63
|
+
return "professional"
|
|
64
|
+
return "starter"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class GiftIdea:
|
|
69
|
+
"""A single gift recommendation."""
|
|
70
|
+
name: str
|
|
71
|
+
description: str
|
|
72
|
+
price_range: str
|
|
73
|
+
budget: Budget
|
|
74
|
+
occasion: Occasion
|
|
75
|
+
source_url: str = ""
|
|
76
|
+
category: str = ""
|
|
77
|
+
personalization_tip: str = ""
|
|
78
|
+
score: float = 0.0 # relevance score 0-1
|
|
79
|
+
|
|
80
|
+
def to_dict(self) -> dict[str, Any]:
|
|
81
|
+
return {
|
|
82
|
+
"name": self.name,
|
|
83
|
+
"description": self.description,
|
|
84
|
+
"price_range": self.price_range,
|
|
85
|
+
"budget": self.budget.value,
|
|
86
|
+
"occasion": self.occasion.value,
|
|
87
|
+
"source_url": self.source_url,
|
|
88
|
+
"category": self.category,
|
|
89
|
+
"personalization_tip": self.personalization_tip,
|
|
90
|
+
"score": self.score,
|
|
91
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pycdc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Please Your Customer Direct Connect — search the web for CRM gift ideas and client appreciation strategies
|
|
5
|
+
Project-URL: Homepage, https://github.com/brnv/pycdc
|
|
6
|
+
Author-email: Artem <brnv@canva.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: appreciation,client-relations,crm,customer,direct-connect,gifts
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Office/Business :: Financial :: Point-Of-Sale
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: httpx>=0.27
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# pycdc
|
|
20
|
+
|
|
21
|
+
**P**lease **Y**our **C**ustomer **D**irect **C**onnect — a library to search the web and get gift ideas for CRM client appreciation.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install pycdc
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from pycdc import GiftFinder, CustomerProfile, Occasion, Budget
|
|
33
|
+
|
|
34
|
+
finder = GiftFinder()
|
|
35
|
+
|
|
36
|
+
# Simple search
|
|
37
|
+
ideas = finder.find(occasion=Occasion.RENEWAL, budget=Budget.STANDARD)
|
|
38
|
+
|
|
39
|
+
# Customer-aware recommendations
|
|
40
|
+
customer = CustomerProfile(
|
|
41
|
+
name="Sarah Chen",
|
|
42
|
+
company="Acme Corp",
|
|
43
|
+
deal_value=50_000,
|
|
44
|
+
interests=["coffee", "hiking", "tech"],
|
|
45
|
+
relationship_length_months=18,
|
|
46
|
+
)
|
|
47
|
+
recs = finder.recommend(customer, Occasion.ANNIVERSARY)
|
|
48
|
+
for r in recs:
|
|
49
|
+
print(f"{r.name} ({r.price_range}) — {r.personalization_tip}")
|
|
50
|
+
|
|
51
|
+
# Full gift strategy
|
|
52
|
+
print(finder.summary_for(customer))
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## CLI
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pycdc search "tech" --budget premium
|
|
59
|
+
pycdc search --occasion holiday --budget modest --json
|
|
60
|
+
pycdc recommend --name "Sarah Chen" --company "Acme" --deal-value 50000 --interests coffee hiking
|
|
61
|
+
pycdc occasions
|
|
62
|
+
pycdc budgets
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pycdc/__init__.py,sha256=BxoSWqQiqB_RR_jXC6KLVeCyGqBg61krKEMCxgleqDE,273
|
|
2
|
+
pycdc/catalog.py,sha256=kbc8gYHY2To0kuazivw-dtJYZxqfg0eY5txG1rs-aWY,5451
|
|
3
|
+
pycdc/cli.py,sha256=hteAWAtIogf9LlM7ptfzf1tEyKDqdq7SYSGNNgmVw8w,2998
|
|
4
|
+
pycdc/client.py,sha256=v89u4VBd2wVu0XtnZeHtjhLuZr6jokkWGVZTOY-ZZVA,5604
|
|
5
|
+
pycdc/models.py,sha256=MOcYGwm3Fa_Cu0dlvQnWvyOh_Ln5jfpcHJTpJD1JoEA,2407
|
|
6
|
+
pycdc-0.1.0.dist-info/METADATA,sha256=jyUuv8yK85Jf99bMnU7RjyMWBw4e6oSJ5f7nmDZtj0s,1786
|
|
7
|
+
pycdc-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
8
|
+
pycdc-0.1.0.dist-info/entry_points.txt,sha256=nF0QyD7wOpajs7KI3BME8dyf6-aGLudjnNTvFkhowWU,41
|
|
9
|
+
pycdc-0.1.0.dist-info/licenses/LICENSE,sha256=2vTtTMBZTlDHJyeU3ehwS7VdCCnprwCnVvv2q52NUJ8,1062
|
|
10
|
+
pycdc-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Artem
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|