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 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
+