geek-cafe-saas-sdk 0.6.0__py3-none-any.whl → 0.7.1__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.
Potentially problematic release.
This version of geek-cafe-saas-sdk might be problematic. Click here for more details.
- geek_cafe_saas_sdk/__init__.py +2 -2
- geek_cafe_saas_sdk/domains/files/handlers/README.md +446 -0
- geek_cafe_saas_sdk/domains/files/handlers/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/files/handlers/files/create/app.py +121 -0
- geek_cafe_saas_sdk/domains/files/handlers/files/download/app.py +80 -0
- geek_cafe_saas_sdk/domains/files/handlers/files/get/app.py +62 -0
- geek_cafe_saas_sdk/domains/files/handlers/files/list/app.py +72 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/create_derived/app.py +99 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/create_main/app.py +104 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/download_bundle/app.py +99 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/get_lineage/app.py +68 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/prepare_bundle/app.py +76 -0
- geek_cafe_saas_sdk/domains/files/models/__init__.py +17 -0
- geek_cafe_saas_sdk/domains/files/models/directory.py +42 -6
- geek_cafe_saas_sdk/domains/files/models/file.py +158 -16
- geek_cafe_saas_sdk/domains/files/models/file_share.py +33 -0
- geek_cafe_saas_sdk/domains/files/models/file_version.py +24 -0
- geek_cafe_saas_sdk/domains/files/services/__init__.py +21 -0
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +54 -135
- geek_cafe_saas_sdk/domains/files/services/file_lineage_service.py +487 -0
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +37 -120
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +67 -103
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +44 -124
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +55 -7
- geek_cafe_saas_sdk/domains/notifications/__init__.py +18 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/__init__.py +1 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +73 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +40 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +34 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +43 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +40 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +40 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +83 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +45 -0
- geek_cafe_saas_sdk/domains/notifications/models/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/notifications/models/notification.py +717 -0
- geek_cafe_saas_sdk/domains/notifications/models/notification_preference.py +365 -0
- geek_cafe_saas_sdk/domains/notifications/models/webhook_subscription.py +339 -0
- geek_cafe_saas_sdk/domains/notifications/services/__init__.py +10 -0
- geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +576 -0
- geek_cafe_saas_sdk/domains/payments/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/payments/handlers/README.md +334 -0
- geek_cafe_saas_sdk/domains/payments/handlers/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +105 -0
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +97 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +97 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +68 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +118 -0
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +89 -0
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/models/__init__.py +17 -0
- geek_cafe_saas_sdk/domains/payments/models/billing_account.py +521 -0
- geek_cafe_saas_sdk/domains/payments/models/payment.py +639 -0
- geek_cafe_saas_sdk/domains/payments/models/payment_intent_ref.py +539 -0
- geek_cafe_saas_sdk/domains/payments/models/refund.py +404 -0
- geek_cafe_saas_sdk/domains/payments/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/payments/services/payment_service.py +405 -0
- geek_cafe_saas_sdk/domains/subscriptions/__init__.py +19 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +408 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/__init__.py +1 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +81 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +48 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +54 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +54 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +83 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +47 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +62 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +82 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +48 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +66 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +54 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +72 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +89 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/addon.py +604 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/discount.py +492 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/plan.py +569 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/usage_record.py +300 -0
- geek_cafe_saas_sdk/domains/subscriptions/services/__init__.py +10 -0
- geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +694 -0
- geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +123 -1
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +213 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +7 -0
- geek_cafe_saas_sdk/services/database_service.py +10 -6
- geek_cafe_saas_sdk/utilities/cognito_utility.py +16 -26
- geek_cafe_saas_sdk/utilities/environment_variables.py +16 -0
- geek_cafe_saas_sdk/utilities/logging_utility.py +77 -0
- {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/METADATA +11 -11
- {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/RECORD +94 -23
- {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/WHEEL +0 -0
- {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UsageRecord model for metered addon usage tracking.
|
|
3
|
+
|
|
4
|
+
Geek Cafe, LLC
|
|
5
|
+
MIT License. See Project Root for the license information.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import datetime as dt
|
|
9
|
+
from typing import Dict, Any, Optional
|
|
10
|
+
from geek_cafe_saas_sdk.models.base_model import BaseModel
|
|
11
|
+
from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UsageRecord(BaseModel):
|
|
15
|
+
"""
|
|
16
|
+
Usage record for metered billing.
|
|
17
|
+
|
|
18
|
+
Tracks usage events for metered addons (API calls, storage, etc.)
|
|
19
|
+
Aggregated for billing at end of period.
|
|
20
|
+
|
|
21
|
+
Key Features:
|
|
22
|
+
- Event-based tracking
|
|
23
|
+
- Aggregation support
|
|
24
|
+
- Idempotency keys
|
|
25
|
+
- Metadata for debugging
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
- API Calls: 1000 calls made
|
|
29
|
+
- Storage: 50GB used
|
|
30
|
+
- SMS Sent: 150 messages
|
|
31
|
+
- Compute Hours: 25 hours
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Action constants
|
|
35
|
+
ACTION_INCREMENT = "increment" # Add to usage
|
|
36
|
+
ACTION_DECREMENT = "decrement" # Subtract from usage
|
|
37
|
+
ACTION_SET = "set" # Set absolute value
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
super().__init__()
|
|
41
|
+
|
|
42
|
+
# Identification
|
|
43
|
+
self._tenant_id: str = ""
|
|
44
|
+
self._subscription_id: str = ""
|
|
45
|
+
self._addon_code: str = ""
|
|
46
|
+
|
|
47
|
+
# Usage details
|
|
48
|
+
self._quantity: float = 0.0 # Amount of usage
|
|
49
|
+
self._action: str = self.ACTION_INCREMENT # increment|decrement|set
|
|
50
|
+
self._timestamp_utc_ts: float = 0.0 # When usage occurred
|
|
51
|
+
|
|
52
|
+
# Metering
|
|
53
|
+
self._meter_event_name: str = "" # e.g., "api_call", "storage_gb"
|
|
54
|
+
self._unit_name: Optional[str] = None # e.g., "call", "GB", "hour"
|
|
55
|
+
|
|
56
|
+
# Billing period
|
|
57
|
+
self._billing_period_start_utc_ts: Optional[float] = None
|
|
58
|
+
self._billing_period_end_utc_ts: Optional[float] = None
|
|
59
|
+
|
|
60
|
+
# Idempotency
|
|
61
|
+
self._idempotency_key: Optional[str] = None # Prevent duplicate recording
|
|
62
|
+
|
|
63
|
+
# Status
|
|
64
|
+
self._is_processed: bool = False # Whether included in invoice
|
|
65
|
+
self._processed_utc_ts: Optional[float] = None
|
|
66
|
+
self._invoice_id: Optional[str] = None
|
|
67
|
+
|
|
68
|
+
# Metadata
|
|
69
|
+
self._metadata: Dict[str, Any] = {} # Custom metadata
|
|
70
|
+
self._source: Optional[str] = None # Where usage came from
|
|
71
|
+
self._description: Optional[str] = None
|
|
72
|
+
|
|
73
|
+
# CRITICAL: Call _setup_indexes() as LAST line in __init__
|
|
74
|
+
self._setup_indexes()
|
|
75
|
+
|
|
76
|
+
def _setup_indexes(self):
|
|
77
|
+
"""Setup DynamoDB indexes for usage record queries."""
|
|
78
|
+
|
|
79
|
+
# Primary index: Usage record by ID
|
|
80
|
+
primary: DynamoDBIndex = DynamoDBIndex()
|
|
81
|
+
primary.name = "primary"
|
|
82
|
+
primary.partition_key.attribute_name = "pk"
|
|
83
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(("usage", self.id))
|
|
84
|
+
primary.sort_key.attribute_name = "sk"
|
|
85
|
+
primary.sort_key.value = lambda: "metadata"
|
|
86
|
+
self.indexes.add_primary(primary)
|
|
87
|
+
|
|
88
|
+
# GSI1: Usage by tenant and subscription
|
|
89
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
90
|
+
gsi.name = "gsi1"
|
|
91
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
92
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id), ("subscription", self.subscription_id))
|
|
93
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
94
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("timestamp", self.timestamp_utc_ts))
|
|
95
|
+
self.indexes.add_secondary(gsi)
|
|
96
|
+
|
|
97
|
+
# GSI2: Usage by addon and period (for aggregation)
|
|
98
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
99
|
+
gsi.name = "gsi2"
|
|
100
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
101
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id), ("addon", self.addon_code))
|
|
102
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
103
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("timestamp", self.timestamp_utc_ts))
|
|
104
|
+
self.indexes.add_secondary(gsi)
|
|
105
|
+
|
|
106
|
+
# Tenant ID
|
|
107
|
+
@property
|
|
108
|
+
def tenant_id(self) -> str:
|
|
109
|
+
"""Tenant ID."""
|
|
110
|
+
return self._tenant_id
|
|
111
|
+
|
|
112
|
+
@tenant_id.setter
|
|
113
|
+
def tenant_id(self, value: str):
|
|
114
|
+
self._tenant_id = value
|
|
115
|
+
|
|
116
|
+
# Subscription ID
|
|
117
|
+
@property
|
|
118
|
+
def subscription_id(self) -> str:
|
|
119
|
+
"""Subscription ID."""
|
|
120
|
+
return self._subscription_id
|
|
121
|
+
|
|
122
|
+
@subscription_id.setter
|
|
123
|
+
def subscription_id(self, value: str):
|
|
124
|
+
self._subscription_id = value
|
|
125
|
+
|
|
126
|
+
# Addon Code
|
|
127
|
+
@property
|
|
128
|
+
def addon_code(self) -> str:
|
|
129
|
+
"""Addon code."""
|
|
130
|
+
return self._addon_code
|
|
131
|
+
|
|
132
|
+
@addon_code.setter
|
|
133
|
+
def addon_code(self, value: str):
|
|
134
|
+
self._addon_code = value
|
|
135
|
+
|
|
136
|
+
# Quantity
|
|
137
|
+
@property
|
|
138
|
+
def quantity(self) -> float:
|
|
139
|
+
"""Usage quantity."""
|
|
140
|
+
return self._quantity
|
|
141
|
+
|
|
142
|
+
@quantity.setter
|
|
143
|
+
def quantity(self, value: float):
|
|
144
|
+
if value < 0:
|
|
145
|
+
raise ValueError("quantity cannot be negative")
|
|
146
|
+
self._quantity = value
|
|
147
|
+
|
|
148
|
+
# Action
|
|
149
|
+
@property
|
|
150
|
+
def action(self) -> str:
|
|
151
|
+
"""Usage action."""
|
|
152
|
+
return self._action
|
|
153
|
+
|
|
154
|
+
@action.setter
|
|
155
|
+
def action(self, value: str):
|
|
156
|
+
valid_actions = [self.ACTION_INCREMENT, self.ACTION_DECREMENT, self.ACTION_SET]
|
|
157
|
+
if value not in valid_actions:
|
|
158
|
+
raise ValueError(f"Invalid action: {value}. Must be one of {valid_actions}")
|
|
159
|
+
self._action = value
|
|
160
|
+
|
|
161
|
+
# Timestamp
|
|
162
|
+
@property
|
|
163
|
+
def timestamp_utc_ts(self) -> float:
|
|
164
|
+
"""Timestamp when usage occurred."""
|
|
165
|
+
return self._timestamp_utc_ts
|
|
166
|
+
|
|
167
|
+
@timestamp_utc_ts.setter
|
|
168
|
+
def timestamp_utc_ts(self, value: float):
|
|
169
|
+
self._timestamp_utc_ts = value
|
|
170
|
+
|
|
171
|
+
# Meter Event Name
|
|
172
|
+
@property
|
|
173
|
+
def meter_event_name(self) -> str:
|
|
174
|
+
"""Meter event name."""
|
|
175
|
+
return self._meter_event_name
|
|
176
|
+
|
|
177
|
+
@meter_event_name.setter
|
|
178
|
+
def meter_event_name(self, value: str):
|
|
179
|
+
self._meter_event_name = value
|
|
180
|
+
|
|
181
|
+
# Unit Name
|
|
182
|
+
@property
|
|
183
|
+
def unit_name(self) -> Optional[str]:
|
|
184
|
+
"""Unit name."""
|
|
185
|
+
return self._unit_name
|
|
186
|
+
|
|
187
|
+
@unit_name.setter
|
|
188
|
+
def unit_name(self, value: Optional[str]):
|
|
189
|
+
self._unit_name = value
|
|
190
|
+
|
|
191
|
+
# Billing Period Start
|
|
192
|
+
@property
|
|
193
|
+
def billing_period_start_utc_ts(self) -> Optional[float]:
|
|
194
|
+
"""Billing period start timestamp."""
|
|
195
|
+
return self._billing_period_start_utc_ts
|
|
196
|
+
|
|
197
|
+
@billing_period_start_utc_ts.setter
|
|
198
|
+
def billing_period_start_utc_ts(self, value: Optional[float]):
|
|
199
|
+
self._billing_period_start_utc_ts = value
|
|
200
|
+
|
|
201
|
+
# Billing Period End
|
|
202
|
+
@property
|
|
203
|
+
def billing_period_end_utc_ts(self) -> Optional[float]:
|
|
204
|
+
"""Billing period end timestamp."""
|
|
205
|
+
return self._billing_period_end_utc_ts
|
|
206
|
+
|
|
207
|
+
@billing_period_end_utc_ts.setter
|
|
208
|
+
def billing_period_end_utc_ts(self, value: Optional[float]):
|
|
209
|
+
self._billing_period_end_utc_ts = value
|
|
210
|
+
|
|
211
|
+
# Idempotency Key
|
|
212
|
+
@property
|
|
213
|
+
def idempotency_key(self) -> Optional[str]:
|
|
214
|
+
"""Idempotency key."""
|
|
215
|
+
return self._idempotency_key
|
|
216
|
+
|
|
217
|
+
@idempotency_key.setter
|
|
218
|
+
def idempotency_key(self, value: Optional[str]):
|
|
219
|
+
self._idempotency_key = value
|
|
220
|
+
|
|
221
|
+
# Is Processed
|
|
222
|
+
@property
|
|
223
|
+
def is_processed(self) -> bool:
|
|
224
|
+
"""Whether usage has been processed/billed."""
|
|
225
|
+
return self._is_processed
|
|
226
|
+
|
|
227
|
+
@is_processed.setter
|
|
228
|
+
def is_processed(self, value: bool):
|
|
229
|
+
self._is_processed = value
|
|
230
|
+
|
|
231
|
+
# Processed Timestamp
|
|
232
|
+
@property
|
|
233
|
+
def processed_utc_ts(self) -> Optional[float]:
|
|
234
|
+
"""When usage was processed."""
|
|
235
|
+
return self._processed_utc_ts
|
|
236
|
+
|
|
237
|
+
@processed_utc_ts.setter
|
|
238
|
+
def processed_utc_ts(self, value: Optional[float]):
|
|
239
|
+
self._processed_utc_ts = value
|
|
240
|
+
|
|
241
|
+
# Invoice ID
|
|
242
|
+
@property
|
|
243
|
+
def invoice_id(self) -> Optional[str]:
|
|
244
|
+
"""Invoice ID if processed."""
|
|
245
|
+
return self._invoice_id
|
|
246
|
+
|
|
247
|
+
@invoice_id.setter
|
|
248
|
+
def invoice_id(self, value: Optional[str]):
|
|
249
|
+
self._invoice_id = value
|
|
250
|
+
|
|
251
|
+
# Metadata
|
|
252
|
+
@property
|
|
253
|
+
def metadata(self) -> Dict[str, Any]:
|
|
254
|
+
"""Custom metadata."""
|
|
255
|
+
return self._metadata
|
|
256
|
+
|
|
257
|
+
@metadata.setter
|
|
258
|
+
def metadata(self, value: Dict[str, Any]):
|
|
259
|
+
self._metadata = value if value else {}
|
|
260
|
+
|
|
261
|
+
# Source
|
|
262
|
+
@property
|
|
263
|
+
def source(self) -> Optional[str]:
|
|
264
|
+
"""Usage source."""
|
|
265
|
+
return self._source
|
|
266
|
+
|
|
267
|
+
@source.setter
|
|
268
|
+
def source(self, value: Optional[str]):
|
|
269
|
+
self._source = value
|
|
270
|
+
|
|
271
|
+
# Description
|
|
272
|
+
@property
|
|
273
|
+
def description(self) -> Optional[str]:
|
|
274
|
+
"""Usage description."""
|
|
275
|
+
return self._description
|
|
276
|
+
|
|
277
|
+
@description.setter
|
|
278
|
+
def description(self, value: Optional[str]):
|
|
279
|
+
self._description = value
|
|
280
|
+
|
|
281
|
+
# Helper Methods
|
|
282
|
+
|
|
283
|
+
def mark_processed(self, invoice_id: Optional[str] = None):
|
|
284
|
+
"""Mark usage record as processed."""
|
|
285
|
+
self._is_processed = True
|
|
286
|
+
self._processed_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
287
|
+
if invoice_id:
|
|
288
|
+
self._invoice_id = invoice_id
|
|
289
|
+
|
|
290
|
+
def is_increment(self) -> bool:
|
|
291
|
+
"""Check if action is increment."""
|
|
292
|
+
return self._action == self.ACTION_INCREMENT
|
|
293
|
+
|
|
294
|
+
def is_decrement(self) -> bool:
|
|
295
|
+
"""Check if action is decrement."""
|
|
296
|
+
return self._action == self.ACTION_DECREMENT
|
|
297
|
+
|
|
298
|
+
def is_set(self) -> bool:
|
|
299
|
+
"""Check if action is set."""
|
|
300
|
+
return self._action == self.ACTION_SET
|