geek-cafe-saas-sdk 0.6.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.
Potentially problematic release.
This version of geek-cafe-saas-sdk might be problematic. Click here for more details.
- geek_cafe_saas_sdk/__init__.py +9 -0
- geek_cafe_saas_sdk/core/__init__.py +11 -0
- geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
- geek_cafe_saas_sdk/core/error_codes.py +132 -0
- geek_cafe_saas_sdk/core/service_errors.py +19 -0
- geek_cafe_saas_sdk/core/service_result.py +121 -0
- geek_cafe_saas_sdk/decorators/__init__.py +64 -0
- geek_cafe_saas_sdk/decorators/auth.py +373 -0
- geek_cafe_saas_sdk/decorators/core.py +358 -0
- geek_cafe_saas_sdk/domains/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
- geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
- geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
- geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
- geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
- geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
- geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
- geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
- geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
- geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
- geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
- geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
- geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
- geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
- geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
- geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
- geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
- geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
- geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
- geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
- geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
- geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
- geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
- geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
- geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
- geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
- geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
- geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
- geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
- geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
- geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
- geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
- geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
- geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
- geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
- geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
- geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
- geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
- geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
- geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
- geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
- geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
- geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
- geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
- geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
- geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
- geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
- geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
- geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
- geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
- geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
- geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
- geek_cafe_saas_sdk/middleware/__init__.py +36 -0
- geek_cafe_saas_sdk/middleware/auth.py +85 -0
- geek_cafe_saas_sdk/middleware/authorization.py +523 -0
- geek_cafe_saas_sdk/middleware/cors.py +63 -0
- geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
- geek_cafe_saas_sdk/middleware/validation.py +80 -0
- geek_cafe_saas_sdk/models/__init__.py +20 -0
- geek_cafe_saas_sdk/models/base_model.py +233 -0
- geek_cafe_saas_sdk/services/__init__.py +18 -0
- geek_cafe_saas_sdk/services/database_service.py +441 -0
- geek_cafe_saas_sdk/utilities/__init__.py +88 -0
- geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
- geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
- geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
- geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
- geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
- geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
- geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
- geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
- geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
- geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
- geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
- geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
- geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
- geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
- geek_cafe_saas_sdk/utilities/response.py +209 -0
- geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/licenses/LICENSE +47 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import secrets
|
|
3
|
+
import string
|
|
4
|
+
import uuid
|
|
5
|
+
import time
|
|
6
|
+
from geek_cafe_saas_sdk.utilities.logging_utility import LoggingUtility
|
|
7
|
+
from geek_cafe_saas_sdk.utilities.datetime_utility import DatetimeUtility
|
|
8
|
+
|
|
9
|
+
logger = LoggingUtility(__name__).logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StringFunctions:
|
|
13
|
+
SPECIAL_CHARACTERS = "!\\#$%&()*+,-.:;<=>?@[]^_{|}~"
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def generate_random_string(length=12, digits=True, letters=True, special=False):
|
|
17
|
+
characters = ""
|
|
18
|
+
if letters:
|
|
19
|
+
characters += string.ascii_letters
|
|
20
|
+
if digits:
|
|
21
|
+
characters += string.digits
|
|
22
|
+
if special:
|
|
23
|
+
characters += StringFunctions.SPECIAL_CHARACTERS
|
|
24
|
+
|
|
25
|
+
random_string = "".join(secrets.choice(characters) for _ in range(length))
|
|
26
|
+
return random_string
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def generate_random_password(length=15, digits=True, letters=True, special=True):
|
|
30
|
+
characters = ""
|
|
31
|
+
# we have a min lenght requirement of 8
|
|
32
|
+
if length < 8:
|
|
33
|
+
length = 8
|
|
34
|
+
|
|
35
|
+
if letters:
|
|
36
|
+
characters += string.ascii_letters
|
|
37
|
+
if digits:
|
|
38
|
+
characters += string.digits
|
|
39
|
+
if special:
|
|
40
|
+
characters += StringFunctions.SPECIAL_CHARACTERS
|
|
41
|
+
|
|
42
|
+
if len(characters) == 0:
|
|
43
|
+
raise RuntimeError(
|
|
44
|
+
"You must choose at least one of the options: digits, letters, special"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Ensure at least two characters from each selected set are included
|
|
48
|
+
password = []
|
|
49
|
+
if letters:
|
|
50
|
+
# lower
|
|
51
|
+
password.append(secrets.choice(string.ascii_lowercase))
|
|
52
|
+
password.append(secrets.choice(string.ascii_lowercase))
|
|
53
|
+
# upper
|
|
54
|
+
password.append(secrets.choice(string.ascii_uppercase))
|
|
55
|
+
password.append(secrets.choice(string.ascii_uppercase))
|
|
56
|
+
if digits:
|
|
57
|
+
password.append(secrets.choice(string.digits))
|
|
58
|
+
password.append(secrets.choice(string.digits))
|
|
59
|
+
if special:
|
|
60
|
+
password.append(secrets.choice(StringFunctions.SPECIAL_CHARACTERS))
|
|
61
|
+
password.append(secrets.choice(StringFunctions.SPECIAL_CHARACTERS))
|
|
62
|
+
|
|
63
|
+
# Fill the remaining length with random characters from the selected sets
|
|
64
|
+
remaining_length = length - len(password)
|
|
65
|
+
password.extend(secrets.choice(characters) for _ in range(remaining_length))
|
|
66
|
+
|
|
67
|
+
# Shuffle the password to randomize the order of characters
|
|
68
|
+
secrets.SystemRandom().shuffle(password)
|
|
69
|
+
|
|
70
|
+
return "".join(password)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def wrap_text(text: str, max_width: int):
|
|
74
|
+
"""Wrap text at a specified width."""
|
|
75
|
+
wrapped_text = ""
|
|
76
|
+
if not text:
|
|
77
|
+
return text
|
|
78
|
+
|
|
79
|
+
while len(text) > max_width:
|
|
80
|
+
# Find the maximum width position, breaking at max_width
|
|
81
|
+
# If there are no spaces to break on, break at max_width directly
|
|
82
|
+
break_point = (
|
|
83
|
+
text.rfind(" ", 0, max_width) if " " in text[0:max_width] else max_width
|
|
84
|
+
)
|
|
85
|
+
if break_point == -1: # no spaces found, hard break
|
|
86
|
+
break_point = max_width
|
|
87
|
+
wrapped_text += text[:break_point] + "\n"
|
|
88
|
+
text = text[break_point:].lstrip() # remove any leading space
|
|
89
|
+
wrapped_text += text
|
|
90
|
+
return wrapped_text
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def generate_uuid():
|
|
94
|
+
return str(uuid.uuid4())
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def generate_hash(input_string: str) -> str:
|
|
98
|
+
"""
|
|
99
|
+
Generates a SHA-256 hash for the given input string.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
input_string (str): The string to hash.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
str: The resulting hash value as a hexadecimal string.
|
|
106
|
+
"""
|
|
107
|
+
# Encode the input string to bytes
|
|
108
|
+
encoded_string = input_string.encode()
|
|
109
|
+
|
|
110
|
+
# Create a SHA-256 hash object
|
|
111
|
+
hash_object = hashlib.sha256()
|
|
112
|
+
|
|
113
|
+
# Update the hash object with the encoded string
|
|
114
|
+
hash_object.update(encoded_string)
|
|
115
|
+
|
|
116
|
+
# Get the hexadecimal representation of the hash
|
|
117
|
+
hash_hex = hash_object.hexdigest()
|
|
118
|
+
|
|
119
|
+
return hash_hex
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def generate_sortable_uuid():
|
|
123
|
+
"""
|
|
124
|
+
Generates a unique id for the execution event
|
|
125
|
+
"""
|
|
126
|
+
epoch_time = time.time()
|
|
127
|
+
sortable_uuid: uuid.UUID = DatetimeUtility.uuid1_utc(timestamp=epoch_time)
|
|
128
|
+
|
|
129
|
+
time_stamp = str(epoch_time).replace(".", "-")
|
|
130
|
+
sortable_id = f"{time_stamp}:{str(sortable_uuid)}"
|
|
131
|
+
return sortable_id
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def to_bool(value: str | bool | int | None) -> bool:
|
|
135
|
+
"""
|
|
136
|
+
Converts a string or boolean value to a boolean.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
value (str | bool | int | None): The value to convert.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
bool: The converted boolean value.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
ValueError: If the input value is not a valid boolean or string representation.
|
|
146
|
+
"""
|
|
147
|
+
return StringFunctions.to_boolean(value)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def to_boolean(value: str | bool | int | None) -> bool:
|
|
151
|
+
"""
|
|
152
|
+
Converts a string or boolean value to a boolean.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
value (str | bool | int | None): The value to convert.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
bool: The converted boolean value.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
ValueError: If the input value is not a valid boolean or string representation.
|
|
162
|
+
"""
|
|
163
|
+
if isinstance(value, bool):
|
|
164
|
+
return value
|
|
165
|
+
if isinstance(value, str):
|
|
166
|
+
value = str(value).lower().strip()
|
|
167
|
+
if value in ("true", "1", "t", "y", "yes"):
|
|
168
|
+
return True
|
|
169
|
+
if value in ("false", "0", "f", "n", "no"):
|
|
170
|
+
return False
|
|
171
|
+
else:
|
|
172
|
+
logger.warning(f"Invalid boolean value: {value}; returning False.")
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
elif isinstance(value, int):
|
|
176
|
+
return bool(value)
|
|
177
|
+
elif value is None:
|
|
178
|
+
return False
|
|
179
|
+
else:
|
|
180
|
+
raise ValueError(f"Invalid boolean value: {value}")
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: geek_cafe_saas_sdk
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Base Reusable Services for SaaS
|
|
5
|
+
Project-URL: Homepage, https://github.com/geekcafe/geek-cafe-services
|
|
6
|
+
Project-URL: Documentation, https://github.com/geekcafe/geek-cafe-services/blob/main/README.md
|
|
7
|
+
Project-URL: Source Code, https://github.com/geekcafe/geek-cafe-services
|
|
8
|
+
Author-email: Eric Wilson <eric.wilson@geekcafe.com>
|
|
9
|
+
License: Geek Cafe Services Business Source License 1.0
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2025 Geek Cafe, LLC. All rights reserved.
|
|
12
|
+
|
|
13
|
+
The "Geek Cafe Services" software (the "Software") is made available under this
|
|
14
|
+
Business Source License (the "License"). This License allows you to view,
|
|
15
|
+
study, and modify the source code, and to use it for personal, educational,
|
|
16
|
+
research, or non-commercial purposes, subject to the following terms.
|
|
17
|
+
|
|
18
|
+
1. Grant of Rights
|
|
19
|
+
a. You may copy and modify the Software for your own personal,
|
|
20
|
+
educational, or internal development use.
|
|
21
|
+
b. You may not use the Software, or modified versions of it,
|
|
22
|
+
to provide any commercial service or product, including
|
|
23
|
+
software-as-a-service, consulting, hosting, or resale,
|
|
24
|
+
without a separate commercial license from Geek Cafe LLC.
|
|
25
|
+
|
|
26
|
+
2. Change Date
|
|
27
|
+
Three (3) years from the date of first public release of a specific
|
|
28
|
+
version of the Software (the “Change Date”), that version will
|
|
29
|
+
automatically be made available under the Apache License 2.0.
|
|
30
|
+
Later versions may have different Change Dates.
|
|
31
|
+
|
|
32
|
+
3. Attribution
|
|
33
|
+
All copies or substantial portions of the Software must retain this
|
|
34
|
+
License text, the copyright notice above, and a clear reference to
|
|
35
|
+
the original source repository (https://github.com/geekcafe/geek-cafe-services).
|
|
36
|
+
|
|
37
|
+
4. Trademarks
|
|
38
|
+
The names “Geek Cafe”, “Geek Cafe Services”, and any related logos are
|
|
39
|
+
trademarks of Geek Cafe LLC and may not be used to endorse or promote
|
|
40
|
+
derivative products without prior written permission.
|
|
41
|
+
|
|
42
|
+
5. Disclaimer of Warranty
|
|
43
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
|
44
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
45
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
|
46
|
+
|
|
47
|
+
6. Limitation of Liability
|
|
48
|
+
IN NO EVENT SHALL GEEK Cafe LLC OR CONTRIBUTORS BE LIABLE FOR ANY CLAIM,
|
|
49
|
+
DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR
|
|
50
|
+
OTHERWISE, ARISING FROM OR IN CONNECTION WITH THE SOFTWARE OR ITS USE.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
For commercial licensing inquiries, contact:
|
|
55
|
+
legal@geekcafe.com
|
|
56
|
+
License-File: LICENSE
|
|
57
|
+
Keywords: api gateway,aws,dynamodb,lambda,saas,serverless,services
|
|
58
|
+
Classifier: Development Status :: 4 - Beta
|
|
59
|
+
Classifier: Framework :: AWS CDK
|
|
60
|
+
Classifier: Intended Audience :: Developers
|
|
61
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
62
|
+
Classifier: Programming Language :: Python :: 3
|
|
63
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
64
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
65
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
66
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
67
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
68
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
69
|
+
Requires-Python: >=3.8
|
|
70
|
+
Requires-Dist: boto3-assist
|
|
71
|
+
Provides-Extra: dev
|
|
72
|
+
Requires-Dist: build; extra == 'dev'
|
|
73
|
+
Requires-Dist: twine; extra == 'dev'
|
|
74
|
+
Description-Content-Type: text/markdown
|
|
75
|
+
|
|
76
|
+
# Geek Cafe Services
|
|
77
|
+
|
|
78
|
+
[](https://www.python.org/downloads/)
|
|
79
|
+
[](https://github.com/geekcafe/geek-cafe-services)
|
|
80
|
+
[](https://aws.amazon.com/dynamodb/)
|
|
81
|
+
[](https://aws.amazon.com/lambda/)
|
|
82
|
+
|
|
83
|
+
> **⚠️ Pre-1.0 Notice**: This library is under active development. Breaking changes may occur until we reach a stable 1.0 GA release. We recommend pinning to specific versions in production.
|
|
84
|
+
|
|
85
|
+
> **✨ New in v0.3.0**: Complete CRUDL Lambda handlers for all core resources (Events, Users, Groups, Messages, Votes)!
|
|
86
|
+
|
|
87
|
+
<!-- COVERAGE-BADGE:START -->
|
|
88
|
+
## Test Coverage
|
|
89
|
+
|
|
90
|
+

|
|
91
|
+

|
|
92
|
+
|
|
93
|
+
**Overall Coverage:** 82.9% (9325/11242 statements)
|
|
94
|
+
|
|
95
|
+
### Coverage Summary
|
|
96
|
+
|
|
97
|
+
| Metric | Value |
|
|
98
|
+
|--------|-------|
|
|
99
|
+
| Total Statements | 11,242 |
|
|
100
|
+
| Covered Statements | 9,325 |
|
|
101
|
+
| Missing Statements | 1,917 |
|
|
102
|
+
| Coverage Percentage | 82.9% |
|
|
103
|
+
| Total Tests | 1127 |
|
|
104
|
+
| Test Status | ✅ All Passing |
|
|
105
|
+
|
|
106
|
+
### Files Needing Attention (< 80% coverage)
|
|
107
|
+
|
|
108
|
+
| Coverage | Missing Lines | File |
|
|
109
|
+
|----------|---------------|------|
|
|
110
|
+
| 17.5% | 47 | `lambda_handlers/_base/authorized_secure_handler.py` |
|
|
111
|
+
| 25.3% | 133 | `domains/communities/services/community_member_service.py` |
|
|
112
|
+
| 46.3% | 58 | `domains/auth/models/role.py` |
|
|
113
|
+
| 46.9% | 34 | `domains/auth/models/permission.py` |
|
|
114
|
+
| 46.9% | 85 | `domains/auth/services/resource_permission_service.py` |
|
|
115
|
+
| 56.2% | 7 | `lambda_handlers/_base/secure_handler.py` |
|
|
116
|
+
| 58.5% | 17 | `domains/messaging/handlers/contact_threads/update/app.py` |
|
|
117
|
+
| 62.3% | 118 | `utilities/lambda_event_utility.py` |
|
|
118
|
+
| 63.4% | 86 | `domains/communities/services/community_service.py` |
|
|
119
|
+
| 64.0% | 41 | `domains/files/services/s3_file_service.py` |
|
|
120
|
+
|
|
121
|
+
*... and 20 more files with < 80% coverage*
|
|
122
|
+
|
|
123
|
+
### Running Tests
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Run all tests with coverage
|
|
127
|
+
./run_unit_tests.sh
|
|
128
|
+
|
|
129
|
+
# View detailed coverage report
|
|
130
|
+
open reports/coverage/index.html
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
*Last updated: 2025-10-15 22:38:06*
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
<!-- COVERAGE-BADGE:END -->
|
|
138
|
+
|
|
139
|
+
## Description
|
|
140
|
+
|
|
141
|
+
**Geek Cafe Services** is a production-ready, enterprise-grade library that provides reusable database services specifically designed for multi-tenant SaaS applications. Built on top of AWS DynamoDB, this library offers a prescriptive approach to building scalable, maintainable backend services with consistent patterns and best practices.
|
|
142
|
+
|
|
143
|
+
### Why Geek Cafe Services?
|
|
144
|
+
|
|
145
|
+
🏗️ **Consistent Architecture**: All services follow the same proven patterns for CRUD operations, error handling, and access control
|
|
146
|
+
🔒 **Multi-Tenant by Design**: Built-in tenant isolation ensures secure data separation across customers
|
|
147
|
+
⚡ **DynamoDB Optimized**: Leverages DynamoDB's strengths with efficient GSI indexes and query patterns
|
|
148
|
+
🛡️ **Production Ready**: Comprehensive error handling, logging, pagination, and batch operations
|
|
149
|
+
🧪 **Fully Tested**: 100% test coverage with comprehensive test suites for reliability
|
|
150
|
+
📖 **Well Documented**: Extensive documentation with practical examples and best practices
|
|
151
|
+
|
|
152
|
+
### Perfect For
|
|
153
|
+
|
|
154
|
+
- **SaaS Applications** requiring multi-tenant data isolation
|
|
155
|
+
- **Serverless Architectures** built on AWS Lambda and DynamoDB
|
|
156
|
+
- **Teams** wanting consistent, proven patterns across services
|
|
157
|
+
- **Rapid Development** with pre-built, tested service components
|
|
158
|
+
|
|
159
|
+
## Installation
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# Clone the repository
|
|
163
|
+
git clone https://github.com/geekcafe/geek-cafe-services.git
|
|
164
|
+
cd geek-cafe-services
|
|
165
|
+
|
|
166
|
+
# Setup the development environment
|
|
167
|
+
./pysetup.sh
|
|
168
|
+
|
|
169
|
+
# Install dependencies
|
|
170
|
+
pip install -r requirements.txt
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Quick Start
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from geek_cafe_saas_sdk.message_service import MessageService
|
|
177
|
+
|
|
178
|
+
# Initialize service
|
|
179
|
+
service = MessageService()
|
|
180
|
+
|
|
181
|
+
# Create a message
|
|
182
|
+
result = service.create(
|
|
183
|
+
tenant_id="your_tenant",
|
|
184
|
+
user_id="your_user",
|
|
185
|
+
type="notification",
|
|
186
|
+
content={"title": "Welcome!", "body": "Thanks for joining us."}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if result.success:
|
|
190
|
+
print(f"Created message: {result.data.id}")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Available Services
|
|
194
|
+
|
|
195
|
+
### 🚀 Lambda Handler Wrappers (NEW in v0.2.0)
|
|
196
|
+
**Purpose**: Eliminate 70-80% of boilerplate code in AWS Lambda functions
|
|
197
|
+
|
|
198
|
+
**Key Capabilities**:
|
|
199
|
+
- ✅ Automatic API key validation from environment
|
|
200
|
+
- ✅ Request body parsing and camelCase → snake_case conversion
|
|
201
|
+
- ✅ Service initialization with connection pooling for warm starts
|
|
202
|
+
- ✅ Built-in CORS and error handling
|
|
203
|
+
- ✅ User context extraction from authorizers
|
|
204
|
+
- ✅ Service injection for easy testing
|
|
205
|
+
- ✅ Support for public and secured endpoints
|
|
206
|
+
|
|
207
|
+
**Available Handlers**:
|
|
208
|
+
- `ApiKeyLambdaHandler` - API key validation (most common)
|
|
209
|
+
- `PublicLambdaHandler` - No authentication (config endpoints)
|
|
210
|
+
- `BaseLambdaHandler` - Extensible base for custom handlers
|
|
211
|
+
|
|
212
|
+
**Quick Example**:
|
|
213
|
+
```python
|
|
214
|
+
from geek_cafe_saas_sdk.lambda_handlers import ApiKeyLambdaHandler
|
|
215
|
+
from geek_cafe_saas_sdk.vote_service import VoteService
|
|
216
|
+
|
|
217
|
+
# All boilerplate handled in 3 lines
|
|
218
|
+
handler = ApiKeyLambdaHandler(
|
|
219
|
+
service_class=VoteService,
|
|
220
|
+
require_body=True,
|
|
221
|
+
convert_case=True
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def lambda_handler(event, context):
|
|
225
|
+
return handler.execute(event, context, create_vote)
|
|
226
|
+
|
|
227
|
+
def create_vote(event, service, user_context):
|
|
228
|
+
# Just your business logic - everything else is handled!
|
|
229
|
+
payload = event["parsed_body"] # Already parsed & converted
|
|
230
|
+
return service.create_vote(
|
|
231
|
+
tenant_id=user_context.get("tenant_id", "anonymous"),
|
|
232
|
+
user_id=user_context.get("user_id", "anonymous"),
|
|
233
|
+
**payload
|
|
234
|
+
)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Use Cases**: Any AWS Lambda function with API key auth, reducing code by 70-80% while maintaining all functionality
|
|
238
|
+
|
|
239
|
+
📖 **[Complete Lambda Handlers Documentation](./docs/lambda_handlers.md)**
|
|
240
|
+
|
|
241
|
+
### 📧 MessageService
|
|
242
|
+
**Purpose**: Complete message and notification management system
|
|
243
|
+
|
|
244
|
+
**Key Capabilities**:
|
|
245
|
+
- ✅ Full CRUD operations with tenant isolation
|
|
246
|
+
- ✅ Flexible JSON content storage for any message type
|
|
247
|
+
- ✅ Efficient querying by user, tenant, and message type
|
|
248
|
+
- ✅ Automatic audit trails and timestamps
|
|
249
|
+
- ✅ Built-in access control and validation
|
|
250
|
+
|
|
251
|
+
**Use Cases**: User notifications, system alerts, communication logs, announcement management
|
|
252
|
+
|
|
253
|
+
### 🗳️ Voting Services Suite
|
|
254
|
+
**Purpose**: Complete voting and rating system with real-time aggregation
|
|
255
|
+
|
|
256
|
+
**Architecture**: Three interconnected services working together:
|
|
257
|
+
|
|
258
|
+
#### VoteService
|
|
259
|
+
- ✅ Individual vote management with automatic upsert behavior
|
|
260
|
+
- ✅ One vote per user per target enforcement
|
|
261
|
+
- ✅ Support for up/down votes or custom vote types
|
|
262
|
+
- ✅ Comprehensive querying by user, target, and tenant
|
|
263
|
+
|
|
264
|
+
#### VoteSummaryService
|
|
265
|
+
- ✅ Pre-calculated vote totals for instant retrieval
|
|
266
|
+
- ✅ Target-based optimization for high-performance lookups
|
|
267
|
+
- ✅ Metadata tracking (last tallied timestamp, vote counts)
|
|
268
|
+
- ✅ Tenant-scoped summary management
|
|
269
|
+
|
|
270
|
+
#### VoteTallyService
|
|
271
|
+
- ✅ Intelligent vote aggregation with pagination support
|
|
272
|
+
- ✅ Batch processing for multiple targets
|
|
273
|
+
- ✅ Stale target detection and automated re-tallying
|
|
274
|
+
- ✅ Comprehensive error handling and resilience
|
|
275
|
+
|
|
276
|
+
**Use Cases**: Product ratings, content voting, feedback systems, community polls, recommendation engines
|
|
277
|
+
|
|
278
|
+
## Documentation
|
|
279
|
+
|
|
280
|
+
📖 **[Complete Documentation](./docs/services_overview.md)**
|
|
281
|
+
|
|
282
|
+
- **[DynamoDB Models](./docs/dynamodb_models.md)** - Model definition patterns and best practices
|
|
283
|
+
- **[Services Pattern](./docs/services_pattern.md)** - Service layer architecture and CRUD operations
|
|
284
|
+
- **[Lambda Handlers](./docs/lambda_handlers.md)** - 🆕 Eliminate Lambda boilerplate (70-80% code reduction)
|
|
285
|
+
- [Services Overview](./docs/services_overview.md) - Architecture and common patterns
|
|
286
|
+
- [MessageService](./docs/message_service.md) - Message management API
|
|
287
|
+
- [Voting Services](./docs/voting_services.md) - Complete voting system documentation
|
|
288
|
+
- [A/B Testing Guide](./docs/ab_testing_guide.md) - Using voting services for A/B testing and experimentation
|
|
289
|
+
- [Development Roadmap](./docs/roadmap.md) - Planned improvements and enhancements
|
|
290
|
+
|
|
291
|
+
## Core Features
|
|
292
|
+
|
|
293
|
+
### 🏛️ **Enterprise Architecture**
|
|
294
|
+
- **Multi-Tenant by Design**: Complete tenant isolation with automatic access control
|
|
295
|
+
- **Consistent Patterns**: All services follow identical CRUD interfaces and conventions
|
|
296
|
+
- **Scalable Design**: Built for high-throughput, multi-customer SaaS applications
|
|
297
|
+
|
|
298
|
+
### 🔧 **Developer Experience**
|
|
299
|
+
- **Type Safety**: Full Python type hints for better IDE support and fewer bugs
|
|
300
|
+
- **Comprehensive Testing**: 100% test coverage with realistic test scenarios
|
|
301
|
+
- **Rich Documentation**: Detailed API docs, examples, and best practices
|
|
302
|
+
- **Easy Integration**: Simple initialization and consistent error handling
|
|
303
|
+
|
|
304
|
+
### ⚡ **Performance & Reliability**
|
|
305
|
+
- **DynamoDB Optimized**: Efficient GSI indexes and query patterns for fast operations
|
|
306
|
+
- **Pagination Support**: Handle large datasets without memory issues
|
|
307
|
+
- **Batch Operations**: Process multiple items efficiently
|
|
308
|
+
- **Error Resilience**: Graceful handling of partial failures and edge cases
|
|
309
|
+
|
|
310
|
+
### 🛡️ **Production Ready**
|
|
311
|
+
- **Structured Logging**: AWS Lambda Powertools integration for observability
|
|
312
|
+
- **Comprehensive Validation**: Input validation with detailed error messages
|
|
313
|
+
- **Access Control**: Automatic tenant and user-based security enforcement
|
|
314
|
+
- **Audit Trails**: Complete tracking of who did what and when
|
|
315
|
+
|
|
316
|
+
## Environment Setup
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# Required environment variables
|
|
320
|
+
export DYNAMODB_TABLE_NAME=your_table_name
|
|
321
|
+
|
|
322
|
+
# Optional AWS configuration (if not using IAM roles)
|
|
323
|
+
export AWS_REGION=us-east-1
|
|
324
|
+
export AWS_ACCESS_KEY_ID=your_access_key
|
|
325
|
+
export AWS_SECRET_ACCESS_KEY=your_secret_key
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Testing
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
# Run all tests
|
|
332
|
+
pytest tests/ -v
|
|
333
|
+
|
|
334
|
+
# Run specific service tests
|
|
335
|
+
pytest tests/test_message_service.py -v
|
|
336
|
+
pytest tests/test_vote_*_service.py -v
|
|
337
|
+
|
|
338
|
+
# Run with coverage
|
|
339
|
+
pytest tests/ --cov=geek_cafe_saas_sdk --cov-report=html
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Project Structure
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
geek-cafe-services/
|
|
346
|
+
├── src/geek_cafe_saas_sdk/
|
|
347
|
+
│ ├── lambda_handlers/ # 🆕 Lambda handler wrappers (v0.2.0)
|
|
348
|
+
│ │ ├── base.py # Base handler with common functionality
|
|
349
|
+
│ │ ├── api_key_handler.py # API key validation handler
|
|
350
|
+
│ │ ├── public_handler.py # Public (no auth) handler
|
|
351
|
+
│ │ └── service_pool.py # Service connection pooling
|
|
352
|
+
│ ├── middleware/ # CORS, auth, error handling decorators
|
|
353
|
+
│ ├── utilities/ # Request/response helpers
|
|
354
|
+
│ ├── models/ # Data models with DynamoDB mapping
|
|
355
|
+
│ ├── *_service.py # Service implementations
|
|
356
|
+
│ ├── database_service.py # Base service class
|
|
357
|
+
│ └── service_result.py # Standardized response wrapper
|
|
358
|
+
├── tests/ # Comprehensive test suite
|
|
359
|
+
├── docs/ # Detailed documentation
|
|
360
|
+
│ └── lambda_handlers.md # 🆕 Lambda wrapper documentation
|
|
361
|
+
├── examples/ # Working code examples
|
|
362
|
+
│ └── lambda_handlers/ # 🆕 Handler examples
|
|
363
|
+
└── README.md # This file
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Contributing
|
|
367
|
+
|
|
368
|
+
We welcome contributions! Here's how to get started:
|
|
369
|
+
|
|
370
|
+
1. **Fork the repository** and create a feature branch
|
|
371
|
+
2. **Follow the existing patterns** - consistency is key
|
|
372
|
+
3. **Add comprehensive tests** for any new functionality
|
|
373
|
+
4. **Update documentation** for API changes
|
|
374
|
+
5. **Submit a Pull Request** with a clear description
|
|
375
|
+
|
|
376
|
+
### Development Guidelines
|
|
377
|
+
|
|
378
|
+
- Follow existing code style and patterns
|
|
379
|
+
- Maintain 100% test coverage for new code
|
|
380
|
+
- Update documentation for any API changes
|
|
381
|
+
- Use meaningful commit messages
|
|
382
|
+
- Test against multiple Python versions if possible
|
|
383
|
+
|
|
384
|
+
## License
|
|
385
|
+
|
|
386
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
387
|
+
|
|
388
|
+
## Support
|
|
389
|
+
|
|
390
|
+
- 📖 **Documentation**: [Complete docs](./docs/services_overview.md)
|
|
391
|
+
- 🐛 **Bug Reports**: [GitHub Issues](https://github.com/geekcafe/geek-cafe-services/issues)
|
|
392
|
+
- 💡 **Feature Requests**: [GitHub Discussions](https://github.com/geekcafe/geek-cafe-services/discussions)
|
|
393
|
+
- 📧 **Questions**: Create an issue with the "question" label
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
**Built with ❤️ for the SaaS development community**
|