Skip to content

Commit dceb660

Browse files
authored
Merge pull request #17 from IABTechLab/ian-UID2-970-publisher-sdk
Ian UI d2 970 publisher sdk
2 parents bea169c + 0de1073 commit dceb660

15 files changed

+871
-5
lines changed

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ test:
66
shell:
77
docker run -it -w $(PWD) -v $(PWD):$(PWD) -u `id -u`:`id -g` $(DOCKERIMAGE) /bin/bash
88

9-
examples: example_client example_auto_refresh example_sharing
9+
examples: example_client example_auto_refresh example_sharing example_publisher
1010

1111
example_client:
1212
docker run -w $(PWD) -v $(PWD):$(PWD) -u `id -u`:`id -g` -e PYTHONPATH=$(PWD) $(DOCKERIMAGE) python3 examples/sample_client.py "$(BASE_URL)" "$(AUTH_KEY)" "$(SECRET_KEY)" "$(AD_TOKEN)"
@@ -20,6 +20,8 @@ example_encryption:
2020
example_sharing:
2121
docker run -w $(PWD) -v $(PWD):$(PWD) -u `id -u`:`id -g` -e PYTHONPATH=$(PWD) $(DOCKERIMAGE) python3 examples/sample_sharing.py "$(BASE_URL)" "$(AUTH_KEY)" "$(SECRET_KEY)" "$(RAW_UID)"
2222

23+
example_publisher:
24+
docker run -w $(PWD) -v $(PWD):$(PWD) -u `id -u`:`id -g` -e PYTHONPATH=$(PWD) $(DOCKERIMAGE) python3 examples/sample_token_generate_refresh.py "$(BASE_URL)" "$(AUTH_KEY)" "$(SECRET_KEY)"
2325

2426
docker:
2527
docker build -t $(DOCKERIMAGE) -f Dockerfile.dev .

README.md

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ This document includes:
77
* [Requirements](#requirements)
88
* [Install](#install)
99
* [Usage for DSPs](#usage-for-dsps)
10+
* [Usage for Publishers](#usage-for-publishers)
11+
* [Standard Integration](#standard-integration)
12+
* [Server-Only Integration](#server-only-integration)
1013
* [Usage for UID2 Sharers](#usage-for-uid2-sharers)
1114
* [Development](#development)
1215
* [Example Usage](#example-usage)
1316

1417
## Who Is this SDK for?
1518

16-
This SDK simplifies integration with UID2 for DSPs and UID Sharers, as described in [UID2 Integration Guides](https://unifiedid.com/docs/category/integration-guides).
19+
This SDK simplifies integration with UID2 for Publishers, DSPs and UID Sharers, as described in [UID2 Integration Guides](https://unifiedid.com/docs/category/integration-guides).
1720

1821
## Requirements
1922

@@ -41,6 +44,58 @@ decrypted_token = client.decrypt(advertising_token)
4144
print(decrypted_token.uid2)
4245
```
4346

47+
## Usage for Publishers
48+
49+
1. Create an instance of Uid2PublisherClient
50+
51+
`client = Uid2PublisherClient(UID2_BASE_URL, UID2_API_KEY, UID2_SECRET_KEY)`
52+
53+
2. Call a function that takes the user's email address or phone number as input and generates a `TokenGenerateResponse` object. The following example uses an email address:
54+
55+
`token_generate_response = client.generate_token(TokenGenerateInput.from_email(emailAddress).do_not_generate_tokens_for_opted_out())`
56+
57+
>IMPORTANT: Be sure to call this function only when you have obtained legal basis to convert the user’s [directly identifying information (DII)](https://unifiedid.com/docs/ref-info/glossary-uid#gl-dii) to UID2 tokens for targeted advertising.
58+
59+
>`do_not_generate_tokens_for_opted_out()` applies `policy=1` in the [/token/generate](https://unifiedid.com/docs/endpoints/post-token-generate#token-generation-policy) call. Without this, `policy` is omitted to maintain backwards compatibility.
60+
61+
### Standard Integration
62+
63+
If you're using standard integration (client and server) (see [UID2 SDK for JavaScript Integration Guide](https://unifiedid.com/docs/guides/publisher-client-side)), follow this step:
64+
65+
* Send this identity as a JSON string back to the client (to use in the [identity field](https://unifiedid.com/docs/sdks/client-side-identity#initopts-object-void)) using the following:
66+
67+
`token_generate_response.get_identity_json_string()` //Note: this method returns `None` if the user has opted out, so be sure to handle that case.
68+
69+
### Server-Only Integration
70+
71+
If you're using server-only integration (see [Publisher Integration Guide, Server-Only](https://unifiedid.com/docs/guides/custom-publisher-integration)):
72+
73+
1. Store this identity as a JSON string in the user's session, using the `token_generate_response.get_identity_json_string()` function. This method returns `None` if the user has opted out, so be sure to handle that case.
74+
2. To retrieve the user's UID2 token, use:
75+
76+
```
77+
identity = token_generate_response.get_identity()
78+
if identity:
79+
advertising_token = identity.get_advertising_token()
80+
```
81+
4. When the user accesses another page, or on a timer, determine whether a refresh is needed:
82+
1. Retrieve the identity JSON string from the user's session, and then call the following function that takes the identity information as input and generates an `IdentityTokens` object:
83+
84+
`identity = IdentityTokens.from_json_string(identityJsonString)`
85+
2. Determine if the identity can be refreshed (that is, the refresh token hasn't expired):
86+
87+
`if not identity or not identity.is_refreshable(): # we must no longer use this identity (for example, remove this identity from the user's session) `
88+
3. Determine if a refresh is needed:
89+
90+
`if identity.is_due_for_refresh()):`
91+
5. If needed, refresh the token and associated values:
92+
93+
`token_refresh_response = client.refresh_token(identity)`
94+
95+
6. Store `token_refresh_response.get_identity_json_string()` in the user's session. If the user has opted out, this method returns `None`, indicating that the user's identity should be removed from the session. To confirm optout, you can use the `token_refresh_response.is_optout()` function.
96+
97+
98+
4499
## Usage for Sharers
45100

46101
A UID2 sharer is a participant that wants to share UID2s or EUIDs with another participant. Raw UID2s must be encrypted into UID2 tokens before sending them to another participant.

examples/sample_encryption.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from uid2_client import encrypt_data, decrypt_data
66

77
# THIS FILE IS DEPRECATED!
8-
# To learn how to encrypt and decrypt a UID2 advertising token, see sample_client.py.
8+
# To learn how to encrypt and decrypt a UID2 advertising token, see sample_sharing.py (For sharers. See sample_client.py for DSPs)
99

1010
def _usage():
1111
print('Usage: python3 sample_encryption.py <base_url> <auth_key> <secret_key> <ad_token> <data>', file=sys.stderr)
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import sys
2+
3+
from uid2_client import Uid2PublisherClient
4+
from uid2_client import TokenGenerateResponse
5+
from uid2_client import TokenGenerateInput
6+
7+
8+
def _usage():
9+
print('Usage: python3 sample_token_generate_refresh.py <base_url> <auth_key> <secret_key>', file=sys.stderr)
10+
sys.exit(1)
11+
12+
13+
if len(sys.argv) <= 3:
14+
_usage()
15+
16+
base_url = sys.argv[1]
17+
auth_key = sys.argv[2]
18+
secret_key = sys.argv[3]
19+
20+
publisher_client = Uid2PublisherClient(base_url, auth_key, secret_key)
21+
22+
print("Generating Token")
23+
try:
24+
token_generate_response = publisher_client.generate_token(TokenGenerateInput.from_email("[email protected]").do_not_generate_tokens_for_opted_out())
25+
except Exception as e:
26+
print(e)
27+
# decide how to handle exception
28+
29+
exit(1)
30+
31+
if(token_generate_response.is_optout()):
32+
print("User has opted out")
33+
exit(0)
34+
tokens = token_generate_response.get_identity()
35+
36+
advertising_token = tokens.get_advertising_token()
37+
refresh_token = tokens.get_refresh_token()
38+
refresh_response_key = tokens.get_refresh_response_key()
39+
refresh_from = tokens.get_refresh_from()
40+
refresh_expires = tokens.get_refresh_expires()
41+
identity_expires = tokens.get_identity_expires()
42+
json_string = tokens.get_json_string()
43+
44+
print('Status =', token_generate_response.status)
45+
print('Advertising Token =', advertising_token)
46+
print('Refresh Token =', refresh_token)
47+
print('Refresh Response Key =', refresh_response_key)
48+
print('Refresh From =', refresh_from)
49+
print('Refresh Expires =', refresh_expires)
50+
print('Identity Expires =', identity_expires)
51+
print('As Json String =', json_string, "\n")
52+
53+
print("Refreshing Token")
54+
try:
55+
token_refresh_response = publisher_client.refresh_token(tokens)
56+
except Exception as e:
57+
print(e)
58+
# decide how to handle exception
59+
60+
exit(1)
61+
62+
63+
if(token_refresh_response.is_optout()):
64+
print("User has opted out")
65+
exit(0)
66+
67+
tokens = token_refresh_response.get_identity()
68+
advertising_token = tokens.get_advertising_token()
69+
refresh_token = tokens.get_refresh_token()
70+
refresh_response_key = tokens.get_refresh_response_key()
71+
refresh_from = tokens.get_refresh_from()
72+
refresh_expires = tokens.get_refresh_expires()
73+
identity_expires = tokens.get_identity_expires()
74+
json_string = tokens.get_json_string()
75+
76+
print('Status =', token_generate_response.status)
77+
print('As Json String =', token_refresh_response.get_identity_json_string())

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
77

88
[project]
99
name = "uid2_client"
10-
version = "2.1.0"
10+
version = "2.2.0"
1111
authors = [
1212
{ name = "UID2 team", email = "[email protected]" }
1313
]

tests/test_normalization.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import json
2+
import unittest
3+
from unittest.mock import patch
4+
5+
from uid2_client import TokenGenerateInput
6+
from uid2_client.input_util import *
7+
import random
8+
import string
9+
10+
11+
class InputNormalizationTests(unittest.TestCase):
12+
13+
def test_invalid_email_normalization(self):
14+
invalid_test_cases = ["", " @", "@", "a@", "@b", "@b.com", "+", " ", "[email protected]", "[email protected]",
15+
16+
for s in invalid_test_cases:
17+
print("Negative case " + s)
18+
with self.assertRaises(ValueError) as context:
19+
TokenGenerateInput.from_email(s).get_as_json_string()
20+
self.assertTrue("invalid email address" in context.exception)
21+
22+
def test_valid_email_normalization(self):
23+
valid_test_cases = [
24+
["[email protected] ", "[email protected]", "dvECjPKZHya0/SIhSGwP0m8SgTv1vzLxPULUOsm880M="],
25+
["[email protected]", "[email protected]", "dvECjPKZHya0/SIhSGwP0m8SgTv1vzLxPULUOsm880M="],
26+
["[email protected]", "[email protected]", "LkLfFrut8Tc3h/fIvYDiBKSbaMiau/DtaLBPQYszdMw="],
27+
["[email protected]", "[email protected]", "rQ4yzdOz4uG8N54326QyZD6/JwqrXn4lmy34cVCojB8="],
28+
["[email protected]", "[email protected]", "weFizOVVWKlLfyorbBU8oxYDv4HJtTZCPMyZ4THzUQE="],
29+
["[email protected]", "[email protected]", "h5JGBrQTGorO7q6IaFMfu5cSqqB6XTp1aybOD11spnQ="],
30+
["[email protected]", "[email protected]", "d1Lr/s4GLLX3SvQVMoQdIMfbQPMAGZYry+2V+0pZlQg="],
31+
[" [email protected]", "[email protected]", "d1Lr/s4GLLX3SvQVMoQdIMfbQPMAGZYry+2V+0pZlQg="],
32+
["[email protected] ", "[email protected]", "d1Lr/s4GLLX3SvQVMoQdIMfbQPMAGZYry+2V+0pZlQg="],
33+
[" [email protected] ", "[email protected]", "d1Lr/s4GLLX3SvQVMoQdIMfbQPMAGZYry+2V+0pZlQg="],
34+
[" [email protected] ", "[email protected]", "d1Lr/s4GLLX3SvQVMoQdIMfbQPMAGZYry+2V+0pZlQg="],
35+
[" [email protected]", "[email protected]", "LkLfFrut8Tc3h/fIvYDiBKSbaMiau/DtaLBPQYszdMw="],
36+
["[email protected] ", "[email protected]", "LkLfFrut8Tc3h/fIvYDiBKSbaMiau/DtaLBPQYszdMw="],
37+
[" [email protected] ", "[email protected]", "LkLfFrut8Tc3h/fIvYDiBKSbaMiau/DtaLBPQYszdMw="],
38+
[" [email protected] ", "[email protected]", "LkLfFrut8Tc3h/fIvYDiBKSbaMiau/DtaLBPQYszdMw="],
39+
["[email protected] ", "[email protected]", "LkLfFrut8Tc3h/fIvYDiBKSbaMiau/DtaLBPQYszdMw="],
40+
["[email protected] ", "[email protected]", "LkLfFrut8Tc3h/fIvYDiBKSbaMiau/DtaLBPQYszdMw="],
41+
[" [email protected] ", "[email protected]", "LkLfFrut8Tc3h/fIvYDiBKSbaMiau/DtaLBPQYszdMw="],
42+
["[email protected]", "[email protected]", "LkLfFrut8Tc3h/fIvYDiBKSbaMiau/DtaLBPQYszdMw="],
43+
["[email protected]", "[email protected]", "LkLfFrut8Tc3h/fIvYDiBKSbaMiau/DtaLBPQYszdMw="],
44+
["[email protected]", "[email protected]", "LkLfFrut8Tc3h/fIvYDiBKSbaMiau/DtaLBPQYszdMw="],
45+
["[email protected] ", "[email protected]", "dvECjPKZHya0/SIhSGwP0m8SgTv1vzLxPULUOsm880M="],
46+
["[email protected] ", "[email protected]", "dvECjPKZHya0/SIhSGwP0m8SgTv1vzLxPULUOsm880M="],
47+
48+
"fAFEUqApQ0V/M9mLj/IO54CgKgtQuARKsOMqtFklD4k="],
49+
["testtest@😊test.com", "testtest@😊test.com",
50+
"tcng5pttf7Y2z4ylZTROvIMw1+IVrMpR4D1KeXSrdiM="],
51+
52+
"0qI21FPLkuez/8RswfmircHPYz9Dtf7/Nch1rSWEQf0="],
53+
]
54+
55+
for test_case in valid_test_cases:
56+
print(f"Positive Test case {test_case[0]} Expected: {test_case[1]}")
57+
normalized = normalize_email_string(test_case[0])
58+
json_string = TokenGenerateInput.from_email(test_case[0]).get_as_json_string()
59+
hashed = json.loads(json_string)["email_hash"]
60+
self.assertEqual(test_case[1], normalized)
61+
self.assertEqual(test_case[2], hashed)
62+
63+
64+
def test_phone_number_is_normalized_negative(self):
65+
test_cases = [
66+
None,
67+
"",
68+
"asdaksjdakfj",
69+
"DH5qQFhi5ALrdqcPiib8cy0Hwykx6frpqxWCkR0uijs",
70+
"QFhi5ALrdqcPiib8cy0Hwykx6frpqxWCkR0uijs",
71+
"06a418f467a14e1631a317b107548a1039d26f12ea45301ab14e7684b36ede58",
72+
"0C7E6A405862E402EB76A70F8A26FC732D07C32931E9FAE9AB1582911D2E8A3B",
73+
"+",
74+
"12345678",
75+
"123456789",
76+
"1234567890",
77+
"+12345678",
78+
"+123456789",
79+
"+ 12345678",
80+
"+ 123456789",
81+
"+ 1234 5678",
82+
"+ 1234 56789",
83+
"+1234567890123456",
84+
"+1234567890A",
85+
"+1234567890 ",
86+
"+1234567890+",
87+
"+12345+67890",
88+
"555-555-5555",
89+
"(555) 555-5555"
90+
]
91+
92+
for s in test_cases:
93+
print(f"Testing phone number '{s}'")
94+
with self.assertRaises(ValueError) as context:
95+
TokenGenerateInput.from_phone(s).get_as_json_string()
96+
self.assertTrue("phone number is not normalized" in context.exception)
97+
98+
def test_phone_number_is_normalized_positive(self):
99+
test_cases = [
100+
"+1234567890",
101+
"+12345678901",
102+
"+123456789012",
103+
"+1234567890123",
104+
"+12345678901234",
105+
"+123456789012345"
106+
]
107+
108+
for s in test_cases:
109+
print(f"Testing phone number '{s}'")
110+
self.assertTrue(is_phone_number_normalized(s))
111+

0 commit comments

Comments
 (0)