dub 0.34.1__py3-none-any.whl → 0.35.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.
Files changed (93) hide show
  1. dub/_version.py +3 -3
  2. dub/basesdk.py +20 -6
  3. dub/models/components/__init__.py +108 -26
  4. dub/models/components/analyticsbrowsers.py +18 -1
  5. dub/models/components/analyticscities.py +18 -1
  6. dub/models/components/analyticscontinents.py +18 -1
  7. dub/models/components/analyticscount.py +18 -1
  8. dub/models/components/analyticscountries.py +20 -1
  9. dub/models/components/analyticsdevices.py +18 -1
  10. dub/models/components/analyticsos.py +18 -1
  11. dub/models/components/analyticsreferers.py +18 -1
  12. dub/models/components/analyticsrefererurls.py +18 -1
  13. dub/models/components/analyticsregions.py +18 -1
  14. dub/models/components/analyticstimeseries.py +18 -1
  15. dub/models/components/analyticstoplinks.py +16 -26
  16. dub/models/components/analyticstopurls.py +18 -1
  17. dub/models/components/analyticstriggers.py +18 -1
  18. dub/models/components/commissioncreatedevent.py +96 -64
  19. dub/models/components/domainschema.py +31 -50
  20. dub/models/components/folderschema.py +18 -19
  21. dub/models/components/leadcreatedevent.py +151 -134
  22. dub/models/components/linkclickedevent.py +57 -70
  23. dub/models/components/linkschema.py +63 -64
  24. dub/models/components/linkwebhookevent.py +43 -51
  25. dub/models/components/partneranalyticscount.py +18 -1
  26. dub/models/components/partneranalyticstimeseries.py +18 -1
  27. dub/models/components/partneranalyticstoplinks.py +16 -27
  28. dub/models/components/partnerapplicationsubmittedevent.py +42 -75
  29. dub/models/components/partnerenrolledevent.py +477 -83
  30. dub/models/components/salecreatedevent.py +152 -151
  31. dub/models/errors/badrequest.py +18 -1
  32. dub/models/errors/conflict.py +18 -1
  33. dub/models/errors/forbidden.py +18 -1
  34. dub/models/errors/internalservererror.py +18 -1
  35. dub/models/errors/inviteexpired.py +18 -1
  36. dub/models/errors/notfound.py +18 -1
  37. dub/models/errors/ratelimitexceeded.py +18 -1
  38. dub/models/errors/unauthorized.py +18 -1
  39. dub/models/errors/unprocessableentity.py +18 -1
  40. dub/models/operations/__init__.py +230 -19
  41. dub/models/operations/approvebountysubmission.py +71 -45
  42. dub/models/operations/banpartner.py +14 -19
  43. dub/models/operations/bulkcreatelinks.py +86 -87
  44. dub/models/operations/bulkupdatelinks.py +97 -82
  45. dub/models/operations/checkdomainstatus.py +1 -17
  46. dub/models/operations/createdomain.py +33 -34
  47. dub/models/operations/createfolder.py +18 -19
  48. dub/models/operations/createlink.py +86 -87
  49. dub/models/operations/createpartner.py +560 -168
  50. dub/models/operations/createpartnerlink.py +74 -85
  51. dub/models/operations/createreferralsembedtoken.py +99 -87
  52. dub/models/operations/createtag.py +18 -1
  53. dub/models/operations/deactivatepartner.py +65 -0
  54. dub/models/operations/getcustomer.py +106 -105
  55. dub/models/operations/getcustomers.py +123 -105
  56. dub/models/operations/getlinkinfo.py +18 -1
  57. dub/models/operations/getlinks.py +36 -1
  58. dub/models/operations/getlinkscount.py +32 -1
  59. dub/models/operations/getqrcode.py +29 -1
  60. dub/models/operations/gettags.py +20 -1
  61. dub/models/operations/listbountysubmissions.py +63 -26
  62. dub/models/operations/listcommissions.py +129 -64
  63. dub/models/operations/listdomains.py +18 -1
  64. dub/models/operations/listevents.py +414 -389
  65. dub/models/operations/listfolders.py +18 -1
  66. dub/models/operations/listpartners.py +510 -84
  67. dub/models/operations/registerdomain.py +1 -17
  68. dub/models/operations/rejectbountysubmission.py +71 -26
  69. dub/models/operations/retrieveanalytics.py +65 -66
  70. dub/models/operations/retrievelinks.py +30 -19
  71. dub/models/operations/retrievepartneranalytics.py +25 -28
  72. dub/models/operations/tracklead.py +38 -83
  73. dub/models/operations/tracksale.py +52 -95
  74. dub/models/operations/updatecommission.py +126 -64
  75. dub/models/operations/updatecustomer.py +122 -131
  76. dub/models/operations/updatedomain.py +50 -35
  77. dub/models/operations/updatefolder.py +34 -19
  78. dub/models/operations/updatelink.py +101 -86
  79. dub/models/operations/updatetag.py +34 -1
  80. dub/models/operations/upsertlink.py +86 -87
  81. dub/models/operations/upsertpartnerlink.py +72 -78
  82. dub/partners.py +288 -0
  83. dub/sdk.py +0 -3
  84. dub/utils/__init__.py +10 -1
  85. {dub-0.34.1.dist-info → dub-0.35.0.dist-info}/METADATA +4 -8
  86. dub-0.35.0.dist-info/RECORD +143 -0
  87. dub/models/components/workspaceschema.py +0 -328
  88. dub/models/operations/getworkspace.py +0 -21
  89. dub/models/operations/updateworkspace.py +0 -78
  90. dub/workspaces.py +0 -561
  91. dub-0.34.1.dist-info/RECORD +0 -146
  92. {dub-0.34.1.dist-info → dub-0.35.0.dist-info}/WHEEL +0 -0
  93. {dub-0.34.1.dist-info → dub-0.35.0.dist-info}/licenses/LICENSE +0 -0
@@ -30,6 +30,22 @@ class GetCustomerRequest(BaseModel):
30
30
  ] = None
31
31
  r"""Whether to include expanded fields on the customer (`link`, `partner`, `discount`)."""
32
32
 
33
+ @model_serializer(mode="wrap")
34
+ def serialize_model(self, handler):
35
+ optional_fields = set(["includeExpandedFields"])
36
+ serialized = handler(self)
37
+ m = {}
38
+
39
+ for n, f in type(self).model_fields.items():
40
+ k = f.alias or n
41
+ val = serialized.get(k)
42
+
43
+ if val != UNSET_SENTINEL:
44
+ if val is not None or k not in optional_fields:
45
+ m[k] = val
46
+
47
+ return m
48
+
33
49
 
34
50
  class GetCustomerLinkTypedDict(TypedDict):
35
51
  id: str
@@ -67,30 +83,14 @@ class GetCustomerLink(BaseModel):
67
83
 
68
84
  @model_serializer(mode="wrap")
69
85
  def serialize_model(self, handler):
70
- optional_fields = []
71
- nullable_fields = ["programId"]
72
- null_default_fields = []
73
-
74
86
  serialized = handler(self)
75
-
76
87
  m = {}
77
88
 
78
89
  for n, f in type(self).model_fields.items():
79
90
  k = f.alias or n
80
91
  val = serialized.get(k)
81
- serialized.pop(k, None)
82
-
83
- optional_nullable = k in optional_fields and k in nullable_fields
84
- is_set = (
85
- self.__pydantic_fields_set__.intersection({n})
86
- or k in null_default_fields
87
- ) # pylint: disable=no-member
88
92
 
89
- if val is not None and val != UNSET_SENTINEL:
90
- m[k] = val
91
- elif val != UNSET_SENTINEL and (
92
- not k in optional_fields or (optional_nullable and is_set)
93
- ):
93
+ if val != UNSET_SENTINEL:
94
94
  m[k] = val
95
95
 
96
96
  return m
@@ -122,30 +122,14 @@ class GetCustomerPartner(BaseModel):
122
122
 
123
123
  @model_serializer(mode="wrap")
124
124
  def serialize_model(self, handler):
125
- optional_fields = []
126
- nullable_fields = ["email", "image"]
127
- null_default_fields = []
128
-
129
125
  serialized = handler(self)
130
-
131
126
  m = {}
132
127
 
133
128
  for n, f in type(self).model_fields.items():
134
129
  k = f.alias or n
135
130
  val = serialized.get(k)
136
- serialized.pop(k, None)
137
131
 
138
- optional_nullable = k in optional_fields and k in nullable_fields
139
- is_set = (
140
- self.__pydantic_fields_set__.intersection({n})
141
- or k in null_default_fields
142
- ) # pylint: disable=no-member
143
-
144
- if val is not None and val != UNSET_SENTINEL:
145
- m[k] = val
146
- elif val != UNSET_SENTINEL and (
147
- not k in optional_fields or (optional_nullable and is_set)
148
- ):
132
+ if val != UNSET_SENTINEL:
149
133
  m[k] = val
150
134
 
151
135
  return m
@@ -188,37 +172,28 @@ class GetCustomerDiscount(BaseModel):
188
172
 
189
173
  @model_serializer(mode="wrap")
190
174
  def serialize_model(self, handler):
191
- optional_fields = ["description", "partnersCount"]
192
- nullable_fields = [
193
- "maxDuration",
194
- "couponId",
195
- "couponTestId",
196
- "description",
197
- "partnersCount",
198
- ]
199
- null_default_fields = []
200
-
175
+ optional_fields = set(["description", "partnersCount"])
176
+ nullable_fields = set(
177
+ ["maxDuration", "couponId", "couponTestId", "description", "partnersCount"]
178
+ )
201
179
  serialized = handler(self)
202
-
203
180
  m = {}
204
181
 
205
182
  for n, f in type(self).model_fields.items():
206
183
  k = f.alias or n
207
184
  val = serialized.get(k)
208
- serialized.pop(k, None)
209
-
210
- optional_nullable = k in optional_fields and k in nullable_fields
211
- is_set = (
212
- self.__pydantic_fields_set__.intersection({n})
213
- or k in null_default_fields
214
- ) # pylint: disable=no-member
215
-
216
- if val is not None and val != UNSET_SENTINEL:
217
- m[k] = val
218
- elif val != UNSET_SENTINEL and (
219
- not k in optional_fields or (optional_nullable and is_set)
220
- ):
221
- m[k] = val
185
+ is_nullable_and_explicitly_set = (
186
+ k in nullable_fields
187
+ and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
188
+ )
189
+
190
+ if val != UNSET_SENTINEL:
191
+ if (
192
+ val is not None
193
+ or k not in optional_fields
194
+ or is_nullable_and_explicitly_set
195
+ ):
196
+ m[k] = val
222
197
 
223
198
  return m
224
199
 
@@ -228,22 +203,28 @@ class GetCustomerResponseBodyTypedDict(TypedDict):
228
203
 
229
204
  id: str
230
205
  r"""The unique ID of the customer. You may use either the customer's `id` on Dub (obtained via `/customers` endpoint) or their `externalId` (unique ID within your system, prefixed with `ext_`, e.g. `ext_123`)."""
231
- external_id: str
232
- r"""Unique identifier for the customer in the client's app."""
233
206
  name: str
234
207
  r"""Name of the customer."""
208
+ external_id: str
209
+ r"""Unique identifier for the customer in the client's app."""
235
210
  created_at: str
236
- r"""The date the customer was created."""
211
+ r"""The date the customer was created (usually the signup date or trial start date)."""
237
212
  email: NotRequired[Nullable[str]]
238
213
  r"""Email of the customer."""
239
214
  avatar: NotRequired[Nullable[str]]
240
215
  r"""Avatar URL of the customer."""
216
+ stripe_customer_id: NotRequired[Nullable[str]]
217
+ r"""The customer's Stripe customer ID. This is useful for attributing recurring sale events to the partner who referred the customer."""
241
218
  country: NotRequired[Nullable[str]]
242
219
  r"""Country of the customer."""
243
220
  sales: NotRequired[Nullable[float]]
244
221
  r"""Total number of sales for the customer."""
245
222
  sale_amount: NotRequired[Nullable[float]]
246
223
  r"""Total amount of sales for the customer."""
224
+ first_sale_at: NotRequired[Nullable[str]]
225
+ r"""The date the customer made their first sale. Useful for calculating the time to first sale and LTV."""
226
+ subscription_canceled_at: NotRequired[Nullable[str]]
227
+ r"""The date the customer canceled their subscription. Useful for calculating LTV and churn rate."""
247
228
  link: NotRequired[Nullable[GetCustomerLinkTypedDict]]
248
229
  program_id: NotRequired[Nullable[str]]
249
230
  partner: NotRequired[Nullable[GetCustomerPartnerTypedDict]]
@@ -256,14 +237,14 @@ class GetCustomerResponseBody(BaseModel):
256
237
  id: str
257
238
  r"""The unique ID of the customer. You may use either the customer's `id` on Dub (obtained via `/customers` endpoint) or their `externalId` (unique ID within your system, prefixed with `ext_`, e.g. `ext_123`)."""
258
239
 
259
- external_id: Annotated[str, pydantic.Field(alias="externalId")]
260
- r"""Unique identifier for the customer in the client's app."""
261
-
262
240
  name: str
263
241
  r"""Name of the customer."""
264
242
 
243
+ external_id: Annotated[str, pydantic.Field(alias="externalId")]
244
+ r"""Unique identifier for the customer in the client's app."""
245
+
265
246
  created_at: Annotated[str, pydantic.Field(alias="createdAt")]
266
- r"""The date the customer was created."""
247
+ r"""The date the customer was created (usually the signup date or trial start date)."""
267
248
 
268
249
  email: OptionalNullable[str] = UNSET
269
250
  r"""Email of the customer."""
@@ -271,6 +252,11 @@ class GetCustomerResponseBody(BaseModel):
271
252
  avatar: OptionalNullable[str] = UNSET
272
253
  r"""Avatar URL of the customer."""
273
254
 
255
+ stripe_customer_id: Annotated[
256
+ OptionalNullable[str], pydantic.Field(alias="stripeCustomerId")
257
+ ] = UNSET
258
+ r"""The customer's Stripe customer ID. This is useful for attributing recurring sale events to the partner who referred the customer."""
259
+
274
260
  country: OptionalNullable[str] = UNSET
275
261
  r"""Country of the customer."""
276
262
 
@@ -282,6 +268,16 @@ class GetCustomerResponseBody(BaseModel):
282
268
  ] = UNSET
283
269
  r"""Total amount of sales for the customer."""
284
270
 
271
+ first_sale_at: Annotated[
272
+ OptionalNullable[str], pydantic.Field(alias="firstSaleAt")
273
+ ] = UNSET
274
+ r"""The date the customer made their first sale. Useful for calculating the time to first sale and LTV."""
275
+
276
+ subscription_canceled_at: Annotated[
277
+ OptionalNullable[str], pydantic.Field(alias="subscriptionCanceledAt")
278
+ ] = UNSET
279
+ r"""The date the customer canceled their subscription. Useful for calculating LTV and churn rate."""
280
+
285
281
  link: OptionalNullable[GetCustomerLink] = UNSET
286
282
 
287
283
  program_id: Annotated[OptionalNullable[str], pydantic.Field(alias="programId")] = (
@@ -294,50 +290,55 @@ class GetCustomerResponseBody(BaseModel):
294
290
 
295
291
  @model_serializer(mode="wrap")
296
292
  def serialize_model(self, handler):
297
- optional_fields = [
298
- "email",
299
- "avatar",
300
- "country",
301
- "sales",
302
- "saleAmount",
303
- "link",
304
- "programId",
305
- "partner",
306
- "discount",
307
- ]
308
- nullable_fields = [
309
- "email",
310
- "avatar",
311
- "country",
312
- "sales",
313
- "saleAmount",
314
- "link",
315
- "programId",
316
- "partner",
317
- "discount",
318
- ]
319
- null_default_fields = []
320
-
293
+ optional_fields = set(
294
+ [
295
+ "email",
296
+ "avatar",
297
+ "stripeCustomerId",
298
+ "country",
299
+ "sales",
300
+ "saleAmount",
301
+ "firstSaleAt",
302
+ "subscriptionCanceledAt",
303
+ "link",
304
+ "programId",
305
+ "partner",
306
+ "discount",
307
+ ]
308
+ )
309
+ nullable_fields = set(
310
+ [
311
+ "email",
312
+ "avatar",
313
+ "stripeCustomerId",
314
+ "country",
315
+ "sales",
316
+ "saleAmount",
317
+ "firstSaleAt",
318
+ "subscriptionCanceledAt",
319
+ "link",
320
+ "programId",
321
+ "partner",
322
+ "discount",
323
+ ]
324
+ )
321
325
  serialized = handler(self)
322
-
323
326
  m = {}
324
327
 
325
328
  for n, f in type(self).model_fields.items():
326
329
  k = f.alias or n
327
330
  val = serialized.get(k)
328
- serialized.pop(k, None)
329
-
330
- optional_nullable = k in optional_fields and k in nullable_fields
331
- is_set = (
332
- self.__pydantic_fields_set__.intersection({n})
333
- or k in null_default_fields
334
- ) # pylint: disable=no-member
335
-
336
- if val is not None and val != UNSET_SENTINEL:
337
- m[k] = val
338
- elif val != UNSET_SENTINEL and (
339
- not k in optional_fields or (optional_nullable and is_set)
340
- ):
341
- m[k] = val
331
+ is_nullable_and_explicitly_set = (
332
+ k in nullable_fields
333
+ and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
334
+ )
335
+
336
+ if val != UNSET_SENTINEL:
337
+ if (
338
+ val is not None
339
+ or k not in optional_fields
340
+ or is_nullable_and_explicitly_set
341
+ ):
342
+ m[k] = val
342
343
 
343
344
  return m
@@ -15,6 +15,8 @@ class GetCustomersQueryParamSortBy(str, Enum):
15
15
 
16
16
  CREATED_AT = "createdAt"
17
17
  SALE_AMOUNT = "saleAmount"
18
+ FIRST_SALE_AT = "firstSaleAt"
19
+ SUBSCRIPTION_CANCELED_AT = "subscriptionCanceledAt"
18
20
 
19
21
 
20
22
  class GetCustomersQueryParamSortOrder(str, Enum):
@@ -132,6 +134,37 @@ class GetCustomersRequest(BaseModel):
132
134
  ] = 100
133
135
  r"""The number of items per page."""
134
136
 
137
+ @model_serializer(mode="wrap")
138
+ def serialize_model(self, handler):
139
+ optional_fields = set(
140
+ [
141
+ "email",
142
+ "externalId",
143
+ "search",
144
+ "country",
145
+ "linkId",
146
+ "programId",
147
+ "partnerId",
148
+ "includeExpandedFields",
149
+ "sortBy",
150
+ "sortOrder",
151
+ "page",
152
+ "pageSize",
153
+ ]
154
+ )
155
+ serialized = handler(self)
156
+ m = {}
157
+
158
+ for n, f in type(self).model_fields.items():
159
+ k = f.alias or n
160
+ val = serialized.get(k)
161
+
162
+ if val != UNSET_SENTINEL:
163
+ if val is not None or k not in optional_fields:
164
+ m[k] = val
165
+
166
+ return m
167
+
135
168
 
136
169
  class GetCustomersLinkTypedDict(TypedDict):
137
170
  id: str
@@ -169,30 +202,14 @@ class GetCustomersLink(BaseModel):
169
202
 
170
203
  @model_serializer(mode="wrap")
171
204
  def serialize_model(self, handler):
172
- optional_fields = []
173
- nullable_fields = ["programId"]
174
- null_default_fields = []
175
-
176
205
  serialized = handler(self)
177
-
178
206
  m = {}
179
207
 
180
208
  for n, f in type(self).model_fields.items():
181
209
  k = f.alias or n
182
210
  val = serialized.get(k)
183
- serialized.pop(k, None)
184
-
185
- optional_nullable = k in optional_fields and k in nullable_fields
186
- is_set = (
187
- self.__pydantic_fields_set__.intersection({n})
188
- or k in null_default_fields
189
- ) # pylint: disable=no-member
190
211
 
191
- if val is not None and val != UNSET_SENTINEL:
192
- m[k] = val
193
- elif val != UNSET_SENTINEL and (
194
- not k in optional_fields or (optional_nullable and is_set)
195
- ):
212
+ if val != UNSET_SENTINEL:
196
213
  m[k] = val
197
214
 
198
215
  return m
@@ -224,30 +241,14 @@ class GetCustomersPartner(BaseModel):
224
241
 
225
242
  @model_serializer(mode="wrap")
226
243
  def serialize_model(self, handler):
227
- optional_fields = []
228
- nullable_fields = ["email", "image"]
229
- null_default_fields = []
230
-
231
244
  serialized = handler(self)
232
-
233
245
  m = {}
234
246
 
235
247
  for n, f in type(self).model_fields.items():
236
248
  k = f.alias or n
237
249
  val = serialized.get(k)
238
- serialized.pop(k, None)
239
250
 
240
- optional_nullable = k in optional_fields and k in nullable_fields
241
- is_set = (
242
- self.__pydantic_fields_set__.intersection({n})
243
- or k in null_default_fields
244
- ) # pylint: disable=no-member
245
-
246
- if val is not None and val != UNSET_SENTINEL:
247
- m[k] = val
248
- elif val != UNSET_SENTINEL and (
249
- not k in optional_fields or (optional_nullable and is_set)
250
- ):
251
+ if val != UNSET_SENTINEL:
251
252
  m[k] = val
252
253
 
253
254
  return m
@@ -290,37 +291,28 @@ class Discount(BaseModel):
290
291
 
291
292
  @model_serializer(mode="wrap")
292
293
  def serialize_model(self, handler):
293
- optional_fields = ["description", "partnersCount"]
294
- nullable_fields = [
295
- "maxDuration",
296
- "couponId",
297
- "couponTestId",
298
- "description",
299
- "partnersCount",
300
- ]
301
- null_default_fields = []
302
-
294
+ optional_fields = set(["description", "partnersCount"])
295
+ nullable_fields = set(
296
+ ["maxDuration", "couponId", "couponTestId", "description", "partnersCount"]
297
+ )
303
298
  serialized = handler(self)
304
-
305
299
  m = {}
306
300
 
307
301
  for n, f in type(self).model_fields.items():
308
302
  k = f.alias or n
309
303
  val = serialized.get(k)
310
- serialized.pop(k, None)
311
-
312
- optional_nullable = k in optional_fields and k in nullable_fields
313
- is_set = (
314
- self.__pydantic_fields_set__.intersection({n})
315
- or k in null_default_fields
316
- ) # pylint: disable=no-member
317
-
318
- if val is not None and val != UNSET_SENTINEL:
319
- m[k] = val
320
- elif val != UNSET_SENTINEL and (
321
- not k in optional_fields or (optional_nullable and is_set)
322
- ):
323
- m[k] = val
304
+ is_nullable_and_explicitly_set = (
305
+ k in nullable_fields
306
+ and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
307
+ )
308
+
309
+ if val != UNSET_SENTINEL:
310
+ if (
311
+ val is not None
312
+ or k not in optional_fields
313
+ or is_nullable_and_explicitly_set
314
+ ):
315
+ m[k] = val
324
316
 
325
317
  return m
326
318
 
@@ -328,22 +320,28 @@ class Discount(BaseModel):
328
320
  class GetCustomersResponseBodyTypedDict(TypedDict):
329
321
  id: str
330
322
  r"""The unique ID of the customer. You may use either the customer's `id` on Dub (obtained via `/customers` endpoint) or their `externalId` (unique ID within your system, prefixed with `ext_`, e.g. `ext_123`)."""
331
- external_id: str
332
- r"""Unique identifier for the customer in the client's app."""
333
323
  name: str
334
324
  r"""Name of the customer."""
325
+ external_id: str
326
+ r"""Unique identifier for the customer in the client's app."""
335
327
  created_at: str
336
- r"""The date the customer was created."""
328
+ r"""The date the customer was created (usually the signup date or trial start date)."""
337
329
  email: NotRequired[Nullable[str]]
338
330
  r"""Email of the customer."""
339
331
  avatar: NotRequired[Nullable[str]]
340
332
  r"""Avatar URL of the customer."""
333
+ stripe_customer_id: NotRequired[Nullable[str]]
334
+ r"""The customer's Stripe customer ID. This is useful for attributing recurring sale events to the partner who referred the customer."""
341
335
  country: NotRequired[Nullable[str]]
342
336
  r"""Country of the customer."""
343
337
  sales: NotRequired[Nullable[float]]
344
338
  r"""Total number of sales for the customer."""
345
339
  sale_amount: NotRequired[Nullable[float]]
346
340
  r"""Total amount of sales for the customer."""
341
+ first_sale_at: NotRequired[Nullable[str]]
342
+ r"""The date the customer made their first sale. Useful for calculating the time to first sale and LTV."""
343
+ subscription_canceled_at: NotRequired[Nullable[str]]
344
+ r"""The date the customer canceled their subscription. Useful for calculating LTV and churn rate."""
347
345
  link: NotRequired[Nullable[GetCustomersLinkTypedDict]]
348
346
  program_id: NotRequired[Nullable[str]]
349
347
  partner: NotRequired[Nullable[GetCustomersPartnerTypedDict]]
@@ -354,14 +352,14 @@ class GetCustomersResponseBody(BaseModel):
354
352
  id: str
355
353
  r"""The unique ID of the customer. You may use either the customer's `id` on Dub (obtained via `/customers` endpoint) or their `externalId` (unique ID within your system, prefixed with `ext_`, e.g. `ext_123`)."""
356
354
 
357
- external_id: Annotated[str, pydantic.Field(alias="externalId")]
358
- r"""Unique identifier for the customer in the client's app."""
359
-
360
355
  name: str
361
356
  r"""Name of the customer."""
362
357
 
358
+ external_id: Annotated[str, pydantic.Field(alias="externalId")]
359
+ r"""Unique identifier for the customer in the client's app."""
360
+
363
361
  created_at: Annotated[str, pydantic.Field(alias="createdAt")]
364
- r"""The date the customer was created."""
362
+ r"""The date the customer was created (usually the signup date or trial start date)."""
365
363
 
366
364
  email: OptionalNullable[str] = UNSET
367
365
  r"""Email of the customer."""
@@ -369,6 +367,11 @@ class GetCustomersResponseBody(BaseModel):
369
367
  avatar: OptionalNullable[str] = UNSET
370
368
  r"""Avatar URL of the customer."""
371
369
 
370
+ stripe_customer_id: Annotated[
371
+ OptionalNullable[str], pydantic.Field(alias="stripeCustomerId")
372
+ ] = UNSET
373
+ r"""The customer's Stripe customer ID. This is useful for attributing recurring sale events to the partner who referred the customer."""
374
+
372
375
  country: OptionalNullable[str] = UNSET
373
376
  r"""Country of the customer."""
374
377
 
@@ -380,6 +383,16 @@ class GetCustomersResponseBody(BaseModel):
380
383
  ] = UNSET
381
384
  r"""Total amount of sales for the customer."""
382
385
 
386
+ first_sale_at: Annotated[
387
+ OptionalNullable[str], pydantic.Field(alias="firstSaleAt")
388
+ ] = UNSET
389
+ r"""The date the customer made their first sale. Useful for calculating the time to first sale and LTV."""
390
+
391
+ subscription_canceled_at: Annotated[
392
+ OptionalNullable[str], pydantic.Field(alias="subscriptionCanceledAt")
393
+ ] = UNSET
394
+ r"""The date the customer canceled their subscription. Useful for calculating LTV and churn rate."""
395
+
383
396
  link: OptionalNullable[GetCustomersLink] = UNSET
384
397
 
385
398
  program_id: Annotated[OptionalNullable[str], pydantic.Field(alias="programId")] = (
@@ -392,50 +405,55 @@ class GetCustomersResponseBody(BaseModel):
392
405
 
393
406
  @model_serializer(mode="wrap")
394
407
  def serialize_model(self, handler):
395
- optional_fields = [
396
- "email",
397
- "avatar",
398
- "country",
399
- "sales",
400
- "saleAmount",
401
- "link",
402
- "programId",
403
- "partner",
404
- "discount",
405
- ]
406
- nullable_fields = [
407
- "email",
408
- "avatar",
409
- "country",
410
- "sales",
411
- "saleAmount",
412
- "link",
413
- "programId",
414
- "partner",
415
- "discount",
416
- ]
417
- null_default_fields = []
418
-
408
+ optional_fields = set(
409
+ [
410
+ "email",
411
+ "avatar",
412
+ "stripeCustomerId",
413
+ "country",
414
+ "sales",
415
+ "saleAmount",
416
+ "firstSaleAt",
417
+ "subscriptionCanceledAt",
418
+ "link",
419
+ "programId",
420
+ "partner",
421
+ "discount",
422
+ ]
423
+ )
424
+ nullable_fields = set(
425
+ [
426
+ "email",
427
+ "avatar",
428
+ "stripeCustomerId",
429
+ "country",
430
+ "sales",
431
+ "saleAmount",
432
+ "firstSaleAt",
433
+ "subscriptionCanceledAt",
434
+ "link",
435
+ "programId",
436
+ "partner",
437
+ "discount",
438
+ ]
439
+ )
419
440
  serialized = handler(self)
420
-
421
441
  m = {}
422
442
 
423
443
  for n, f in type(self).model_fields.items():
424
444
  k = f.alias or n
425
445
  val = serialized.get(k)
426
- serialized.pop(k, None)
427
-
428
- optional_nullable = k in optional_fields and k in nullable_fields
429
- is_set = (
430
- self.__pydantic_fields_set__.intersection({n})
431
- or k in null_default_fields
432
- ) # pylint: disable=no-member
433
-
434
- if val is not None and val != UNSET_SENTINEL:
435
- m[k] = val
436
- elif val != UNSET_SENTINEL and (
437
- not k in optional_fields or (optional_nullable and is_set)
438
- ):
439
- m[k] = val
446
+ is_nullable_and_explicitly_set = (
447
+ k in nullable_fields
448
+ and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
449
+ )
450
+
451
+ if val != UNSET_SENTINEL:
452
+ if (
453
+ val is not None
454
+ or k not in optional_fields
455
+ or is_nullable_and_explicitly_set
456
+ ):
457
+ m[k] = val
440
458
 
441
459
  return m