coconnect 1.0.0__tar.gz
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.
- coconnect-1.0.0/PKG-INFO +43 -0
- coconnect-1.0.0/README.md +34 -0
- coconnect-1.0.0/coconnect/__init__.py +146 -0
- coconnect-1.0.0/coconnect.egg-info/PKG-INFO +43 -0
- coconnect-1.0.0/coconnect.egg-info/SOURCES.txt +7 -0
- coconnect-1.0.0/coconnect.egg-info/dependency_links.txt +1 -0
- coconnect-1.0.0/coconnect.egg-info/top_level.txt +1 -0
- coconnect-1.0.0/pyproject.toml +16 -0
- coconnect-1.0.0/setup.cfg +4 -0
coconnect-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: coconnect
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Send your platform's events to CoConnect Hub, in your own words. Server side.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: coconnect,quests,rewards,partner,events
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# coconnect
|
|
11
|
+
|
|
12
|
+
Send what users do on your platform to CoConnect Hub, named the way your platform already names it. CoConnect matches each event to the right task and rewards the member.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install coconnect
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import os
|
|
20
|
+
from coconnect import CoConnect
|
|
21
|
+
|
|
22
|
+
hub = CoConnect(token=os.environ["COCONNECT_TOKEN"])
|
|
23
|
+
|
|
24
|
+
# When a user signs up (cc_id is the cookie set by https://hub.coconnect.co/sdk.js on your site):
|
|
25
|
+
hub.send("user_registered", user="u_1001", cc_id=request.cookies.get("cc_id"))
|
|
26
|
+
|
|
27
|
+
# Anything else, in your own words, with whatever details you have:
|
|
28
|
+
hub.send("ftd", user="u_1001", amount=75, currency="USD", id="dep_10482")
|
|
29
|
+
hub.send("kyc_failed", user="u_1001", status="failed", reason="Document unreadable")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- **`user`** is required: your own ID for the user.
|
|
33
|
+
- **`cc_id`** links the user to their CoConnect account. Send it at least once per user.
|
|
34
|
+
- **`id`** is your reference for the event. If you leave it out, the SDK generates one, so a retried request is never counted twice.
|
|
35
|
+
- **Any other keyword arguments** are kept as details of the event. Emails and phone numbers are hashed by CoConnect on arrival.
|
|
36
|
+
- **Batching:** `send_batch([...])` sends up to 50 events at once.
|
|
37
|
+
|
|
38
|
+
The SDK also handles:
|
|
39
|
+
- **Retries:** network errors, 429 and 5xx responses are retried with backoff.
|
|
40
|
+
- **Errors:** a bad token or a malformed event raises `CoConnectError`.
|
|
41
|
+
- **No dependencies** beyond the standard library.
|
|
42
|
+
|
|
43
|
+
Keep your token on the server.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# coconnect
|
|
2
|
+
|
|
3
|
+
Send what users do on your platform to CoConnect Hub, named the way your platform already names it. CoConnect matches each event to the right task and rewards the member.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install coconnect
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import os
|
|
11
|
+
from coconnect import CoConnect
|
|
12
|
+
|
|
13
|
+
hub = CoConnect(token=os.environ["COCONNECT_TOKEN"])
|
|
14
|
+
|
|
15
|
+
# When a user signs up (cc_id is the cookie set by https://hub.coconnect.co/sdk.js on your site):
|
|
16
|
+
hub.send("user_registered", user="u_1001", cc_id=request.cookies.get("cc_id"))
|
|
17
|
+
|
|
18
|
+
# Anything else, in your own words, with whatever details you have:
|
|
19
|
+
hub.send("ftd", user="u_1001", amount=75, currency="USD", id="dep_10482")
|
|
20
|
+
hub.send("kyc_failed", user="u_1001", status="failed", reason="Document unreadable")
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- **`user`** is required: your own ID for the user.
|
|
24
|
+
- **`cc_id`** links the user to their CoConnect account. Send it at least once per user.
|
|
25
|
+
- **`id`** is your reference for the event. If you leave it out, the SDK generates one, so a retried request is never counted twice.
|
|
26
|
+
- **Any other keyword arguments** are kept as details of the event. Emails and phone numbers are hashed by CoConnect on arrival.
|
|
27
|
+
- **Batching:** `send_batch([...])` sends up to 50 events at once.
|
|
28
|
+
|
|
29
|
+
The SDK also handles:
|
|
30
|
+
- **Retries:** network errors, 429 and 5xx responses are retried with backoff.
|
|
31
|
+
- **Errors:** a bad token or a malformed event raises `CoConnectError`.
|
|
32
|
+
- **No dependencies** beyond the standard library.
|
|
33
|
+
|
|
34
|
+
Keep your token on the server.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""CoConnect Hub SDK for Python (3.8+), server side only.
|
|
2
|
+
|
|
3
|
+
Partners send what happened on their platform, in their own words::
|
|
4
|
+
|
|
5
|
+
from coconnect import CoConnect
|
|
6
|
+
hub = CoConnect(token=os.environ["COCONNECT_TOKEN"])
|
|
7
|
+
hub.send("ftd", user="u_1001", cc_id=cc_id, amount=75, currency="USD")
|
|
8
|
+
|
|
9
|
+
CoConnect matches each event to the right task. The SDK adds a unique id to
|
|
10
|
+
every event (so a retry is never counted twice), retries when the hub is
|
|
11
|
+
briefly unreachable, and skips events no task needs once the hub says so.
|
|
12
|
+
No dependencies beyond the standard library.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import random
|
|
17
|
+
import re
|
|
18
|
+
import time
|
|
19
|
+
import urllib.error
|
|
20
|
+
import urllib.request
|
|
21
|
+
import uuid
|
|
22
|
+
|
|
23
|
+
__all__ = ["CoConnect", "CoConnectError"]
|
|
24
|
+
__version__ = "1.0.0"
|
|
25
|
+
|
|
26
|
+
_KNOWN = {"user", "cc_id", "status", "id", "amount", "currency", "reason", "wallet", "email", "x", "discord", "properties"}
|
|
27
|
+
_RETRY = {408, 425, 429, 500, 502, 503, 504}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CoConnectError(Exception):
|
|
31
|
+
def __init__(self, message, status=None, body=None):
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.status = status
|
|
34
|
+
self.body = body
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _event_key(name):
|
|
38
|
+
s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", str(name or "")).lower()
|
|
39
|
+
return re.sub(r"[^a-z0-9]+", "_", s).strip("_")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CoConnect:
|
|
43
|
+
def __init__(self, token, base_url="https://hub.coconnect.co", max_retries=4, timeout=10, filter=True):
|
|
44
|
+
"""token: your partner token from CoConnect (keep it on the server)."""
|
|
45
|
+
if not token or not isinstance(token, str):
|
|
46
|
+
raise CoConnectError("A token is required.")
|
|
47
|
+
self.token = token
|
|
48
|
+
self.base_url = base_url.rstrip("/")
|
|
49
|
+
self.max_retries = max_retries
|
|
50
|
+
self.timeout = timeout
|
|
51
|
+
self.filter = filter
|
|
52
|
+
self._config = None
|
|
53
|
+
self._config_at = 0.0
|
|
54
|
+
|
|
55
|
+
def send(self, event, user, **details):
|
|
56
|
+
"""Send one event, named the way your platform names it (e.g. "ftd").
|
|
57
|
+
|
|
58
|
+
user: your own ID for the user (required). Optional: cc_id, status
|
|
59
|
+
("completed" | "failed" | "reversed"), id (your reference), amount,
|
|
60
|
+
currency, reason, wallet, email, x, discord, and any other details.
|
|
61
|
+
"""
|
|
62
|
+
out = self.send_batch([dict(details, event=event, user=user)])
|
|
63
|
+
results = out.get("results") or []
|
|
64
|
+
return results[0] if results else out
|
|
65
|
+
|
|
66
|
+
def send_batch(self, events):
|
|
67
|
+
"""Up to 50 events in one request, each a dict with event, user and details."""
|
|
68
|
+
if not events:
|
|
69
|
+
raise CoConnectError("No events.")
|
|
70
|
+
if len(events) > 50:
|
|
71
|
+
raise CoConnectError("At most 50 events per batch.")
|
|
72
|
+
wanted = [self._shape(e) for e in events if self._wanted(e.get("event"))]
|
|
73
|
+
if not wanted:
|
|
74
|
+
return {"ok": True, "results": [], "skipped": len(events)}
|
|
75
|
+
return self._request("POST", "/api/partner/events", {"events": wanted})
|
|
76
|
+
|
|
77
|
+
def _shape(self, e):
|
|
78
|
+
event = e.get("event")
|
|
79
|
+
if not event:
|
|
80
|
+
raise CoConnectError("Each event needs a name.")
|
|
81
|
+
if e.get("user") in (None, ""):
|
|
82
|
+
raise CoConnectError('"%s" needs user: your own ID for the user.' % event)
|
|
83
|
+
properties = dict(e.get("properties") or {})
|
|
84
|
+
for k, v in e.items():
|
|
85
|
+
if k not in _KNOWN and k != "event" and v is not None:
|
|
86
|
+
properties[k] = v
|
|
87
|
+
body = {
|
|
88
|
+
"event": str(event),
|
|
89
|
+
"user": str(e["user"]),
|
|
90
|
+
"status": e.get("status", "completed"),
|
|
91
|
+
# A unique reference per event: a retried request is then counted once.
|
|
92
|
+
"id": str(e["id"]) if e.get("id") is not None else str(uuid.uuid4()),
|
|
93
|
+
"properties": properties,
|
|
94
|
+
}
|
|
95
|
+
for k in ("cc_id", "amount", "currency", "reason", "wallet", "email", "x", "discord"):
|
|
96
|
+
if e.get(k) is not None:
|
|
97
|
+
body[k] = e[k]
|
|
98
|
+
return body
|
|
99
|
+
|
|
100
|
+
def _wanted(self, name):
|
|
101
|
+
if not self.filter:
|
|
102
|
+
return True
|
|
103
|
+
cfg = self._get_config()
|
|
104
|
+
key = _event_key(name)
|
|
105
|
+
if not cfg or key not in (cfg.get("quiet") or []) or key in (cfg.get("events") or []):
|
|
106
|
+
return True
|
|
107
|
+
return random.random() < float(cfg.get("sample", 0.05))
|
|
108
|
+
|
|
109
|
+
def _get_config(self):
|
|
110
|
+
age = float((self._config or {}).get("refreshSeconds", 300))
|
|
111
|
+
if self._config is not None and time.time() - self._config_at < age:
|
|
112
|
+
return self._config
|
|
113
|
+
try:
|
|
114
|
+
self._config = self._request("GET", "/api/partner/config")
|
|
115
|
+
except CoConnectError:
|
|
116
|
+
# Without advice, everything is sent.
|
|
117
|
+
self._config = None
|
|
118
|
+
self._config_at = time.time()
|
|
119
|
+
return self._config
|
|
120
|
+
|
|
121
|
+
def _request(self, method, path, body=None):
|
|
122
|
+
last = None
|
|
123
|
+
for attempt in range(self.max_retries + 1):
|
|
124
|
+
if attempt:
|
|
125
|
+
time.sleep(min(8.0, 0.5 * 2 ** (attempt - 1)) + random.random() * 0.25)
|
|
126
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
127
|
+
req = urllib.request.Request(
|
|
128
|
+
self.base_url + path,
|
|
129
|
+
data=data,
|
|
130
|
+
method=method,
|
|
131
|
+
headers={"content-type": "application/json", "x-cc-token": self.token, "user-agent": "coconnect-sdk-python/" + __version__},
|
|
132
|
+
)
|
|
133
|
+
try:
|
|
134
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as res:
|
|
135
|
+
return json.loads(res.read() or b"{}")
|
|
136
|
+
except urllib.error.HTTPError as e:
|
|
137
|
+
try:
|
|
138
|
+
payload = json.loads(e.read() or b"{}")
|
|
139
|
+
except ValueError:
|
|
140
|
+
payload = {}
|
|
141
|
+
last = CoConnectError(payload.get("error") or "CoConnect answered %s" % e.code, e.code, payload)
|
|
142
|
+
if e.code not in _RETRY:
|
|
143
|
+
raise last
|
|
144
|
+
except (urllib.error.URLError, TimeoutError, OSError) as e:
|
|
145
|
+
last = CoConnectError("Couldn't reach CoConnect: %s" % e)
|
|
146
|
+
raise last
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: coconnect
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Send your platform's events to CoConnect Hub, in your own words. Server side.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: coconnect,quests,rewards,partner,events
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# coconnect
|
|
11
|
+
|
|
12
|
+
Send what users do on your platform to CoConnect Hub, named the way your platform already names it. CoConnect matches each event to the right task and rewards the member.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install coconnect
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import os
|
|
20
|
+
from coconnect import CoConnect
|
|
21
|
+
|
|
22
|
+
hub = CoConnect(token=os.environ["COCONNECT_TOKEN"])
|
|
23
|
+
|
|
24
|
+
# When a user signs up (cc_id is the cookie set by https://hub.coconnect.co/sdk.js on your site):
|
|
25
|
+
hub.send("user_registered", user="u_1001", cc_id=request.cookies.get("cc_id"))
|
|
26
|
+
|
|
27
|
+
# Anything else, in your own words, with whatever details you have:
|
|
28
|
+
hub.send("ftd", user="u_1001", amount=75, currency="USD", id="dep_10482")
|
|
29
|
+
hub.send("kyc_failed", user="u_1001", status="failed", reason="Document unreadable")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- **`user`** is required: your own ID for the user.
|
|
33
|
+
- **`cc_id`** links the user to their CoConnect account. Send it at least once per user.
|
|
34
|
+
- **`id`** is your reference for the event. If you leave it out, the SDK generates one, so a retried request is never counted twice.
|
|
35
|
+
- **Any other keyword arguments** are kept as details of the event. Emails and phone numbers are hashed by CoConnect on arrival.
|
|
36
|
+
- **Batching:** `send_batch([...])` sends up to 50 events at once.
|
|
37
|
+
|
|
38
|
+
The SDK also handles:
|
|
39
|
+
- **Retries:** network errors, 429 and 5xx responses are retried with backoff.
|
|
40
|
+
- **Errors:** a bad token or a malformed event raises `CoConnectError`.
|
|
41
|
+
- **No dependencies** beyond the standard library.
|
|
42
|
+
|
|
43
|
+
Keep your token on the server.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
coconnect
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "coconnect"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Send your platform's events to CoConnect Hub, in your own words. Server side."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["coconnect", "quests", "rewards", "partner", "events"]
|
|
13
|
+
dependencies = []
|
|
14
|
+
|
|
15
|
+
[tool.setuptools]
|
|
16
|
+
packages = ["coconnect"]
|