paygent-sdk 3.0.1__tar.gz → 4.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.
- {paygent_sdk-3.0.1/paygent_sdk.egg-info → paygent_sdk-4.0.0}/PKG-INFO +1 -1
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/paygent_sdk/__init__.py +11 -6
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/paygent_sdk/client.py +204 -180
- paygent_sdk-4.0.0/paygent_sdk/constants.py +1995 -0
- paygent_sdk-4.0.0/paygent_sdk/models.py +119 -0
- paygent_sdk-4.0.0/paygent_sdk/voice_client.py +316 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/paygent_sdk/wrappers/anthropic_wrapper.py +3 -2
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/paygent_sdk/wrappers/gemini_wrapper.py +7 -6
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/paygent_sdk/wrappers/mistral_wrapper.py +3 -2
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/paygent_sdk/wrappers/openai_wrapper.py +7 -6
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0/paygent_sdk.egg-info}/PKG-INFO +1 -1
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/pyproject.toml +1 -1
- paygent_sdk-3.0.1/paygent_sdk/constants.py +0 -339
- paygent_sdk-3.0.1/paygent_sdk/models.py +0 -546
- paygent_sdk-3.0.1/paygent_sdk/voice_client.py +0 -283
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/LICENSE +0 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/MANIFEST.in +0 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/README.md +0 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/examples/__init__.py +0 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/examples/advanced_usage.py +0 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/examples/basic_usage.py +0 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/examples/constants_usage.py +0 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/paygent_sdk/wrappers/__init__.py +0 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/paygent_sdk/wrappers/langchain_wrapper.py +0 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/paygent_sdk.egg-info/SOURCES.txt +0 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/paygent_sdk.egg-info/dependency_links.txt +0 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/paygent_sdk.egg-info/requires.txt +0 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/paygent_sdk.egg-info/top_level.txt +0 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/requirements.txt +0 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/setup.cfg +0 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/setup.py +0 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/tests/__init__.py +0 -0
- {paygent_sdk-3.0.1 → paygent_sdk-4.0.0}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: paygent-sdk
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0
|
|
4
4
|
Summary: Official Python SDK for Paygent - Track AI usage and costs across multiple providers (OpenAI, Anthropic, Google, DeepSeek, etc.)
|
|
5
5
|
Home-page: https://github.com/paygent/paygent-sdk-python
|
|
6
6
|
Author: Paygent
|
|
@@ -8,8 +8,10 @@ For the Go SDK equivalent, see: https://github.com/paygent/paygent-sdk-go
|
|
|
8
8
|
|
|
9
9
|
from .client import Client
|
|
10
10
|
from .models import (
|
|
11
|
-
UsageData, UsageDataWithStrings, APIRequest,
|
|
12
|
-
SttUsageData, TtsUsageData,
|
|
11
|
+
UsageData, UsageDataWithStrings, APIRequest,
|
|
12
|
+
SttUsageData, TtsUsageData,
|
|
13
|
+
RawUsageData, SendUsageV2Response, CostBreakdown,
|
|
14
|
+
Customer, CustomerCreateOrGetRequest
|
|
13
15
|
)
|
|
14
16
|
from .voice_client import send_stt_usage, send_tts_usage # Import to attach methods to Client
|
|
15
17
|
|
|
@@ -50,14 +52,17 @@ __all__ = [
|
|
|
50
52
|
"UsageData",
|
|
51
53
|
"UsageDataWithStrings",
|
|
52
54
|
"APIRequest",
|
|
53
|
-
"ModelPricing",
|
|
54
|
-
"MODEL_PRICING",
|
|
55
55
|
|
|
56
56
|
# Voice data models
|
|
57
57
|
"SttUsageData",
|
|
58
58
|
"TtsUsageData",
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
|
|
60
|
+
# New V2 & Customer models
|
|
61
|
+
"RawUsageData",
|
|
62
|
+
"SendUsageV2Response",
|
|
63
|
+
"CostBreakdown",
|
|
64
|
+
"Customer",
|
|
65
|
+
"CustomerCreateOrGetRequest",
|
|
61
66
|
|
|
62
67
|
# Wrappers
|
|
63
68
|
"PaygentOpenAI",
|
|
@@ -17,7 +17,16 @@ try:
|
|
|
17
17
|
except ImportError:
|
|
18
18
|
tiktoken = None
|
|
19
19
|
|
|
20
|
-
from .models import
|
|
20
|
+
from .models import (
|
|
21
|
+
UsageData,
|
|
22
|
+
UsageDataWithStrings,
|
|
23
|
+
APIRequest,
|
|
24
|
+
RawUsageData,
|
|
25
|
+
SendUsageV2Response,
|
|
26
|
+
CostBreakdown,
|
|
27
|
+
Customer,
|
|
28
|
+
CustomerCreateOrGetRequest,
|
|
29
|
+
)
|
|
21
30
|
|
|
22
31
|
|
|
23
32
|
class Client:
|
|
@@ -79,52 +88,6 @@ class Client:
|
|
|
79
88
|
"""
|
|
80
89
|
return cls(api_key)
|
|
81
90
|
|
|
82
|
-
def _calculate_cost(self, model: str, usage_data: UsageData) -> float:
|
|
83
|
-
"""
|
|
84
|
-
Calculate the cost based on model and usage data.
|
|
85
|
-
|
|
86
|
-
Args:
|
|
87
|
-
model: The AI model name
|
|
88
|
-
usage_data: Usage data containing token counts
|
|
89
|
-
|
|
90
|
-
Returns:
|
|
91
|
-
Calculated cost in USD
|
|
92
|
-
"""
|
|
93
|
-
pricing = MODEL_PRICING.get(model)
|
|
94
|
-
if not pricing:
|
|
95
|
-
self.logger.warning(f"Unknown model '{model}', using default pricing")
|
|
96
|
-
# Use default pricing for unknown models (per 1000 tokens)
|
|
97
|
-
pricing = ModelPricing(
|
|
98
|
-
prompt_tokens_cost=0.0001, # $0.10 per 1000 tokens
|
|
99
|
-
completion_tokens_cost=0.0001 # $0.10 per 1000 tokens
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
# Calculate cost per 1000 tokens
|
|
103
|
-
prompt_cost = (usage_data.prompt_tokens / 1000.0) * pricing.prompt_tokens_cost
|
|
104
|
-
|
|
105
|
-
# Handle cached tokens: if model doesn't support caching (cached_tokens_cost is None),
|
|
106
|
-
# bill cached tokens at regular prompt token rate
|
|
107
|
-
cached_cost = 0.0
|
|
108
|
-
if usage_data.cached_tokens and usage_data.cached_tokens > 0:
|
|
109
|
-
if pricing.cached_tokens_cost is not None:
|
|
110
|
-
# Model supports caching - use cached token price
|
|
111
|
-
cached_cost = (usage_data.cached_tokens / 1000.0) * pricing.cached_tokens_cost
|
|
112
|
-
else:
|
|
113
|
-
# Model doesn't support caching - bill at prompt token rate
|
|
114
|
-
cached_cost = (usage_data.cached_tokens / 1000.0) * pricing.prompt_tokens_cost
|
|
115
|
-
|
|
116
|
-
completion_cost = (usage_data.completion_tokens / 1000.0) * pricing.completion_tokens_cost
|
|
117
|
-
total_cost = prompt_cost + cached_cost + completion_cost
|
|
118
|
-
|
|
119
|
-
self.logger.debug(
|
|
120
|
-
f"Cost calculation for model '{model}': "
|
|
121
|
-
f"prompt_tokens={usage_data.prompt_tokens} ({prompt_cost:.6f}), "
|
|
122
|
-
f"cached_tokens={usage_data.cached_tokens or 0} ({cached_cost:.6f}), "
|
|
123
|
-
f"completion_tokens={usage_data.completion_tokens} ({completion_cost:.6f}), "
|
|
124
|
-
f"total={total_cost:.6f}"
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
return total_cost
|
|
128
91
|
|
|
129
92
|
def send_usage(
|
|
130
93
|
self,
|
|
@@ -174,67 +137,6 @@ class Client:
|
|
|
174
137
|
# Cost calculated (no logging for performance)
|
|
175
138
|
|
|
176
139
|
# Prepare API request
|
|
177
|
-
api_request = APIRequest(
|
|
178
|
-
agent_id=agent_id,
|
|
179
|
-
customer_id=customer_id,
|
|
180
|
-
indicator=indicator,
|
|
181
|
-
amount=cost
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
# Prepare request data
|
|
185
|
-
request_data = {
|
|
186
|
-
"agentId": api_request.agent_id,
|
|
187
|
-
"customerId": api_request.customer_id,
|
|
188
|
-
"indicator": api_request.indicator,
|
|
189
|
-
"amount": api_request.amount,
|
|
190
|
-
"inputToken": regular_prompt_tokens, # Send non-cached tokens
|
|
191
|
-
"cachedToken": cached_tokens, # Send cached tokens separately
|
|
192
|
-
"outputToken": usage_data.completion_tokens,
|
|
193
|
-
"model": usage_data.model,
|
|
194
|
-
"serviceProvider": usage_data.service_provider
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
self.logger.debug(f"API request body: {json.dumps(request_data)}")
|
|
198
|
-
|
|
199
|
-
# Create HTTP request
|
|
200
|
-
url = urljoin(self.base_url, "/api/v1/usage")
|
|
201
|
-
|
|
202
|
-
headers = {
|
|
203
|
-
"Content-Type": "application/json",
|
|
204
|
-
"paygent-api-key": self.api_key
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
self.logger.debug(f"Making HTTP POST request to: {url}")
|
|
208
|
-
|
|
209
|
-
try:
|
|
210
|
-
# Make HTTP request
|
|
211
|
-
response = self.session.post(
|
|
212
|
-
url,
|
|
213
|
-
json=request_data,
|
|
214
|
-
headers=headers,
|
|
215
|
-
timeout=30
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
self.logger.debug(
|
|
219
|
-
f"API response status: {response.status_code}, "
|
|
220
|
-
f"body: {response.text}"
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
# Check response status
|
|
224
|
-
if 200 <= response.status_code < 300:
|
|
225
|
-
# Success - no logging to minimize verbosity
|
|
226
|
-
return
|
|
227
|
-
|
|
228
|
-
# Handle error response
|
|
229
|
-
self.logger.error(
|
|
230
|
-
f"API request failed with status {response.status_code}: {response.text}"
|
|
231
|
-
)
|
|
232
|
-
response.raise_for_status()
|
|
233
|
-
|
|
234
|
-
except requests.RequestException as e:
|
|
235
|
-
self.logger.error(f"HTTP request failed: {e}")
|
|
236
|
-
raise
|
|
237
|
-
|
|
238
140
|
def set_log_level(self, level: int) -> None:
|
|
239
141
|
"""
|
|
240
142
|
Set the logging level for the client.
|
|
@@ -339,38 +241,34 @@ class Client:
|
|
|
339
241
|
word_count = len(text.split())
|
|
340
242
|
return int(word_count * 1.3)
|
|
341
243
|
|
|
342
|
-
def
|
|
244
|
+
def send_usage(
|
|
245
|
+
self,
|
|
246
|
+
agent_id: str,
|
|
247
|
+
customer_id: str,
|
|
248
|
+
indicator: str,
|
|
249
|
+
usage_data: UsageData
|
|
250
|
+
) -> None:
|
|
343
251
|
"""
|
|
344
|
-
|
|
252
|
+
Send usage data to the Paygent API.
|
|
253
|
+
This legacy method now internally uses the V2 API for server-side pricing.
|
|
345
254
|
|
|
346
255
|
Args:
|
|
347
|
-
|
|
348
|
-
|
|
256
|
+
agent_id: Unique identifier for the agent
|
|
257
|
+
customer_id: Unique identifier for the customer
|
|
258
|
+
indicator: Indicator for the usage event
|
|
259
|
+
usage_data: Usage data containing model and token information
|
|
349
260
|
|
|
350
|
-
|
|
351
|
-
|
|
261
|
+
Raises:
|
|
262
|
+
requests.RequestException: If the HTTP request fails
|
|
352
263
|
"""
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
f"Token counting for model '{model}': "
|
|
360
|
-
f"prompt_tokens={prompt_tokens}, completion_tokens={completion_tokens}, "
|
|
361
|
-
f"total_tokens={total_tokens}"
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
# Create UsageData for cost calculation
|
|
365
|
-
usage_data_obj = UsageData(
|
|
366
|
-
service_provider=usage_data.service_provider,
|
|
367
|
-
model=model,
|
|
368
|
-
prompt_tokens=prompt_tokens,
|
|
369
|
-
completion_tokens=completion_tokens,
|
|
370
|
-
total_tokens=total_tokens
|
|
264
|
+
raw_usage = RawUsageData(
|
|
265
|
+
provider=usage_data.service_provider,
|
|
266
|
+
model=usage_data.model,
|
|
267
|
+
input_tokens=usage_data.prompt_tokens,
|
|
268
|
+
output_tokens=usage_data.completion_tokens,
|
|
269
|
+
cached_tokens=usage_data.cached_tokens
|
|
371
270
|
)
|
|
372
|
-
|
|
373
|
-
return self._calculate_cost(model, usage_data_obj)
|
|
271
|
+
self.send_usage_v2(agent_id, customer_id, indicator, raw_usage)
|
|
374
272
|
|
|
375
273
|
def send_usage_with_token_string(
|
|
376
274
|
self,
|
|
@@ -381,7 +279,8 @@ class Client:
|
|
|
381
279
|
) -> None:
|
|
382
280
|
"""
|
|
383
281
|
Send usage data to the Paygent API using prompt and output strings.
|
|
384
|
-
The function automatically counts tokens using proper tokenizers for each model provider
|
|
282
|
+
The function automatically counts tokens using proper tokenizers for each model provider.
|
|
283
|
+
This method now internally uses the V2 API for server-side pricing.
|
|
385
284
|
|
|
386
285
|
Args:
|
|
387
286
|
agent_id: Unique identifier for the agent
|
|
@@ -393,54 +292,73 @@ class Client:
|
|
|
393
292
|
requests.RequestException: If the HTTP request fails
|
|
394
293
|
ValueError: If the usage data is invalid
|
|
395
294
|
"""
|
|
396
|
-
# Removed verbose logging - only log errors
|
|
397
|
-
# Calculate cost from strings
|
|
398
|
-
try:
|
|
399
|
-
cost = self._calculate_cost_from_strings(usage_data.model, usage_data)
|
|
400
|
-
except Exception as e:
|
|
401
|
-
self.logger.error(f"Failed to calculate cost from strings: {e}")
|
|
402
|
-
raise ValueError(f"Failed to calculate cost from strings: {e}") from e
|
|
403
|
-
|
|
404
|
-
# Cost calculated from strings (no logging for performance)
|
|
405
|
-
|
|
406
295
|
# Calculate token counts for API request
|
|
407
296
|
prompt_tokens = self._get_token_count(usage_data.model, usage_data.prompt_string)
|
|
408
297
|
completion_tokens = self._get_token_count(usage_data.model, usage_data.output_string)
|
|
409
298
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
amount=cost
|
|
299
|
+
raw_usage = RawUsageData(
|
|
300
|
+
provider=usage_data.service_provider,
|
|
301
|
+
model=usage_data.model,
|
|
302
|
+
input_tokens=prompt_tokens,
|
|
303
|
+
output_tokens=completion_tokens
|
|
416
304
|
)
|
|
417
|
-
|
|
305
|
+
self.send_usage_v2(agent_id, customer_id, indicator, raw_usage)
|
|
306
|
+
|
|
307
|
+
def send_usage_v2(
|
|
308
|
+
self,
|
|
309
|
+
agent_id: str,
|
|
310
|
+
customer_id: str,
|
|
311
|
+
indicator: str,
|
|
312
|
+
raw_usage: RawUsageData
|
|
313
|
+
) -> SendUsageV2Response:
|
|
314
|
+
"""
|
|
315
|
+
Send usage data to the Paygent API v2 (backend-calculated pricing).
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
agent_id: Unique identifier for the agent
|
|
319
|
+
customer_id: Unique identifier for the customer
|
|
320
|
+
indicator: Indicator for the usage event
|
|
321
|
+
raw_usage: Raw usage data including model, provider, tokens, etc.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
SendUsageV2Response containing cost breakdown and model metadata
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
requests.RequestException: If the HTTP request fails
|
|
328
|
+
"""
|
|
329
|
+
self.logger.debug(f"Sending V2 usage data for model: {raw_usage.model}")
|
|
330
|
+
|
|
418
331
|
# Prepare request data
|
|
419
332
|
request_data = {
|
|
420
|
-
"agentId":
|
|
421
|
-
"customerId":
|
|
422
|
-
"indicator":
|
|
423
|
-
"
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
"serviceProvider": usage_data.service_provider
|
|
333
|
+
"agentId": agent_id,
|
|
334
|
+
"customerId": customer_id,
|
|
335
|
+
"indicator": indicator,
|
|
336
|
+
"rawUsage": {
|
|
337
|
+
"provider": raw_usage.provider,
|
|
338
|
+
"model": raw_usage.model,
|
|
339
|
+
}
|
|
428
340
|
}
|
|
429
|
-
|
|
430
|
-
self.logger.debug(f"API request body: {json.dumps(request_data)}")
|
|
431
|
-
|
|
432
|
-
# Create HTTP request
|
|
433
|
-
url = urljoin(self.base_url, "/api/v1/usage")
|
|
434
341
|
|
|
342
|
+
# Add optional fields
|
|
343
|
+
if raw_usage.input_tokens is not None:
|
|
344
|
+
request_data["rawUsage"]["inputTokens"] = raw_usage.input_tokens
|
|
345
|
+
if raw_usage.output_tokens is not None:
|
|
346
|
+
request_data["rawUsage"]["outputTokens"] = raw_usage.output_tokens
|
|
347
|
+
if raw_usage.cached_tokens is not None:
|
|
348
|
+
request_data["rawUsage"]["cachedTokens"] = raw_usage.cached_tokens
|
|
349
|
+
if raw_usage.audio_duration is not None:
|
|
350
|
+
request_data["rawUsage"]["audioDuration"] = raw_usage.audio_duration
|
|
351
|
+
if raw_usage.character_count is not None:
|
|
352
|
+
request_data["rawUsage"]["characterCount"] = raw_usage.character_count
|
|
353
|
+
|
|
354
|
+
# Create HTTP request
|
|
355
|
+
url = urljoin(self.base_url, "/api/v2/usage")
|
|
435
356
|
headers = {
|
|
436
357
|
"Content-Type": "application/json",
|
|
437
358
|
"paygent-api-key": self.api_key
|
|
438
359
|
}
|
|
439
|
-
|
|
440
|
-
self.logger.debug(f"Making HTTP POST request to: {url}")
|
|
441
|
-
|
|
360
|
+
|
|
442
361
|
try:
|
|
443
|
-
# Make HTTP request
|
|
444
362
|
response = self.session.post(
|
|
445
363
|
url,
|
|
446
364
|
json=request_data,
|
|
@@ -449,21 +367,127 @@ class Client:
|
|
|
449
367
|
)
|
|
450
368
|
|
|
451
369
|
self.logger.debug(
|
|
452
|
-
f"API response status: {response.status_code}, "
|
|
370
|
+
f"V2 API response status: {response.status_code}, "
|
|
453
371
|
f"body: {response.text}"
|
|
454
372
|
)
|
|
373
|
+
|
|
374
|
+
response.raise_for_status()
|
|
375
|
+
|
|
376
|
+
# Parse response
|
|
377
|
+
response_data = response.json()
|
|
378
|
+
self.logger.info("V2 usage data sent successfully")
|
|
379
|
+
|
|
380
|
+
# Parse breakdown
|
|
381
|
+
breakdown_data = response_data.get("breakdown", {})
|
|
382
|
+
breakdown = CostBreakdown(
|
|
383
|
+
input_cost=breakdown_data.get("inputCost", 0.0),
|
|
384
|
+
output_cost=breakdown_data.get("outputCost", 0.0),
|
|
385
|
+
cached_cost=breakdown_data.get("cachedCost", 0.0),
|
|
386
|
+
audio_cost=breakdown_data.get("audioCost", 0.0),
|
|
387
|
+
total_cost=breakdown_data.get("totalCost", 0.0)
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
return SendUsageV2Response(
|
|
391
|
+
cp_data_id=response_data.get("cpDataId", ""),
|
|
392
|
+
calculated_cost=response_data.get("calculatedCost", 0.0),
|
|
393
|
+
breakdown=breakdown,
|
|
394
|
+
model_metadata=response_data.get("modelMetadata", {})
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
except requests.RequestException as e:
|
|
398
|
+
self.logger.error(f"Failed to send V2 usage data: {e}")
|
|
399
|
+
raise
|
|
455
400
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
401
|
+
def create_or_get_customer(self, request: CustomerCreateOrGetRequest) -> Customer:
|
|
402
|
+
"""
|
|
403
|
+
Create a customer if they don't exist, or return the existing customer.
|
|
404
|
+
This method is idempotent - calling it multiple times with the same external_id will return the same customer.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
request: Customer creation request with required name and external_id
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
The created or existing customer
|
|
411
|
+
|
|
412
|
+
Raises:
|
|
413
|
+
ValueError: If validation fails
|
|
414
|
+
requests.RequestException: If the HTTP request fails
|
|
415
|
+
"""
|
|
416
|
+
# Validate required fields
|
|
417
|
+
if not request.name or request.name.strip() == '':
|
|
418
|
+
self.logger.error('Validation failed: Customer name is required')
|
|
419
|
+
raise ValueError('Customer name is required')
|
|
420
|
+
|
|
421
|
+
if not request.external_id or request.external_id.strip() == '':
|
|
422
|
+
self.logger.error('Validation failed: Customer external_id is required')
|
|
423
|
+
raise ValueError('Customer external_id is required')
|
|
424
|
+
|
|
425
|
+
self.logger.debug(f"Creating or getting customer with external_id: {request.external_id}")
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
# Prepare request data
|
|
429
|
+
request_data = {
|
|
430
|
+
"name": request.name,
|
|
431
|
+
"externalId": request.external_id,
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
# Add optional fields
|
|
435
|
+
if request.email:
|
|
436
|
+
request_data["email"] = request.email
|
|
437
|
+
if request.website:
|
|
438
|
+
request_data["website"] = request.website
|
|
439
|
+
if request.phone:
|
|
440
|
+
request_data["phone"] = request.phone
|
|
441
|
+
if request.address_line1:
|
|
442
|
+
request_data["addressLine1"] = request.address_line1
|
|
443
|
+
if request.address_line2:
|
|
444
|
+
request_data["addressLine2"] = request.address_line2
|
|
445
|
+
if request.city:
|
|
446
|
+
request_data["city"] = request.city
|
|
447
|
+
if request.state:
|
|
448
|
+
request_data["state"] = request.state
|
|
449
|
+
if request.zip_code:
|
|
450
|
+
request_data["zipCode"] = request.zip_code
|
|
451
|
+
if request.country:
|
|
452
|
+
request_data["country"] = request.country
|
|
453
|
+
|
|
454
|
+
# Call create-or-get endpoint
|
|
455
|
+
url = urljoin(self.base_url, "/api/v1/customers/create-or-get")
|
|
456
|
+
headers = {
|
|
457
|
+
"Content-Type": "application/json",
|
|
458
|
+
"paygent-api-key": self.api_key
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
response = self.session.post(
|
|
462
|
+
url,
|
|
463
|
+
json=request_data,
|
|
464
|
+
headers=headers,
|
|
465
|
+
timeout=30
|
|
464
466
|
)
|
|
467
|
+
|
|
465
468
|
response.raise_for_status()
|
|
466
|
-
|
|
469
|
+
|
|
470
|
+
# Parse response
|
|
471
|
+
response_data = response.json()
|
|
472
|
+
self.logger.info(f"Customer created/retrieved successfully with external_id: {request.external_id}")
|
|
473
|
+
|
|
474
|
+
return Customer(
|
|
475
|
+
id=response_data.get("id", ""),
|
|
476
|
+
external_id=response_data.get("externalId", ""),
|
|
477
|
+
name=response_data.get("name", ""),
|
|
478
|
+
email=response_data.get("email"),
|
|
479
|
+
website=response_data.get("website"),
|
|
480
|
+
phone=response_data.get("phone"),
|
|
481
|
+
address_line1=response_data.get("addressLine1"),
|
|
482
|
+
address_line2=response_data.get("addressLine2"),
|
|
483
|
+
city=response_data.get("city"),
|
|
484
|
+
state=response_data.get("state"),
|
|
485
|
+
zip_code=response_data.get("zipCode"),
|
|
486
|
+
country=response_data.get("country"),
|
|
487
|
+
created_at=response_data.get("createdAt"),
|
|
488
|
+
updated_at=response_data.get("updatedAt")
|
|
489
|
+
)
|
|
490
|
+
|
|
467
491
|
except requests.RequestException as e:
|
|
468
|
-
self.logger.error(f"
|
|
492
|
+
self.logger.error(f"Failed to create or get customer: {e}")
|
|
469
493
|
raise
|