django-cfg 1.2.22__py3-none-any.whl → 1.2.25__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/knowbase/tasks/archive_tasks.py +6 -6
- django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
- django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
- django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
- django_cfg/apps/payments/admin/__init__.py +23 -0
- django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
- django_cfg/apps/payments/admin/balance_admin.py +434 -0
- django_cfg/apps/payments/admin/currencies_admin.py +186 -0
- django_cfg/apps/payments/admin/filters.py +259 -0
- django_cfg/apps/payments/admin/payments_admin.py +142 -0
- django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
- django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
- django_cfg/apps/payments/config/__init__.py +65 -0
- django_cfg/apps/payments/config/module.py +70 -0
- django_cfg/apps/payments/config/providers.py +115 -0
- django_cfg/apps/payments/config/settings.py +96 -0
- django_cfg/apps/payments/config/utils.py +52 -0
- django_cfg/apps/payments/decorators.py +291 -0
- django_cfg/apps/payments/management/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/README.md +178 -0
- django_cfg/apps/payments/management/commands/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
- django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
- django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
- django_cfg/apps/payments/managers/currency_manager.py +65 -14
- django_cfg/apps/payments/middleware/api_access.py +294 -0
- django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
- django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
- django_cfg/apps/payments/migrations/0001_initial.py +125 -11
- django_cfg/apps/payments/models/__init__.py +18 -0
- django_cfg/apps/payments/models/api_keys.py +2 -2
- django_cfg/apps/payments/models/balance.py +2 -2
- django_cfg/apps/payments/models/base.py +16 -0
- django_cfg/apps/payments/models/events.py +2 -2
- django_cfg/apps/payments/models/payments.py +112 -2
- django_cfg/apps/payments/models/subscriptions.py +2 -2
- django_cfg/apps/payments/services/__init__.py +64 -7
- django_cfg/apps/payments/services/billing/__init__.py +8 -0
- django_cfg/apps/payments/services/cache/__init__.py +15 -0
- django_cfg/apps/payments/services/cache/base.py +30 -0
- django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
- django_cfg/apps/payments/services/core/__init__.py +17 -0
- django_cfg/apps/payments/services/core/balance_service.py +447 -0
- django_cfg/apps/payments/services/core/fallback_service.py +432 -0
- django_cfg/apps/payments/services/core/payment_service.py +576 -0
- django_cfg/apps/payments/services/core/subscription_service.py +614 -0
- django_cfg/apps/payments/services/internal_types.py +297 -0
- django_cfg/apps/payments/services/middleware/__init__.py +8 -0
- django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
- django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
- django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
- django_cfg/apps/payments/services/providers/__init__.py +22 -0
- django_cfg/apps/payments/services/providers/base.py +137 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
- django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
- django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
- django_cfg/apps/payments/services/providers/registry.py +103 -0
- django_cfg/apps/payments/services/security/__init__.py +34 -0
- django_cfg/apps/payments/services/security/error_handler.py +637 -0
- django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
- django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
- django_cfg/apps/payments/services/validators/__init__.py +8 -0
- django_cfg/apps/payments/signals/__init__.py +13 -0
- django_cfg/apps/payments/signals/api_key_signals.py +160 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -0
- django_cfg/apps/payments/signals/subscription_signals.py +196 -0
- django_cfg/apps/payments/tasks/__init__.py +12 -0
- django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
- django_cfg/apps/payments/urls.py +5 -5
- django_cfg/apps/payments/utils/__init__.py +45 -0
- django_cfg/apps/payments/utils/billing_utils.py +342 -0
- django_cfg/apps/payments/utils/config_utils.py +245 -0
- django_cfg/apps/payments/utils/middleware_utils.py +228 -0
- django_cfg/apps/payments/utils/validation_utils.py +94 -0
- django_cfg/apps/payments/views/payment_views.py +40 -2
- django_cfg/apps/payments/views/webhook_views.py +266 -0
- django_cfg/apps/payments/viewsets.py +65 -0
- django_cfg/apps/support/signals.py +16 -4
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/cli/README.md +2 -2
- django_cfg/cli/commands/create_project.py +1 -1
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/cli/main.py +1 -1
- django_cfg/cli/utils.py +5 -5
- django_cfg/core/config.py +18 -4
- django_cfg/models/payments.py +546 -0
- django_cfg/models/revolution.py +1 -1
- django_cfg/models/tasks.py +51 -2
- django_cfg/modules/base.py +12 -6
- django_cfg/modules/django_currency/README.md +104 -269
- django_cfg/modules/django_currency/__init__.py +99 -41
- django_cfg/modules/django_currency/clients/__init__.py +11 -0
- django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
- django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
- django_cfg/modules/django_currency/core/__init__.py +42 -0
- django_cfg/modules/django_currency/core/converter.py +169 -0
- django_cfg/modules/django_currency/core/exceptions.py +28 -0
- django_cfg/modules/django_currency/core/models.py +54 -0
- django_cfg/modules/django_currency/database/__init__.py +25 -0
- django_cfg/modules/django_currency/database/database_loader.py +507 -0
- django_cfg/modules/django_currency/utils/__init__.py +9 -0
- django_cfg/modules/django_currency/utils/cache.py +92 -0
- django_cfg/modules/django_email.py +42 -4
- django_cfg/modules/django_unfold/dashboard.py +20 -0
- django_cfg/registry/core.py +10 -0
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/METADATA +11 -6
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/RECORD +113 -50
- django_cfg/apps/agents/examples/__init__.py +0 -3
- django_cfg/apps/agents/examples/simple_example.py +0 -161
- django_cfg/apps/knowbase/examples/__init__.py +0 -3
- django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
- django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
- django_cfg/apps/payments/services/base.py +0 -68
- django_cfg/apps/payments/services/nowpayments.py +0 -78
- django_cfg/apps/payments/services/providers.py +0 -77
- django_cfg/apps/payments/services/redis_service.py +0 -215
- django_cfg/modules/django_currency/cache.py +0 -430
- django_cfg/modules/django_currency/converter.py +0 -324
- django_cfg/modules/django_currency/service.py +0 -277
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,297 @@
|
|
1
|
+
"""
|
2
|
+
Internal Service Types - ONLY for inter-service communication.
|
3
|
+
|
4
|
+
DO NOT duplicate Django ORM or DRF! Only for:
|
5
|
+
1. Providers -> Services (external API response validation)
|
6
|
+
2. Service -> Service (internal operations)
|
7
|
+
3. Configuration (settings and parameters)
|
8
|
+
"""
|
9
|
+
|
10
|
+
from pydantic import BaseModel, Field, ConfigDict
|
11
|
+
from decimal import Decimal
|
12
|
+
from datetime import datetime
|
13
|
+
from typing import Optional, Dict, Any
|
14
|
+
from enum import Enum
|
15
|
+
|
16
|
+
|
17
|
+
# =============================================================================
|
18
|
+
# PROVIDERS - External API response validation
|
19
|
+
# =============================================================================
|
20
|
+
|
21
|
+
class ProviderResponse(BaseModel):
|
22
|
+
"""Validation for any provider response"""
|
23
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
24
|
+
|
25
|
+
success: bool
|
26
|
+
provider_payment_id: Optional[str] = None
|
27
|
+
payment_url: Optional[str] = None
|
28
|
+
pay_amount: Optional[Decimal] = None
|
29
|
+
pay_currency: Optional[str] = None
|
30
|
+
pay_address: Optional[str] = None
|
31
|
+
status: Optional[str] = None
|
32
|
+
error_message: Optional[str] = None
|
33
|
+
# Legacy fields for backward compatibility with tests
|
34
|
+
amount: Optional[Decimal] = None
|
35
|
+
currency: Optional[str] = None
|
36
|
+
payment_id: Optional[str] = None
|
37
|
+
payment_status: Optional[str] = None
|
38
|
+
currency_code: Optional[str] = None
|
39
|
+
|
40
|
+
|
41
|
+
class WebhookData(BaseModel):
|
42
|
+
"""Provider webhook validation"""
|
43
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
44
|
+
|
45
|
+
provider_payment_id: str
|
46
|
+
status: str
|
47
|
+
pay_amount: Optional[Decimal] = None
|
48
|
+
pay_currency: Optional[str] = None
|
49
|
+
actually_paid: Optional[Decimal] = None
|
50
|
+
order_id: Optional[str] = None
|
51
|
+
signature: Optional[str] = None
|
52
|
+
error_message: Optional[str] = None
|
53
|
+
|
54
|
+
|
55
|
+
# =============================================================================
|
56
|
+
# INTER-SERVICE OPERATIONS - Service-to-service typing
|
57
|
+
# =============================================================================
|
58
|
+
|
59
|
+
class ServiceOperationResult(BaseModel):
|
60
|
+
"""Result of inter-service operation"""
|
61
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
62
|
+
|
63
|
+
success: bool
|
64
|
+
error_code: Optional[str] = None
|
65
|
+
error_message: Optional[str] = None
|
66
|
+
data: Dict[str, Any] = Field(default_factory=dict)
|
67
|
+
|
68
|
+
|
69
|
+
class BalanceUpdateRequest(BaseModel):
|
70
|
+
"""Balance update request between services"""
|
71
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
72
|
+
|
73
|
+
user_id: int = Field(gt=0)
|
74
|
+
amount: Decimal
|
75
|
+
source: str
|
76
|
+
reference_id: Optional[str] = None
|
77
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
78
|
+
|
79
|
+
|
80
|
+
class AccessCheckRequest(BaseModel):
|
81
|
+
"""Access check request between services"""
|
82
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
83
|
+
|
84
|
+
user_id: int = Field(gt=0)
|
85
|
+
endpoint_group: str
|
86
|
+
use_cache: bool = True
|
87
|
+
|
88
|
+
|
89
|
+
class AccessCheckResult(BaseModel):
|
90
|
+
"""Access check result"""
|
91
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
92
|
+
|
93
|
+
allowed: bool
|
94
|
+
subscription_id: Optional[str] = None
|
95
|
+
reason: Optional[str] = None
|
96
|
+
remaining_requests: Optional[int] = None
|
97
|
+
usage_percentage: Optional[float] = None
|
98
|
+
|
99
|
+
|
100
|
+
# =============================================================================
|
101
|
+
# CONFIGURATION - Service settings
|
102
|
+
# =============================================================================
|
103
|
+
|
104
|
+
class RedisConfig(BaseModel):
|
105
|
+
"""Redis configuration"""
|
106
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
107
|
+
|
108
|
+
host: str = "localhost"
|
109
|
+
port: int = 6379
|
110
|
+
db: int = 0
|
111
|
+
password: Optional[str] = None
|
112
|
+
max_connections: int = 10
|
113
|
+
timeout_seconds: int = 5
|
114
|
+
|
115
|
+
|
116
|
+
class ProviderConfig(BaseModel):
|
117
|
+
"""Base provider configuration"""
|
118
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
119
|
+
|
120
|
+
enabled: bool = True
|
121
|
+
api_key: str
|
122
|
+
sandbox: bool = False
|
123
|
+
timeout_seconds: int = 30
|
124
|
+
max_retries: int = 3
|
125
|
+
|
126
|
+
|
127
|
+
# =============================================================================
|
128
|
+
# CACHE OPERATIONS - Minimal cache typing
|
129
|
+
# =============================================================================
|
130
|
+
|
131
|
+
class CacheKey(BaseModel):
|
132
|
+
"""Cache key typing"""
|
133
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
134
|
+
|
135
|
+
key: str
|
136
|
+
ttl_seconds: Optional[int] = None
|
137
|
+
|
138
|
+
|
139
|
+
class RateLimitResult(BaseModel):
|
140
|
+
"""Rate limit check result"""
|
141
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
142
|
+
|
143
|
+
allowed: bool
|
144
|
+
remaining: int
|
145
|
+
reset_at: datetime
|
146
|
+
retry_after_seconds: Optional[int] = None
|
147
|
+
|
148
|
+
|
149
|
+
# =============================================================================
|
150
|
+
# SERVICE RESPONSE MODELS - Typed responses for service methods
|
151
|
+
# =============================================================================
|
152
|
+
|
153
|
+
class PaymentCreationResult(BaseModel):
|
154
|
+
"""Payment creation response"""
|
155
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
156
|
+
|
157
|
+
success: bool
|
158
|
+
payment_id: Optional[str] = None
|
159
|
+
provider_payment_id: Optional[str] = None
|
160
|
+
payment_url: Optional[str] = None
|
161
|
+
error: Optional[str] = None
|
162
|
+
|
163
|
+
|
164
|
+
class WebhookProcessingResult(BaseModel):
|
165
|
+
"""Webhook processing response"""
|
166
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
167
|
+
|
168
|
+
success: bool
|
169
|
+
payment_id: Optional[str] = None
|
170
|
+
status_updated: bool = False
|
171
|
+
balance_updated: bool = False
|
172
|
+
error: Optional[str] = None
|
173
|
+
|
174
|
+
|
175
|
+
class PaymentStatusResult(BaseModel):
|
176
|
+
"""Payment status response"""
|
177
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
178
|
+
|
179
|
+
payment_id: str
|
180
|
+
status: str
|
181
|
+
amount_usd: Decimal
|
182
|
+
currency_code: str
|
183
|
+
provider: str
|
184
|
+
provider_payment_id: Optional[str] = None
|
185
|
+
created_at: datetime
|
186
|
+
updated_at: datetime
|
187
|
+
|
188
|
+
|
189
|
+
class UserBalanceResult(BaseModel):
|
190
|
+
"""User balance response"""
|
191
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
192
|
+
|
193
|
+
id: str
|
194
|
+
user_id: int
|
195
|
+
available_balance: Decimal
|
196
|
+
total_balance: Decimal
|
197
|
+
reserved_balance: Decimal
|
198
|
+
last_updated: datetime
|
199
|
+
created_at: datetime
|
200
|
+
|
201
|
+
|
202
|
+
class TransferResult(BaseModel):
|
203
|
+
"""Funds transfer response"""
|
204
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
205
|
+
|
206
|
+
success: bool
|
207
|
+
transaction_id: Optional[str] = None
|
208
|
+
from_user_id: int
|
209
|
+
to_user_id: int
|
210
|
+
amount: Decimal
|
211
|
+
error: Optional[str] = None
|
212
|
+
error_code: Optional[str] = None
|
213
|
+
|
214
|
+
|
215
|
+
class TransactionInfo(BaseModel):
|
216
|
+
"""Transaction information"""
|
217
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
218
|
+
|
219
|
+
id: str
|
220
|
+
user_id: int
|
221
|
+
transaction_type: str
|
222
|
+
amount: Decimal
|
223
|
+
balance_after: Decimal
|
224
|
+
source: str
|
225
|
+
reference_id: Optional[str] = None
|
226
|
+
description: Optional[str] = None
|
227
|
+
created_at: datetime
|
228
|
+
|
229
|
+
|
230
|
+
class EndpointGroupInfo(BaseModel):
|
231
|
+
"""Endpoint group information"""
|
232
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
233
|
+
|
234
|
+
id: str
|
235
|
+
name: str
|
236
|
+
display_name: str
|
237
|
+
|
238
|
+
|
239
|
+
class SubscriptionInfo(BaseModel):
|
240
|
+
"""Subscription information"""
|
241
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
242
|
+
|
243
|
+
id: str
|
244
|
+
endpoint_group: EndpointGroupInfo
|
245
|
+
status: str
|
246
|
+
tier: str
|
247
|
+
monthly_price: Decimal
|
248
|
+
usage_current: int
|
249
|
+
usage_limit: int
|
250
|
+
usage_percentage: float
|
251
|
+
remaining_requests: int
|
252
|
+
expires_at: datetime
|
253
|
+
next_billing: Optional[datetime] = None
|
254
|
+
created_at: datetime
|
255
|
+
|
256
|
+
|
257
|
+
class SubscriptionAnalytics(BaseModel):
|
258
|
+
"""Subscription analytics response"""
|
259
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
260
|
+
|
261
|
+
period: Dict[str, Any] = Field(default_factory=dict)
|
262
|
+
total_revenue: Decimal
|
263
|
+
active_subscriptions: int
|
264
|
+
new_subscriptions: int
|
265
|
+
churned_subscriptions: int
|
266
|
+
error: Optional[str] = None
|
267
|
+
|
268
|
+
|
269
|
+
# =============================================================================
|
270
|
+
# ADDITIONAL RESPONSE MODELS - Missing Pydantic models
|
271
|
+
# =============================================================================
|
272
|
+
|
273
|
+
class PaymentHistoryItem(BaseModel):
|
274
|
+
"""Single payment item for history lists"""
|
275
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
276
|
+
|
277
|
+
id: str
|
278
|
+
user_id: int
|
279
|
+
amount: Decimal
|
280
|
+
currency: str
|
281
|
+
status: str
|
282
|
+
provider: str
|
283
|
+
provider_payment_id: Optional[str] = None
|
284
|
+
created_at: datetime
|
285
|
+
updated_at: datetime
|
286
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
287
|
+
|
288
|
+
|
289
|
+
class ProviderInfo(BaseModel):
|
290
|
+
"""Payment provider information"""
|
291
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
292
|
+
|
293
|
+
name: str
|
294
|
+
display_name: str
|
295
|
+
supported_currencies: list[str] = Field(default_factory=list)
|
296
|
+
is_active: bool
|
297
|
+
features: Dict[str, Any] = Field(default_factory=dict)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
"""
|
2
|
+
Payment system monitoring services.
|
3
|
+
|
4
|
+
Provides health monitoring, alerting, and fallback mechanisms
|
5
|
+
for payment providers and system components.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .provider_health import (
|
9
|
+
ProviderHealthMonitor,
|
10
|
+
ProviderHealthCheck,
|
11
|
+
ProviderHealthSummary,
|
12
|
+
HealthStatus,
|
13
|
+
get_health_monitor
|
14
|
+
)
|
15
|
+
|
16
|
+
__all__ = [
|
17
|
+
'ProviderHealthMonitor',
|
18
|
+
'ProviderHealthCheck',
|
19
|
+
'ProviderHealthSummary',
|
20
|
+
'HealthStatus',
|
21
|
+
'get_health_monitor'
|
22
|
+
]
|
@@ -0,0 +1,222 @@
|
|
1
|
+
"""
|
2
|
+
Pydantic schemas for provider API responses.
|
3
|
+
|
4
|
+
Type-safe models for validating and parsing responses
|
5
|
+
from payment provider health check endpoints.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import Dict, Optional, Any
|
9
|
+
from decimal import Decimal
|
10
|
+
from datetime import datetime
|
11
|
+
from pydantic import BaseModel, Field, validator
|
12
|
+
|
13
|
+
|
14
|
+
class CryptAPIInfoResponse(BaseModel):
|
15
|
+
"""CryptAPI /btc/info/ response schema."""
|
16
|
+
|
17
|
+
coin: str = Field(..., description="Cryptocurrency name")
|
18
|
+
logo: str = Field(..., description="Logo URL")
|
19
|
+
ticker: str = Field(..., description="Currency ticker")
|
20
|
+
minimum_transaction: int = Field(..., description="Minimum transaction in satoshis")
|
21
|
+
minimum_transaction_coin: str = Field(..., description="Minimum transaction in coin units")
|
22
|
+
minimum_fee: int = Field(..., description="Minimum fee in satoshis")
|
23
|
+
minimum_fee_coin: str = Field(..., description="Minimum fee in coin units")
|
24
|
+
fee_percent: str = Field(..., description="Fee percentage")
|
25
|
+
network_fee_estimation: str = Field(..., description="Network fee estimation")
|
26
|
+
status: str = Field(..., description="API status")
|
27
|
+
prices: Dict[str, str] = Field(..., description="Prices in various fiat currencies")
|
28
|
+
prices_updated: str = Field(..., description="Prices last updated timestamp")
|
29
|
+
|
30
|
+
@validator('status')
|
31
|
+
def validate_status(cls, v):
|
32
|
+
"""Validate that status is success."""
|
33
|
+
if v != 'success':
|
34
|
+
raise ValueError(f"Expected status 'success', got '{v}'")
|
35
|
+
return v
|
36
|
+
|
37
|
+
@validator('prices')
|
38
|
+
def validate_prices_not_empty(cls, v):
|
39
|
+
"""Validate that prices dict is not empty."""
|
40
|
+
if not v:
|
41
|
+
raise ValueError("Prices dictionary cannot be empty")
|
42
|
+
return v
|
43
|
+
|
44
|
+
def get_usd_price(self) -> Optional[Decimal]:
|
45
|
+
"""Get USD price as Decimal."""
|
46
|
+
usd_price = self.prices.get('USD')
|
47
|
+
if usd_price:
|
48
|
+
try:
|
49
|
+
return Decimal(usd_price)
|
50
|
+
except:
|
51
|
+
return None
|
52
|
+
return None
|
53
|
+
|
54
|
+
|
55
|
+
class NowPaymentsStatusResponse(BaseModel):
|
56
|
+
"""NowPayments /v1/status response schema."""
|
57
|
+
|
58
|
+
message: str = Field(..., description="Status message")
|
59
|
+
|
60
|
+
@validator('message')
|
61
|
+
def validate_message_ok(cls, v):
|
62
|
+
"""Validate that message is OK."""
|
63
|
+
if v.upper() != 'OK':
|
64
|
+
raise ValueError(f"Expected message 'OK', got '{v}'")
|
65
|
+
return v
|
66
|
+
|
67
|
+
|
68
|
+
class StripeErrorResponse(BaseModel):
|
69
|
+
"""Stripe API error response schema."""
|
70
|
+
|
71
|
+
class StripeError(BaseModel):
|
72
|
+
message: str = Field(..., description="Error message")
|
73
|
+
type: str = Field(..., description="Error type")
|
74
|
+
|
75
|
+
error: StripeError = Field(..., description="Error details")
|
76
|
+
|
77
|
+
@validator('error')
|
78
|
+
def validate_auth_error(cls, v):
|
79
|
+
"""Validate this is an authentication error (meaning API is healthy)."""
|
80
|
+
if v.type != 'invalid_request_error':
|
81
|
+
raise ValueError(f"Expected auth error, got '{v.type}'")
|
82
|
+
return v
|
83
|
+
|
84
|
+
|
85
|
+
class CryptomusErrorResponse(BaseModel):
|
86
|
+
"""Cryptomus API error response schema."""
|
87
|
+
|
88
|
+
error: str = Field(..., description="Error message")
|
89
|
+
|
90
|
+
@validator('error')
|
91
|
+
def validate_not_found_error(cls, v):
|
92
|
+
"""Validate this is a not found error (meaning API is responding)."""
|
93
|
+
if v.lower() not in ['not found', 'unauthorized', 'forbidden']:
|
94
|
+
raise ValueError(f"Unexpected error: {v}")
|
95
|
+
return v
|
96
|
+
|
97
|
+
|
98
|
+
class GenericAPIHealthResponse(BaseModel):
|
99
|
+
"""Generic API health response for unknown formats."""
|
100
|
+
|
101
|
+
status_code: int = Field(..., description="HTTP status code")
|
102
|
+
response_body: str = Field(..., description="Raw response body")
|
103
|
+
response_time_ms: float = Field(..., description="Response time in milliseconds")
|
104
|
+
|
105
|
+
def is_healthy(self) -> bool:
|
106
|
+
"""Determine if API is healthy based on status code."""
|
107
|
+
# 2xx = healthy, 401/403 = healthy (auth required), 4xx = degraded, 5xx = unhealthy
|
108
|
+
if 200 <= self.status_code < 300:
|
109
|
+
return True
|
110
|
+
elif self.status_code in [401, 403]:
|
111
|
+
return True # Auth required but API responding
|
112
|
+
else:
|
113
|
+
return False
|
114
|
+
|
115
|
+
|
116
|
+
class ProviderHealthResponse(BaseModel):
|
117
|
+
"""Unified health response model for all providers."""
|
118
|
+
|
119
|
+
provider_name: str = Field(..., description="Provider name")
|
120
|
+
is_healthy: bool = Field(..., description="Is provider healthy")
|
121
|
+
status_code: int = Field(..., description="HTTP status code")
|
122
|
+
response_time_ms: float = Field(..., description="Response time in milliseconds")
|
123
|
+
error_message: Optional[str] = Field(None, description="Error message if unhealthy")
|
124
|
+
parsed_response: Optional[Dict[str, Any]] = Field(None, description="Parsed API response")
|
125
|
+
raw_response: Optional[str] = Field(None, description="Raw response body")
|
126
|
+
checked_at: datetime = Field(default_factory=datetime.now, description="Check timestamp")
|
127
|
+
|
128
|
+
class Config:
|
129
|
+
json_encoders = {
|
130
|
+
datetime: lambda v: v.isoformat()
|
131
|
+
}
|
132
|
+
|
133
|
+
|
134
|
+
def parse_provider_response(provider_name: str, status_code: int, response_body: str, response_time_ms: float) -> ProviderHealthResponse:
|
135
|
+
"""
|
136
|
+
Parse provider API response using appropriate schema.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
provider_name: Name of the provider
|
140
|
+
status_code: HTTP status code
|
141
|
+
response_body: Raw response body
|
142
|
+
response_time_ms: Response time in milliseconds
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
ProviderHealthResponse with parsed data
|
146
|
+
"""
|
147
|
+
parsed_response = None
|
148
|
+
error_message = None
|
149
|
+
is_healthy = False
|
150
|
+
|
151
|
+
try:
|
152
|
+
import json
|
153
|
+
response_json = json.loads(response_body) if response_body else {}
|
154
|
+
|
155
|
+
if provider_name == 'cryptapi':
|
156
|
+
if status_code == 200:
|
157
|
+
cryptapi_response = CryptAPIInfoResponse(**response_json)
|
158
|
+
parsed_response = cryptapi_response.dict()
|
159
|
+
is_healthy = True
|
160
|
+
else:
|
161
|
+
error_message = f"CryptAPI returned status {status_code}"
|
162
|
+
|
163
|
+
elif provider_name == 'nowpayments':
|
164
|
+
if status_code == 200:
|
165
|
+
nowpayments_response = NowPaymentsStatusResponse(**response_json)
|
166
|
+
parsed_response = nowpayments_response.dict()
|
167
|
+
is_healthy = True
|
168
|
+
else:
|
169
|
+
error_message = f"NowPayments returned status {status_code}"
|
170
|
+
|
171
|
+
elif provider_name == 'stripe':
|
172
|
+
if status_code == 401:
|
173
|
+
stripe_response = StripeErrorResponse(**response_json)
|
174
|
+
parsed_response = stripe_response.dict()
|
175
|
+
is_healthy = True # Auth error = API responding
|
176
|
+
elif 200 <= status_code < 300:
|
177
|
+
parsed_response = response_json
|
178
|
+
is_healthy = True
|
179
|
+
else:
|
180
|
+
error_message = f"Stripe returned unexpected status {status_code}"
|
181
|
+
|
182
|
+
elif provider_name == 'cryptomus':
|
183
|
+
if status_code == 404 and response_json.get('error') == 'Not found':
|
184
|
+
cryptomus_response = CryptomusErrorResponse(**response_json)
|
185
|
+
parsed_response = cryptomus_response.dict()
|
186
|
+
is_healthy = True # Not found = API responding
|
187
|
+
elif status_code == 204:
|
188
|
+
# No Content = API responding and healthy
|
189
|
+
parsed_response = {'status': 'no_content', 'message': 'API responding correctly'}
|
190
|
+
is_healthy = True
|
191
|
+
elif status_code in [401, 403]:
|
192
|
+
is_healthy = True # Auth required = API responding
|
193
|
+
parsed_response = response_json
|
194
|
+
elif 200 <= status_code < 300:
|
195
|
+
parsed_response = response_json
|
196
|
+
is_healthy = True
|
197
|
+
else:
|
198
|
+
error_message = f"Cryptomus returned status {status_code}"
|
199
|
+
|
200
|
+
else:
|
201
|
+
# Generic handling for unknown providers
|
202
|
+
generic_response = GenericAPIHealthResponse(
|
203
|
+
status_code=status_code,
|
204
|
+
response_body=response_body,
|
205
|
+
response_time_ms=response_time_ms
|
206
|
+
)
|
207
|
+
parsed_response = generic_response.dict()
|
208
|
+
is_healthy = generic_response.is_healthy()
|
209
|
+
|
210
|
+
except Exception as e:
|
211
|
+
error_message = f"Failed to parse {provider_name} response: {str(e)}"
|
212
|
+
is_healthy = False
|
213
|
+
|
214
|
+
return ProviderHealthResponse(
|
215
|
+
provider_name=provider_name,
|
216
|
+
is_healthy=is_healthy,
|
217
|
+
status_code=status_code,
|
218
|
+
response_time_ms=response_time_ms,
|
219
|
+
error_message=error_message,
|
220
|
+
parsed_response=parsed_response,
|
221
|
+
raw_response=response_body
|
222
|
+
)
|