coconnect 1.0.0__tar.gz → 1.1.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.
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: coconnect
3
+ Version: 1.1.0
4
+ Summary: Send your platform's events to CoConnect Hub, in your own words. Server side.
5
+ License-Expression: 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. Everything runs on your server; nothing is added to your web pages.
13
+
14
+ ```bash
15
+ pip install coconnect
16
+ ```
17
+
18
+ ## 1. Keep the CoConnect ID
19
+
20
+ Members arrive from CoConnect with their CoConnect ID in the link (`?cc_id=ck_...`). Read it on your server when they land and keep it in a first-party cookie or the session, so it survives sign-up redirects such as Google sign-in:
21
+
22
+ ```python
23
+ from coconnect import cc_id_from_url
24
+
25
+ # Flask shown; any framework works the same way.
26
+ @app.after_request
27
+ def keep_cc_id(response):
28
+ cc_id = cc_id_from_url(request.full_path)
29
+ if cc_id:
30
+ response.set_cookie("cc_id", cc_id, max_age=30 * 86400, httponly=True, secure=True, samesite="Lax")
31
+ return response
32
+
33
+ # At sign-up, save request.cookies.get("cc_id") with the new account.
34
+ ```
35
+
36
+ ## 2. Send events
37
+
38
+ ```python
39
+ import os
40
+ from coconnect import CoConnect
41
+
42
+ hub = CoConnect(
43
+ token=os.environ["COCONNECT_TOKEN"],
44
+ secret=os.environ.get("COCONNECT_SECRET"), # optional: your signing secret
45
+ )
46
+
47
+ # When a user signs up:
48
+ hub.send("user_registered", user="u_1001", cc_id=user.cc_id)
49
+
50
+ # Anything else, in your own words, with whatever details you have:
51
+ hub.send("ftd", user="u_1001", amount=75, currency="USD", id="dep_10482")
52
+ hub.send("kyc_failed", user="u_1001", status="failed", reason="Document unreadable")
53
+ ```
54
+
55
+ - **`user`** is required: your own ID for the user.
56
+ - **`cc_id`** links the user to their CoConnect account. Send it at least once per user.
57
+ - **`id`** is your reference for the event. If you leave it out, the SDK generates one, so a retried request is never counted twice.
58
+ - **Any other keyword arguments** are kept as details of the event. Emails and phone numbers are hashed by CoConnect on arrival.
59
+ - **Batching:** `send_batch([...])` sends up to 50 events at once.
60
+
61
+ ## Signed requests
62
+
63
+ If CoConnect gave you a signing secret (`ccs_...`), pass it as `secret`. Every request is then signed with a timestamp, so it cannot be altered or replayed, even by someone who has seen your token. Without the client, `sign_request(secret, timestamp, method, path, body)` builds the `X-CC-Signature` value.
64
+
65
+ ## Also handled
66
+
67
+ - **Retries:** network errors, 429 and 5xx responses are retried with backoff.
68
+ - **Errors:** a bad token, a bad signature or a malformed event raises `CoConnectError`.
69
+ - **No dependencies** beyond the standard library.
70
+
71
+ Keep your token and secret on the server.
@@ -0,0 +1,62 @@
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. Everything runs on your server; nothing is added to your web pages.
4
+
5
+ ```bash
6
+ pip install coconnect
7
+ ```
8
+
9
+ ## 1. Keep the CoConnect ID
10
+
11
+ Members arrive from CoConnect with their CoConnect ID in the link (`?cc_id=ck_...`). Read it on your server when they land and keep it in a first-party cookie or the session, so it survives sign-up redirects such as Google sign-in:
12
+
13
+ ```python
14
+ from coconnect import cc_id_from_url
15
+
16
+ # Flask shown; any framework works the same way.
17
+ @app.after_request
18
+ def keep_cc_id(response):
19
+ cc_id = cc_id_from_url(request.full_path)
20
+ if cc_id:
21
+ response.set_cookie("cc_id", cc_id, max_age=30 * 86400, httponly=True, secure=True, samesite="Lax")
22
+ return response
23
+
24
+ # At sign-up, save request.cookies.get("cc_id") with the new account.
25
+ ```
26
+
27
+ ## 2. Send events
28
+
29
+ ```python
30
+ import os
31
+ from coconnect import CoConnect
32
+
33
+ hub = CoConnect(
34
+ token=os.environ["COCONNECT_TOKEN"],
35
+ secret=os.environ.get("COCONNECT_SECRET"), # optional: your signing secret
36
+ )
37
+
38
+ # When a user signs up:
39
+ hub.send("user_registered", user="u_1001", cc_id=user.cc_id)
40
+
41
+ # Anything else, in your own words, with whatever details you have:
42
+ hub.send("ftd", user="u_1001", amount=75, currency="USD", id="dep_10482")
43
+ hub.send("kyc_failed", user="u_1001", status="failed", reason="Document unreadable")
44
+ ```
45
+
46
+ - **`user`** is required: your own ID for the user.
47
+ - **`cc_id`** links the user to their CoConnect account. Send it at least once per user.
48
+ - **`id`** is your reference for the event. If you leave it out, the SDK generates one, so a retried request is never counted twice.
49
+ - **Any other keyword arguments** are kept as details of the event. Emails and phone numbers are hashed by CoConnect on arrival.
50
+ - **Batching:** `send_batch([...])` sends up to 50 events at once.
51
+
52
+ ## Signed requests
53
+
54
+ If CoConnect gave you a signing secret (`ccs_...`), pass it as `secret`. Every request is then signed with a timestamp, so it cannot be altered or replayed, even by someone who has seen your token. Without the client, `sign_request(secret, timestamp, method, path, body)` builds the `X-CC-Signature` value.
55
+
56
+ ## Also handled
57
+
58
+ - **Retries:** network errors, 429 and 5xx responses are retried with backoff.
59
+ - **Errors:** a bad token, a bad signature or a malformed event raises `CoConnectError`.
60
+ - **No dependencies** beyond the standard library.
61
+
62
+ Keep your token and secret on the server.
@@ -8,23 +8,48 @@ Partners send what happened on their platform, in their own words::
8
8
 
9
9
  CoConnect matches each event to the right task. The SDK adds a unique id to
10
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.
11
+ briefly unreachable, skips events no task needs once the hub says so, and
12
+ signs every request when you give it your signing secret.
12
13
  No dependencies beyond the standard library.
13
14
  """
14
15
 
16
+ import hashlib
17
+ import hmac
15
18
  import json
16
19
  import random
17
20
  import re
18
21
  import time
19
22
  import urllib.error
23
+ import urllib.parse
20
24
  import urllib.request
21
25
  import uuid
22
26
 
23
- __all__ = ["CoConnect", "CoConnectError"]
24
- __version__ = "1.0.0"
27
+ __all__ = ["CoConnect", "CoConnectError", "cc_id_from_url", "sign_request"]
28
+ __version__ = "1.1.0"
25
29
 
26
30
  _KNOWN = {"user", "cc_id", "status", "id", "amount", "currency", "reason", "wallet", "email", "x", "discord", "properties"}
27
31
  _RETRY = {408, 425, 429, 500, 502, 503, 504}
32
+ _CC_ID = re.compile(r"^[A-Za-z0-9_-]{4,64}$")
33
+
34
+
35
+ def cc_id_from_url(url):
36
+ """The CoConnect ID a member arrived with, read from your landing URL
37
+ (``?cc_id=...``), or None. Accepts a full URL or a path with its query.
38
+ Call it on your server when the request comes in, and keep the value in a
39
+ first-party cookie or the session until sign-up."""
40
+ try:
41
+ values = urllib.parse.parse_qs(urllib.parse.urlsplit(str(url)).query).get("cc_id") or []
42
+ except ValueError:
43
+ return None
44
+ value = values[0] if values else None
45
+ return value if value and _CC_ID.match(value) else None
46
+
47
+
48
+ def sign_request(secret, timestamp, method, path, body=""):
49
+ """The X-CC-Signature value CoConnect expects: ``v1=`` and the hex
50
+ HMAC-SHA256 of ``<timestamp>.<METHOD>.<path with query>.<raw body>``."""
51
+ message = "%s.%s.%s.%s" % (timestamp, method.upper(), path, body)
52
+ return "v1=" + hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()
28
53
 
29
54
 
30
55
  class CoConnectError(Exception):
@@ -40,11 +65,16 @@ def _event_key(name):
40
65
 
41
66
 
42
67
  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)."""
68
+ def __init__(self, token, secret=None, base_url="https://hub.coconnect.co", max_retries=4, timeout=10, filter=True):
69
+ """token: your partner token from CoConnect (keep it on the server).
70
+ secret: your signing secret (ccs_...), if CoConnect gave you one;
71
+ every request is then signed."""
45
72
  if not token or not isinstance(token, str):
46
73
  raise CoConnectError("A token is required.")
74
+ if secret is not None and not isinstance(secret, str):
75
+ raise CoConnectError("The signing secret must be a string.")
47
76
  self.token = token
77
+ self.secret = secret or None
48
78
  self.base_url = base_url.rstrip("/")
49
79
  self.max_retries = max_retries
50
80
  self.timeout = timeout
@@ -120,15 +150,25 @@ class CoConnect:
120
150
 
121
151
  def _request(self, method, path, body=None):
122
152
  last = None
153
+ # Serialised once: the signature covers these exact bytes.
154
+ raw = json.dumps(body) if body is not None else ""
155
+ url = self.base_url + path
156
+ parts = urllib.parse.urlsplit(url)
157
+ signed_path = parts.path + ("?" + parts.query if parts.query else "")
123
158
  for attempt in range(self.max_retries + 1):
124
159
  if attempt:
125
160
  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
161
+ headers = {"content-type": "application/json", "x-cc-token": self.token, "user-agent": "coconnect-sdk-python/" + __version__}
162
+ if self.secret:
163
+ # A fresh timestamp per attempt, so a retry is never refused as stale.
164
+ ts = str(int(time.time()))
165
+ headers["x-cc-timestamp"] = ts
166
+ headers["x-cc-signature"] = sign_request(self.secret, ts, method, signed_path, raw)
127
167
  req = urllib.request.Request(
128
- self.base_url + path,
129
- data=data,
168
+ url,
169
+ data=raw.encode() if body is not None else None,
130
170
  method=method,
131
- headers={"content-type": "application/json", "x-cc-token": self.token, "user-agent": "coconnect-sdk-python/" + __version__},
171
+ headers=headers,
132
172
  )
133
173
  try:
134
174
  with urllib.request.urlopen(req, timeout=self.timeout) as res:
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: coconnect
3
+ Version: 1.1.0
4
+ Summary: Send your platform's events to CoConnect Hub, in your own words. Server side.
5
+ License-Expression: 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. Everything runs on your server; nothing is added to your web pages.
13
+
14
+ ```bash
15
+ pip install coconnect
16
+ ```
17
+
18
+ ## 1. Keep the CoConnect ID
19
+
20
+ Members arrive from CoConnect with their CoConnect ID in the link (`?cc_id=ck_...`). Read it on your server when they land and keep it in a first-party cookie or the session, so it survives sign-up redirects such as Google sign-in:
21
+
22
+ ```python
23
+ from coconnect import cc_id_from_url
24
+
25
+ # Flask shown; any framework works the same way.
26
+ @app.after_request
27
+ def keep_cc_id(response):
28
+ cc_id = cc_id_from_url(request.full_path)
29
+ if cc_id:
30
+ response.set_cookie("cc_id", cc_id, max_age=30 * 86400, httponly=True, secure=True, samesite="Lax")
31
+ return response
32
+
33
+ # At sign-up, save request.cookies.get("cc_id") with the new account.
34
+ ```
35
+
36
+ ## 2. Send events
37
+
38
+ ```python
39
+ import os
40
+ from coconnect import CoConnect
41
+
42
+ hub = CoConnect(
43
+ token=os.environ["COCONNECT_TOKEN"],
44
+ secret=os.environ.get("COCONNECT_SECRET"), # optional: your signing secret
45
+ )
46
+
47
+ # When a user signs up:
48
+ hub.send("user_registered", user="u_1001", cc_id=user.cc_id)
49
+
50
+ # Anything else, in your own words, with whatever details you have:
51
+ hub.send("ftd", user="u_1001", amount=75, currency="USD", id="dep_10482")
52
+ hub.send("kyc_failed", user="u_1001", status="failed", reason="Document unreadable")
53
+ ```
54
+
55
+ - **`user`** is required: your own ID for the user.
56
+ - **`cc_id`** links the user to their CoConnect account. Send it at least once per user.
57
+ - **`id`** is your reference for the event. If you leave it out, the SDK generates one, so a retried request is never counted twice.
58
+ - **Any other keyword arguments** are kept as details of the event. Emails and phone numbers are hashed by CoConnect on arrival.
59
+ - **Batching:** `send_batch([...])` sends up to 50 events at once.
60
+
61
+ ## Signed requests
62
+
63
+ If CoConnect gave you a signing secret (`ccs_...`), pass it as `secret`. Every request is then signed with a timestamp, so it cannot be altered or replayed, even by someone who has seen your token. Without the client, `sign_request(secret, timestamp, method, path, body)` builds the `X-CC-Signature` value.
64
+
65
+ ## Also handled
66
+
67
+ - **Retries:** network errors, 429 and 5xx responses are retried with backoff.
68
+ - **Errors:** a bad token, a bad signature or a malformed event raises `CoConnectError`.
69
+ - **No dependencies** beyond the standard library.
70
+
71
+ Keep your token and secret on the server.
@@ -1,14 +1,14 @@
1
1
  [build-system]
2
- requires = ["setuptools>=61"]
2
+ requires = ["setuptools>=77"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "coconnect"
7
- version = "1.0.0"
7
+ version = "1.1.0"
8
8
  description = "Send your platform's events to CoConnect Hub, in your own words. Server side."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
11
- license = { text = "MIT" }
11
+ license = "MIT"
12
12
  keywords = ["coconnect", "quests", "rewards", "partner", "events"]
13
13
  dependencies = []
14
14
 
coconnect-1.0.0/PKG-INFO DELETED
@@ -1,43 +0,0 @@
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.
coconnect-1.0.0/README.md DELETED
@@ -1,34 +0,0 @@
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.
@@ -1,43 +0,0 @@
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.
File without changes