geek-cafe-saas-sdk 0.7.1__py3-none-any.whl → 0.7.3__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.

@@ -3,7 +3,7 @@ Geek Cafe Services - Base Reusable Services for SaaS
3
3
 
4
4
  Version 0.6.0 adds File System Service
5
5
  """
6
- __version__ = "0.7.1"
6
+ __version__ = "0.7.3"
7
7
 
8
8
  # Import main modules for easier access
9
9
  from . import services
@@ -500,12 +500,14 @@ class FileShareService(DatabaseService[FileShare]):
500
500
  def _get_file_any_user(self, tenant_id: str, file_id: str) -> ServiceResult[File]:
501
501
  """Get file without access control (for internal use)."""
502
502
  try:
503
- pk = f"FILE#{tenant_id}#{file_id}"
504
- sk = "metadata"
503
+
504
+ file = File()
505
+ file.id = file_id
506
+ file.tenant_id = tenant_id
505
507
 
506
508
  result = self.dynamodb.get(
507
509
  table_name=self.table_name,
508
- key={"pk": pk, "sk": sk}
510
+ model=file
509
511
  )
510
512
 
511
513
  if not result or 'Item' not in result:
@@ -530,21 +532,21 @@ class FileShareService(DatabaseService[FileShare]):
530
532
  ) -> Optional[FileShare]:
531
533
  """Check if share already exists."""
532
534
  try:
533
- gsi1_pk = f"FILE#{tenant_id}#{file_id}"
534
- gsi1_sk = f"USER#{shared_with_user_id}"
535
+ # Query GSI1 by file_id to get all shares for this file
536
+ temp_share = FileShare()
537
+ temp_share.file_id = file_id
535
538
 
536
- results = self.dynamodb.query(
537
- key=Key('gsi1_pk').eq(gsi1_pk) & Key('gsi1_sk').eq(gsi1_sk),
538
- table_name=self.table_name,
539
- index_name="gsi1",
540
- limit=1
541
- )
539
+ result = self._query_by_index(temp_share, "gsi1", limit=100)
542
540
 
543
- items = results.get('Items', [])
544
- if items:
545
- share = FileShare()
546
- share.map(items[0])
547
- return share
541
+ if not result.success:
542
+ return None
543
+
544
+ # Filter for matching tenant and user (active shares only)
545
+ for share in result.data:
546
+ if (share.tenant_id == tenant_id and
547
+ share.shared_with_user_id == shared_with_user_id and
548
+ share.status == "active"):
549
+ return share
548
550
 
549
551
  return None
550
552
 
@@ -559,12 +561,14 @@ class FileShareService(DatabaseService[FileShare]):
559
561
  ) -> None:
560
562
  """Increment share access count."""
561
563
  try:
562
- pk = f"FILE#{tenant_id}#{file_id}"
563
- sk = f"SHARE#{share_id}"
564
+ share = FileShare()
565
+ share.id = share_id
566
+ share.tenant_id = tenant_id
567
+ share.file_id = file_id
564
568
 
565
569
  result = self.dynamodb.get(
566
570
  table_name=self.table_name,
567
- key={"pk": pk, "sk": sk}
571
+ model=share
568
572
  )
569
573
 
570
574
  if result and 'Item' in result:
@@ -575,6 +579,9 @@ class FileShareService(DatabaseService[FileShare]):
575
579
  share.last_accessed_at = dt.datetime.now(dt.UTC).timestamp()
576
580
 
577
581
  share.prep_for_save()
578
- self._save_model(share)
582
+ self.dynamodb.save(
583
+ table_name=self.table_name,
584
+ item=share
585
+ )
579
586
  except Exception:
580
587
  pass # Best effort
@@ -350,11 +350,12 @@ class FileSystemService(DatabaseService[File]):
350
350
  )
351
351
 
352
352
  # Delete from DynamoDB
353
- pk = f"FILE#{tenant_id}#{resource_id}"
354
- sk = "metadata"
353
+ file = File()
354
+ file.id = resource_id
355
+ file.tenant_id = tenant_id
355
356
 
356
357
  self.dynamodb.delete(
357
- primary_key={"pk": pk, "sk": sk},
358
+ model=file,
358
359
  table_name=self.table_name
359
360
  )
360
361
  else:
@@ -515,20 +515,12 @@ class FileVersionService(DatabaseService[FileVersion]):
515
515
  def _get_file(self, tenant_id: str, file_id: str, user_id: str) -> ServiceResult[File]:
516
516
  """Get file with access control."""
517
517
  try:
518
- pk = f"FILE#{tenant_id}#{file_id}"
519
- sk = "metadata"
518
+ # Use DatabaseService helper to get file
519
+ file = self._get_model_by_id_with_tenant_check(file_id, File, tenant_id)
520
520
 
521
- result = self.dynamodb.get(
522
- table_name=self.table_name,
523
- key={"pk": pk, "sk": sk}
524
- )
525
-
526
- if not result or 'Item' not in result:
521
+ if not file:
527
522
  raise NotFoundError(f"File not found: {file_id}")
528
523
 
529
- file = File()
530
- file.map(result['Item'])
531
-
532
524
  if file.owner_id != user_id:
533
525
  raise AccessDeniedError("You do not have access to this file")
534
526
 
@@ -543,21 +535,14 @@ class FileVersionService(DatabaseService[FileVersion]):
543
535
  def _get_latest_version_number(self, tenant_id: str, file_id: str) -> int:
544
536
  """Get the latest version number for a file."""
545
537
  try:
546
- gsi1_pk = f"FILE#{tenant_id}#{file_id}"
538
+ # Query versions for this file using DatabaseService helper
539
+ temp_version = FileVersion()
540
+ temp_version.file_id = file_id
547
541
 
548
- results = self.dynamodb.query(
549
- key=Key('gsi1_pk').eq(gsi1_pk) & Key('gsi1_sk').begins_with("VERSION#"),
550
- table_name=self.table_name,
551
- index_name="gsi1",
552
- limit=1,
553
- ascending=False # Get highest version number
554
- )
542
+ result = self._query_by_index(temp_version, "gsi1", limit=1, ascending=False)
555
543
 
556
- items = results.get('Items', [])
557
- if items:
558
- version = FileVersion()
559
- version.map(items[0])
560
- return version.version_number
544
+ if result.success and result.data:
545
+ return result.data[0].version_number
561
546
 
562
547
  return 0 # No versions yet
563
548
 
@@ -572,17 +557,10 @@ class FileVersionService(DatabaseService[FileVersion]):
572
557
  ) -> None:
573
558
  """Mark a version as not current."""
574
559
  try:
575
- pk = f"FILE#{tenant_id}#{file_id}"
576
- sk = f"VERSION#{version_id}"
560
+ # Use DatabaseService helper to get version
561
+ version = self._get_model_by_id_with_tenant_check(version_id, FileVersion, tenant_id)
577
562
 
578
- result = self.dynamodb.get(
579
- table_name=self.table_name,
580
- key={"pk": pk, "sk": sk}
581
- )
582
-
583
- if result and 'Item' in result:
584
- version = FileVersion()
585
- version.map(result['Item'])
563
+ if version:
586
564
  version.is_current = False
587
565
 
588
566
  version.prep_for_save()
@@ -599,12 +577,15 @@ class FileVersionService(DatabaseService[FileVersion]):
599
577
  ) -> None:
600
578
  """Update file record with current version info."""
601
579
  try:
602
- pk = f"FILE#{tenant_id}#{file_id}"
603
- sk = "metadata"
580
+ file = File()
581
+ file.id = file_id
582
+ file.tenant_id = tenant_id
583
+
584
+ key = file.get_key("gsi1")
604
585
 
605
586
  result = self.dynamodb.get(
606
587
  table_name=self.table_name,
607
- key={"pk": pk, "sk": sk}
588
+ key=key
608
589
  )
609
590
 
610
591
  if result and 'Item' in result:
@@ -33,13 +33,13 @@ class ContactThread(BaseModel):
33
33
  super().__init__()
34
34
  self._subject: str | None = None
35
35
  self._status: str = "open" # open, in_progress, resolved, closed
36
- self._priority: str = "medium" # low, medium, high, urgent
36
+ self._priority: Optional[str] = "medium" # low, medium, high, urgent
37
37
 
38
38
  # Sender information (guest or authenticated user)
39
39
  self._sender: Dict[str, Any] = {} # {id, name, email, session_id}
40
40
 
41
41
  # Assignment and routing
42
- self._assigned_to: str | None = None # Staff user ID
42
+ self._assigned_to: Optional[str] = None # Staff user ID
43
43
  self._inbox_id: str = "support" # support, sales, billing, etc.
44
44
 
45
45
  # Messages embedded in thread (suitable for low volume)
@@ -178,7 +178,7 @@ class ContactThread(BaseModel):
178
178
 
179
179
  @priority.setter
180
180
  def priority(self, value: str | None):
181
- valid_priorities = ["low", "medium", "high", "urgent"]
181
+ valid_priorities = ["low", "medium", "high", "urgent", None]
182
182
  if value in valid_priorities:
183
183
  self._priority = value
184
184
  else:
@@ -245,7 +245,7 @@ class ContactThreadService(DatabaseService[ContactThread]):
245
245
  tenant_id=tenant_id, status=status)
246
246
 
247
247
  def list_by_assigned_user(self, assigned_to: str, tenant_id: str,
248
- status: str = None, limit: int = 50) -> ServiceResult[List[ContactThread]]:
248
+ *, status: str = None, priority: str = None, limit: int = 50) -> ServiceResult[List[ContactThread]]:
249
249
  """
250
250
  List contact threads assigned to a specific user using GSI3.
251
251
 
@@ -263,6 +263,13 @@ class ContactThreadService(DatabaseService[ContactThread]):
263
263
  temp_thread.assigned_to = assigned_to
264
264
  if status:
265
265
  temp_thread.status = status
266
+ else:
267
+ temp_thread.status = None
268
+
269
+ if priority:
270
+ temp_thread.priority = priority
271
+ else:
272
+ temp_thread.priority = None
266
273
 
267
274
  result = self._query_by_index(
268
275
  temp_thread,
@@ -2,14 +2,17 @@
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  from typing import Generic, TypeVar, Dict, Any, List, Optional
5
- from boto3_assist.dynamodb.dynamodb import DynamoDB
5
+ from boto3_assist.dynamodb.dynamodb import DynamoDB, DynamoDBIndex
6
6
  from ..core.service_result import ServiceResult
7
7
  from ..core.service_errors import ValidationError, AccessDeniedError, NotFoundError
8
8
  from ..core.error_codes import ErrorCode
9
9
  import os
10
+ from aws_lambda_powertools import Logger
10
11
 
11
12
  T = TypeVar("T")
12
13
 
14
+ logger = Logger()
15
+
13
16
 
14
17
  class DatabaseService(ABC, Generic[T]):
15
18
  """Base service class for database operations."""
@@ -23,6 +26,8 @@ class DatabaseService(ABC, Generic[T]):
23
26
  if not self.table_name:
24
27
  raise ValueError("Table name is required")
25
28
 
29
+ self.LOG_DYNAMO_DB_QUERY = os.getenv("LOG_DYNAMO_DB_QUERY", False)
30
+
26
31
  @abstractmethod
27
32
  def create(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[T]:
28
33
  """Create a new resource."""
@@ -226,6 +231,26 @@ class DatabaseService(ABC, Generic[T]):
226
231
  # Get the key for the specified index from the provided model
227
232
  key = model.get_key(index_name).key()
228
233
 
234
+ # key.to_string()
235
+ # extract_key_values(key_expression) is coming soon from boto3_assist
236
+ if self.LOG_DYNAMO_DB_QUERY:
237
+ if hasattr(DynamoDBIndex, 'extract_key_values'):
238
+ values = DynamoDBIndex.extract_key_values(key_expression)
239
+ logger.info(f"Querying index {index_name} with key {values}")
240
+ else:
241
+ try:
242
+ value = {
243
+ 'pk': key._values[0]._values[1],
244
+ 'sk': key._values[1]._values[1],
245
+ 'operator': key._values[1].expression_operator,
246
+ 'format': key._values[1].expression_format,
247
+ 'index_name': index_name
248
+ }
249
+ logger.info(f"Querying index {index_name} with key {value}")
250
+ except Exception as e:
251
+ logger.error(f"Failed to extract key values: {e}")
252
+
253
+
229
254
  # Execute the query
230
255
  response = self.dynamodb.query(
231
256
  table_name=self.table_name,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geek_cafe_saas_sdk
3
- Version: 0.7.1
3
+ Version: 0.7.3
4
4
  Summary: Base Reusable Services for SaaS
5
5
  Project-URL: Homepage, https://github.com/geekcafe/geek-cafe-services
6
6
  Project-URL: Documentation, https://github.com/geekcafe/geek-cafe-services/blob/main/README.md
@@ -1,4 +1,4 @@
1
- geek_cafe_saas_sdk/__init__.py,sha256=cLF_Fa1JQqGWMYk2J8hp9nX4IuMyO4gKR7qmaKV_xcs,187
1
+ geek_cafe_saas_sdk/__init__.py,sha256=4M6Pbs_H8QB-sOy-gFdAxI7j6eUOW25SyD0OuAnPN98,187
2
2
  geek_cafe_saas_sdk/core/__init__.py,sha256=3o3-n1ojO_a_X2bEfzhnxmcjAzuzAU73VTcuRva_f5U,279
3
3
  geek_cafe_saas_sdk/core/audit_mixin.py,sha256=hQz0XYUr_Trqpv4JXxywVNxqJJehQwLu79Y1NBpRwzo,1150
4
4
  geek_cafe_saas_sdk/core/error_codes.py,sha256=vf82TDaJ0qIQLzgjTu96nK9cAaSWK7pYfnOW8HCvSxE,5010
@@ -86,9 +86,9 @@ geek_cafe_saas_sdk/domains/files/models/file_version.py,sha256=L15TvTfzhEDbs_Lwf
86
86
  geek_cafe_saas_sdk/domains/files/services/__init__.py,sha256=tzbye87Sk50h7XZJtcvhFF9uAKQr-HgckPfh0wsYVyk,556
87
87
  geek_cafe_saas_sdk/domains/files/services/directory_service.py,sha256=4WKyEB5ho_4dnvljQq0VHY_mSt07p6tyrJvIZRQXpM0,22836
88
88
  geek_cafe_saas_sdk/domains/files/services/file_lineage_service.py,sha256=TdYPTu0fun71RtGaFzrnMI0ONSb1Je7NE8qSkBZz0eM,17545
89
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py,sha256=bkz7A59ZzMRL996fN4laTqn0dUWApALZ7OqSlv2xr2k,20119
90
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py,sha256=LEC7CjdL04OwP8X3o62-GicQIzjcAG-UcKRLJNjXMeU,19077
91
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py,sha256=-Ckm3XZVzDFinqSiA5X3bkURde8EMTSuKek1eoMYD7U,23421
89
+ geek_cafe_saas_sdk/domains/files/services/file_share_service.py,sha256=mlw2k5t8VeVJ-6RAqOBgbSuJ3F1vVC4FZpYNnRoALEU,20376
90
+ geek_cafe_saas_sdk/domains/files/services/file_system_service.py,sha256=uoO3RT_FZuGZxA4S3YmpXUIKymj6M8qcnL2DIyoCD9o,19079
91
+ geek_cafe_saas_sdk/domains/files/services/file_version_service.py,sha256=A5x8Dlo9p07Tnqwumj4JwHoonFg4Jzx4zY2sC7dDtUs,22886
92
92
  geek_cafe_saas_sdk/domains/files/services/s3_file_service.py,sha256=T0lyUK97w-L-PrqzTRhdqxKmiW_w15k2y3BslE6WvQw,16920
93
93
  geek_cafe_saas_sdk/domains/messaging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
94
94
  geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -111,11 +111,11 @@ geek_cafe_saas_sdk/domains/messaging/models/__init__.py,sha256=VtJUxBdtCOxUJzXbD
111
111
  geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py,sha256=xrzP3gItEOIlLDaJHIRysFcpMOnAdK84sE-4pakLEkk,10565
112
112
  geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py,sha256=KyOfP54Ho2oU7upZa5Z-bMDwJfGq8oX5_apFCjxg4fY,6058
113
113
  geek_cafe_saas_sdk/domains/messaging/models/chat_message.py,sha256=sFXp4hTQPlI9pTWp_K4H-XUdz7IO3FozX0PphQm0kYs,13273
114
- geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py,sha256=PTMtwY6OTy002Y1OwQFtvyENIDZIeLzDj88siXZUhrA,12836
114
+ geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py,sha256=v4VzsY7TvuCU2YXouNHd9pMFyPl732jpQ7Hmzr6qPtw,12855
115
115
  geek_cafe_saas_sdk/domains/messaging/services/__init__.py,sha256=qTcn7zYppwdcUSWLCCWNNorJhauCizmhlup7pqiAGXs,287
116
116
  geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py,sha256=ismv9N2ZKwRVErvsSXS9_IBl8kPX-AbRkAvrq4zNywM,26604
117
117
  geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py,sha256=OVG3E-9Xz86s6OeYNY7dShil-rv9N6R6EJO54xNM948,19100
118
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py,sha256=Z0B3WvElgzdaEbXOPfh9XT8BSlGe0csQ0-rJCOGZlYI,20685
118
+ geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py,sha256=k4-qecEgTB3KTFeWAeEoih8vRQR04gkRf63DefBuruE,20918
119
119
  geek_cafe_saas_sdk/domains/notifications/__init__.py,sha256=3T6Y-gVQ9BD-RxgUVnfmk2Ss9NndwLGy2C4rNgDKV3E,424
120
120
  geek_cafe_saas_sdk/domains/notifications/handlers/__init__.py,sha256=TV8USs_qzUi7V8IzgthzwJ69hUC-6WCREg3YN6Glhxs,32
121
121
  geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py,sha256=hQZEuj6Zj8005Q1Ix7t8cmtBN25ILJUens13mEMvS74,2087
@@ -241,7 +241,7 @@ geek_cafe_saas_sdk/middleware/validation.py,sha256=0KnHaBDHo72lAkAUIzEqGl-JCgyIm
241
241
  geek_cafe_saas_sdk/models/__init__.py,sha256=M9p2n9YvwVMw9DMgMJaS-DcmmTJ7EfCmuuIFDF4wnUc,655
242
242
  geek_cafe_saas_sdk/models/base_model.py,sha256=H-t7ZQEUwmkvV9xPyd0jVbbieAUwu9n5MCtjvqFwtYw,7353
243
243
  geek_cafe_saas_sdk/services/__init__.py,sha256=guO3SxaTEy7NAWNG0orrsjRaUT25XvP0pY7W3lRwePY,614
244
- geek_cafe_saas_sdk/services/database_service.py,sha256=Ej0TO_dDERDsxPguMypiUhd9U5MNAOOKo3PTRDyX-QQ,16979
244
+ geek_cafe_saas_sdk/services/database_service.py,sha256=LMYhlNox8Ab-2eIbmBm6AW0EQ1fy9gWi6HP_pOFeCfs,18159
245
245
  geek_cafe_saas_sdk/utilities/__init__.py,sha256=g8voZ8KYeFJQ6HKYhrp5q6mMHgP-AV0ezmouXRiGbDU,2064
246
246
  geek_cafe_saas_sdk/utilities/cognito_utility.py,sha256=S36jAwtJAgeBMN6bL7WKjfLU97f78lRptzpISAQb1kQ,20208
247
247
  geek_cafe_saas_sdk/utilities/custom_exceptions.py,sha256=Yc5Kg2kYXeYZEzuX1u48WczT9jtBZYUiWyTv7NsNDPs,5173
@@ -259,7 +259,7 @@ geek_cafe_saas_sdk/utilities/logging_utility.py,sha256=3VwPGleoRw7c4Bn43_Kujl1CQ
259
259
  geek_cafe_saas_sdk/utilities/message_query_helper.py,sha256=8iMuacRPfom_T06-VMhaSp-90D8604q7waM-GVcpPNQ,11500
260
260
  geek_cafe_saas_sdk/utilities/response.py,sha256=0sylVbm_ieIsNGsm9mWXSIdLSyOeoACwFZQQUxyim3s,5964
261
261
  geek_cafe_saas_sdk/utilities/string_functions.py,sha256=_1F4dGyzuD3fAcV1w7A8bv1e-3p0DbNAv_i6-fsekg8,5973
262
- geek_cafe_saas_sdk-0.7.1.dist-info/METADATA,sha256=wpZ4YyWHwza7H9saWsOBmuDc9vHwHE4ppkDdRbg3df4,16068
263
- geek_cafe_saas_sdk-0.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
264
- geek_cafe_saas_sdk-0.7.1.dist-info/licenses/LICENSE,sha256=EHsHc4GFN0U63LgqDSY2aQRKRupbq6QgixCwopdIU2E,2097
265
- geek_cafe_saas_sdk-0.7.1.dist-info/RECORD,,
262
+ geek_cafe_saas_sdk-0.7.3.dist-info/METADATA,sha256=Zo81XcADCmBMrVZh8AhEAdeFoiul4WJJ2ypnfiePmkM,16068
263
+ geek_cafe_saas_sdk-0.7.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
264
+ geek_cafe_saas_sdk-0.7.3.dist-info/licenses/LICENSE,sha256=EHsHc4GFN0U63LgqDSY2aQRKRupbq6QgixCwopdIU2E,2097
265
+ geek_cafe_saas_sdk-0.7.3.dist-info/RECORD,,