gsab 0.1.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.
- auth/__init__.py +1 -0
- auth/authenticator.py +39 -0
- core/__init__.py +1 -0
- core/connection.py +39 -0
- core/schema.py +233 -0
- core/sheet_manager.py +470 -0
- gsab-0.1.0.dist-info/LICENSE.md +21 -0
- gsab-0.1.0.dist-info/METADATA +140 -0
- gsab-0.1.0.dist-info/RECORD +17 -0
- gsab-0.1.0.dist-info/WHEEL +5 -0
- gsab-0.1.0.dist-info/top_level.txt +3 -0
- tests/__init__.py +0 -0
- tests/test_auth.py +34 -0
- tests/test_encryption.py +73 -0
- tests/test_gsheets_db.py +207 -0
- tests/test_installation.py +38 -0
- tests/test_sheet_manager.py +171 -0
auth/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Authentication package."""
|
auth/authenticator.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from google.oauth2 import service_account
|
|
2
|
+
from google.auth.transport.requests import Request
|
|
3
|
+
|
|
4
|
+
class GoogleAuthenticator:
|
|
5
|
+
"""Handles authentication with Google Sheets API."""
|
|
6
|
+
|
|
7
|
+
SCOPES = [
|
|
8
|
+
'https://www.googleapis.com/auth/spreadsheets',
|
|
9
|
+
'https://www.googleapis.com/auth/drive.file',
|
|
10
|
+
'https://www.googleapis.com/auth/drive'
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
def __init__(self, credentials_path: str):
|
|
14
|
+
self.credentials_path = credentials_path
|
|
15
|
+
self.creds = None
|
|
16
|
+
|
|
17
|
+
def authenticate(self):
|
|
18
|
+
"""
|
|
19
|
+
Authenticate using service account credentials.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Google OAuth2 credentials
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
self.creds = service_account.Credentials.from_service_account_file(
|
|
26
|
+
self.credentials_path,
|
|
27
|
+
scopes=self.SCOPES
|
|
28
|
+
)
|
|
29
|
+
# Ensure the credentials are valid
|
|
30
|
+
if not self.creds.valid:
|
|
31
|
+
request = Request()
|
|
32
|
+
self.creds.refresh(request)
|
|
33
|
+
return self.creds
|
|
34
|
+
except Exception as e:
|
|
35
|
+
raise AuthenticationError(f"Authentication failed: {str(e)}")
|
|
36
|
+
|
|
37
|
+
class AuthenticationError(Exception):
|
|
38
|
+
"""Custom exception for authentication errors."""
|
|
39
|
+
pass
|
core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core package."""
|
core/connection.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any
|
|
2
|
+
from google.oauth2 import service_account
|
|
3
|
+
from googleapiclient.discovery import build
|
|
4
|
+
from ..auth.authenticator import GoogleAuthenticator
|
|
5
|
+
from ..exceptions.custom_exceptions import ConnectionError
|
|
6
|
+
|
|
7
|
+
class SheetConnection:
|
|
8
|
+
"""Manages connection to Google Sheets API."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, credentials_path: str = None):
|
|
11
|
+
"""Initialize connection with credentials path."""
|
|
12
|
+
self.credentials_path = credentials_path
|
|
13
|
+
self.credentials = None
|
|
14
|
+
self.service = None
|
|
15
|
+
|
|
16
|
+
async def connect(self) -> None:
|
|
17
|
+
"""Establish connection to Google Sheets API."""
|
|
18
|
+
try:
|
|
19
|
+
SCOPES = [
|
|
20
|
+
'https://www.googleapis.com/auth/spreadsheets',
|
|
21
|
+
'https://www.googleapis.com/auth/drive',
|
|
22
|
+
'https://www.googleapis.com/auth/drive.file'
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# Use service account credentials
|
|
26
|
+
self.credentials = service_account.Credentials.from_service_account_file(
|
|
27
|
+
self.credentials_path,
|
|
28
|
+
scopes=SCOPES
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
self.service = build('sheets', 'v4', credentials=self.credentials)
|
|
32
|
+
|
|
33
|
+
except Exception as e:
|
|
34
|
+
raise ConnectionError(f"Failed to connect to Google Sheets API: {str(e)}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_connected(self) -> bool:
|
|
38
|
+
"""Check if connection is established."""
|
|
39
|
+
return self.service is not None
|
core/schema.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
from typing import Dict, Any, List, Union, Optional, Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
import re
|
|
5
|
+
from datetime import datetime, date
|
|
6
|
+
|
|
7
|
+
class FieldType(Enum):
|
|
8
|
+
STRING = "string"
|
|
9
|
+
INTEGER = "integer"
|
|
10
|
+
FLOAT = "float"
|
|
11
|
+
BOOLEAN = "boolean"
|
|
12
|
+
DATE = "date"
|
|
13
|
+
DATETIME = "datetime"
|
|
14
|
+
JSON = "json"
|
|
15
|
+
ENCRYPTED = "encrypted"
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ValidationRule:
|
|
19
|
+
"""Defines a validation rule for a field."""
|
|
20
|
+
condition: Callable[[Any], bool]
|
|
21
|
+
error_message: str
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Field:
|
|
25
|
+
name: str
|
|
26
|
+
field_type: FieldType
|
|
27
|
+
required: bool = True
|
|
28
|
+
unique: bool = False
|
|
29
|
+
default: Any = None
|
|
30
|
+
min_length: Optional[int] = None
|
|
31
|
+
max_length: Optional[int] = None
|
|
32
|
+
pattern: Optional[str] = None
|
|
33
|
+
min_value: Optional[Union[int, float]] = None
|
|
34
|
+
max_value: Optional[Union[int, float]] = None
|
|
35
|
+
validation_rules: List[ValidationRule] = None
|
|
36
|
+
encrypted: bool = False
|
|
37
|
+
|
|
38
|
+
def __post_init__(self):
|
|
39
|
+
self.validation_rules = self.validation_rules or []
|
|
40
|
+
self._add_default_validations()
|
|
41
|
+
|
|
42
|
+
def _add_default_validations(self):
|
|
43
|
+
"""Add default validation rules based on field type and constraints."""
|
|
44
|
+
if self.min_length is not None:
|
|
45
|
+
self.validation_rules.append(
|
|
46
|
+
ValidationRule(
|
|
47
|
+
lambda x: len(str(x)) >= self.min_length,
|
|
48
|
+
f"Value must be at least {self.min_length} characters long"
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if self.max_length is not None:
|
|
53
|
+
self.validation_rules.append(
|
|
54
|
+
ValidationRule(
|
|
55
|
+
lambda x: len(str(x)) <= self.max_length,
|
|
56
|
+
f"Value must be at most {self.max_length} characters long"
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if self.pattern is not None:
|
|
61
|
+
self.validation_rules.append(
|
|
62
|
+
ValidationRule(
|
|
63
|
+
lambda x: bool(re.match(self.pattern, str(x))),
|
|
64
|
+
f"Value must match pattern: {self.pattern}"
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if self.min_value is not None:
|
|
69
|
+
self.validation_rules.append(
|
|
70
|
+
ValidationRule(
|
|
71
|
+
lambda x: x >= self.min_value,
|
|
72
|
+
f"Value must be greater than or equal to {self.min_value}"
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if self.max_value is not None:
|
|
77
|
+
self.validation_rules.append(
|
|
78
|
+
ValidationRule(
|
|
79
|
+
lambda x: x <= self.max_value,
|
|
80
|
+
f"Value must be less than or equal to {self.max_value}"
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
class Schema:
|
|
85
|
+
"""Defines the structure of a sheet."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, name: str, fields: List[Field]):
|
|
88
|
+
self.name = name
|
|
89
|
+
self.fields = fields
|
|
90
|
+
self._validate_schema()
|
|
91
|
+
self._field_map = {field.name: field for field in fields}
|
|
92
|
+
|
|
93
|
+
def _validate_schema(self) -> None:
|
|
94
|
+
"""Validate schema definition."""
|
|
95
|
+
field_names = set()
|
|
96
|
+
for field in self.fields:
|
|
97
|
+
if field.name in field_names:
|
|
98
|
+
raise ValueError(f"Duplicate field name: {field.name}")
|
|
99
|
+
field_names.add(field.name)
|
|
100
|
+
|
|
101
|
+
def validate_value(self, field_name: str, value: Any) -> List[str]:
|
|
102
|
+
"""
|
|
103
|
+
Validate a value against field rules.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
field_name: Name of the field
|
|
107
|
+
value: Value to validate
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
List of validation error messages (empty if valid)
|
|
111
|
+
"""
|
|
112
|
+
field = self._field_map.get(field_name)
|
|
113
|
+
if not field:
|
|
114
|
+
raise ValueError(f"Unknown field: {field_name}")
|
|
115
|
+
|
|
116
|
+
errors = []
|
|
117
|
+
|
|
118
|
+
# Skip validation for None values if field is not required
|
|
119
|
+
if value is None:
|
|
120
|
+
if field.required:
|
|
121
|
+
errors.append(f"Field {field_name} is required")
|
|
122
|
+
return errors
|
|
123
|
+
|
|
124
|
+
# Type validation
|
|
125
|
+
try:
|
|
126
|
+
self._convert_value(value, field.field_type)
|
|
127
|
+
except ValueError as e:
|
|
128
|
+
errors.append(str(e))
|
|
129
|
+
return errors
|
|
130
|
+
|
|
131
|
+
# Custom validation rules
|
|
132
|
+
for rule in field.validation_rules:
|
|
133
|
+
try:
|
|
134
|
+
if not rule.condition(value):
|
|
135
|
+
errors.append(rule.error_message)
|
|
136
|
+
except Exception as e:
|
|
137
|
+
errors.append(f"Validation error: {str(e)}")
|
|
138
|
+
|
|
139
|
+
return errors
|
|
140
|
+
|
|
141
|
+
def _convert_value(self, value: Any, field_type: FieldType) -> Any:
|
|
142
|
+
"""Convert and validate value type."""
|
|
143
|
+
try:
|
|
144
|
+
if field_type == FieldType.INTEGER:
|
|
145
|
+
return int(value)
|
|
146
|
+
elif field_type == FieldType.FLOAT:
|
|
147
|
+
return float(value)
|
|
148
|
+
elif field_type == FieldType.BOOLEAN:
|
|
149
|
+
return bool(value)
|
|
150
|
+
elif field_type == FieldType.DATE:
|
|
151
|
+
if isinstance(value, str):
|
|
152
|
+
return datetime.strptime(value, "%Y-%m-%d").date()
|
|
153
|
+
elif isinstance(value, date):
|
|
154
|
+
return value
|
|
155
|
+
raise ValueError("Invalid date format")
|
|
156
|
+
elif field_type == FieldType.DATETIME:
|
|
157
|
+
if isinstance(value, str):
|
|
158
|
+
return datetime.fromisoformat(value)
|
|
159
|
+
elif isinstance(value, datetime):
|
|
160
|
+
return value
|
|
161
|
+
raise ValueError("Invalid datetime format")
|
|
162
|
+
else:
|
|
163
|
+
return str(value)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
raise ValueError(f"Invalid value for type {field_type}: {value}")
|
|
166
|
+
|
|
167
|
+
def validate_field(self, field_name: str, value: Any) -> List[str]:
|
|
168
|
+
"""Validate a single field value."""
|
|
169
|
+
errors = []
|
|
170
|
+
field = self.get_field(field_name)
|
|
171
|
+
|
|
172
|
+
if field:
|
|
173
|
+
# Type validation
|
|
174
|
+
if field.field_type == FieldType.INTEGER:
|
|
175
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
|
176
|
+
errors.append(f"Invalid value for type {field.field_type}: {value}")
|
|
177
|
+
elif field_name == "age" and value < 0:
|
|
178
|
+
errors.append("Age must be a positive number")
|
|
179
|
+
|
|
180
|
+
elif field.field_type == FieldType.FLOAT:
|
|
181
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
|
182
|
+
errors.append(f"Invalid value for type {field.field_type}: {value}")
|
|
183
|
+
|
|
184
|
+
elif field.field_type == FieldType.BOOLEAN:
|
|
185
|
+
if not isinstance(value, bool):
|
|
186
|
+
errors.append(f"Invalid value for type {field.field_type}: {value}")
|
|
187
|
+
|
|
188
|
+
elif field.field_type == FieldType.STRING:
|
|
189
|
+
if not isinstance(value, str):
|
|
190
|
+
errors.append(f"Invalid value for type {field.field_type}: {value}")
|
|
191
|
+
elif field.pattern and not re.match(field.pattern, value):
|
|
192
|
+
errors.append(f"Value does not match pattern {field.pattern}: {value}")
|
|
193
|
+
|
|
194
|
+
return errors
|
|
195
|
+
|
|
196
|
+
def validate(self, data: Dict[str, Any]) -> List[str]:
|
|
197
|
+
"""
|
|
198
|
+
Validate data against schema.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
data: Dictionary of field values to validate
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
List of validation error messages
|
|
205
|
+
"""
|
|
206
|
+
errors = []
|
|
207
|
+
|
|
208
|
+
# Check required fields
|
|
209
|
+
for field in self.fields:
|
|
210
|
+
if field.required and field.name not in data:
|
|
211
|
+
errors.append(f"Field {field.name} is required")
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
value = data.get(field.name)
|
|
215
|
+
if value is not None:
|
|
216
|
+
# Validate field value
|
|
217
|
+
field_errors = self.validate_field(field.name, value)
|
|
218
|
+
errors.extend(field_errors)
|
|
219
|
+
|
|
220
|
+
return errors
|
|
221
|
+
|
|
222
|
+
def get_field(self, field_name: str) -> Optional[Field]:
|
|
223
|
+
"""
|
|
224
|
+
Get field by name.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
field_name: Name of the field to retrieve
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Field object if found, None otherwise
|
|
231
|
+
"""
|
|
232
|
+
return self._field_map.get(field_name)
|
|
233
|
+
|