gsab 0.1.0__tar.gz

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.
gsab-0.1.0/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Ajmal Aksar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
gsab-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.1
2
+ Name: gsab
3
+ Version: 0.1.0
4
+ Summary: A database-like interface for Google Sheets
5
+ Home-page: https://github.com/ajmalaksar25/gsab
6
+ Author: Ajmal Aksar
7
+ Author-email: ajmalaksar25@gmail.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE.md
14
+ Requires-Dist: google-auth-oauthlib>=0.4.6
15
+ Requires-Dist: fastapi>=0.68.0
16
+ Requires-Dist: uvicorn>=0.15.0
17
+ Requires-Dist: python-multipart>=0.0.5
18
+ Requires-Dist: jinja2>=3.0.0
19
+ Requires-Dist: aiofiles>=0.8.0
20
+ Requires-Dist: pytest>=6.0.0
21
+ Requires-Dist: pytest-asyncio>=0.15.0
22
+ Requires-Dist: pytest-cov>=2.12.0
23
+ Requires-Dist: google-auth>=2.3.3
24
+ Requires-Dist: google-api-python-client>=2.31.0
25
+ Requires-Dist: cryptography>=35.0.0
26
+ Requires-Dist: python-dotenv>=0.19.2
27
+
28
+ # Google Sheets as Backend (GSAB)
29
+
30
+ A Python library that enables using Google Sheets as a database backend with features like schema validation and encryption.
31
+
32
+ ## Features
33
+
34
+ - 🔒 Secure Google Sheets integration with OAuth2
35
+ - 📊 Schema validation and type checking
36
+ - 🔐 Field-level encryption for sensitive data
37
+ - 🌐 Async/await support
38
+ - 📝 Comprehensive logging
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install gsheets-db
44
+ ```
45
+
46
+ ## Quick Start
47
+
48
+ 1. Set up Google Cloud Project and enable Google Sheets API:
49
+ - Go to [Google Cloud Console](https://console.cloud.google.com)
50
+ - Create a new project or select existing one
51
+ - Enable Google Sheets API
52
+ - Create OAuth 2.0 credentials
53
+ - Download credentials JSON file
54
+
55
+ 2. Set up environment variables:
56
+
57
+ ```bash
58
+ GOOGLE_CREDENTIALS_PATH=/path/to/credentials.json
59
+ ENCRYPTION_KEY=your-encryption-key
60
+ ```
61
+
62
+ 3. Basic Usage:
63
+
64
+ ```python
65
+ from gsab import SheetConnection, Schema, Field, FieldType, SheetManager
66
+
67
+ # Define your schema
68
+ schema = Schema("users", [
69
+ Field("id", FieldType.INTEGER, required=True, unique=True),
70
+ Field("email", FieldType.STRING, required=True),
71
+ Field("password", FieldType.STRING, required=True, encrypted=True)
72
+ ])
73
+
74
+ # Connect and use
75
+ async def main():
76
+ connection = SheetConnection()
77
+ await connection.connect()
78
+
79
+ sheet_manager = SheetManager(connection, schema)
80
+
81
+ # Create a new sheet
82
+ sheet = await sheet_manager.create_sheet("Users Data")
83
+
84
+ # Insert data
85
+ await sheet_manager.insert({
86
+ "id": 1,
87
+ "email": "user@example.com",
88
+ "password": "secretpass123" # Will be automatically encrypted
89
+ })
90
+
91
+ ```
92
+
93
+ ## Schema Definition
94
+
95
+ Define your data structure with type checking and validation:
96
+
97
+ ```python
98
+ from gsab import Schema, Field, FieldType, ValidationRule
99
+
100
+ schema = Schema("users", [
101
+ Field("id", FieldType.INTEGER, required=True, unique=True),
102
+ Field("email", FieldType.STRING, required=True),
103
+ Field("age", FieldType.INTEGER, min_value=0, max_value=150),
104
+ Field("password", FieldType.STRING, required=True, encrypted=True)
105
+ ])
106
+ ```
107
+
108
+ ## Security Features
109
+
110
+ ### Field Encryption
111
+
112
+ Sensitive data is automatically encrypted when the field is marked with `encrypted=True`:
113
+
114
+ ```python
115
+ # Fields marked as encrypted will be automatically handled
116
+ schema = Schema("users", [
117
+ Field("ssn", FieldType.STRING, encrypted=True),
118
+ Field("credit_card", FieldType.STRING, encrypted=True)
119
+ ])
120
+ ```
121
+
122
+ <!-- ## Contributing
123
+
124
+ We love your input! We want to make contributing to GSheetsDB as easy and transparent as possible, whether it's:
125
+
126
+ - Reporting a bug
127
+ - Discussing the current state of the code
128
+ - Submitting a fix
129
+ - Proposing new features
130
+ - Becoming a maintainer
131
+
132
+ Check out our [Contributing Guide](CONTRIBUTING.md) for ways to get started. -->
133
+
134
+ ## License
135
+
136
+ This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
137
+
138
+ [![PyPI version](https://badge.fury.io/py/gsab.svg)](https://badge.fury.io/py/gsab)
139
+ [![Tests](https://github.com/ajmalaksar25/gsab/actions/workflows/tests.yml/badge.svg)](https://github.com/ajmalaksar25/gsab/actions/workflows/tests.yml)
140
+ [![codecov](https://codecov.io/gh/ajmalaksar25/gsab/branch/main/graph/badge.svg)](https://codecov.io/gh/ajmalaksar25/gsab)
gsab-0.1.0/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # Google Sheets as Backend (GSAB)
2
+
3
+ A Python library that enables using Google Sheets as a database backend with features like schema validation and encryption.
4
+
5
+ ## Features
6
+
7
+ - 🔒 Secure Google Sheets integration with OAuth2
8
+ - 📊 Schema validation and type checking
9
+ - 🔐 Field-level encryption for sensitive data
10
+ - 🌐 Async/await support
11
+ - 📝 Comprehensive logging
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install gsheets-db
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ 1. Set up Google Cloud Project and enable Google Sheets API:
22
+ - Go to [Google Cloud Console](https://console.cloud.google.com)
23
+ - Create a new project or select existing one
24
+ - Enable Google Sheets API
25
+ - Create OAuth 2.0 credentials
26
+ - Download credentials JSON file
27
+
28
+ 2. Set up environment variables:
29
+
30
+ ```bash
31
+ GOOGLE_CREDENTIALS_PATH=/path/to/credentials.json
32
+ ENCRYPTION_KEY=your-encryption-key
33
+ ```
34
+
35
+ 3. Basic Usage:
36
+
37
+ ```python
38
+ from gsab import SheetConnection, Schema, Field, FieldType, SheetManager
39
+
40
+ # Define your schema
41
+ schema = Schema("users", [
42
+ Field("id", FieldType.INTEGER, required=True, unique=True),
43
+ Field("email", FieldType.STRING, required=True),
44
+ Field("password", FieldType.STRING, required=True, encrypted=True)
45
+ ])
46
+
47
+ # Connect and use
48
+ async def main():
49
+ connection = SheetConnection()
50
+ await connection.connect()
51
+
52
+ sheet_manager = SheetManager(connection, schema)
53
+
54
+ # Create a new sheet
55
+ sheet = await sheet_manager.create_sheet("Users Data")
56
+
57
+ # Insert data
58
+ await sheet_manager.insert({
59
+ "id": 1,
60
+ "email": "user@example.com",
61
+ "password": "secretpass123" # Will be automatically encrypted
62
+ })
63
+
64
+ ```
65
+
66
+ ## Schema Definition
67
+
68
+ Define your data structure with type checking and validation:
69
+
70
+ ```python
71
+ from gsab import Schema, Field, FieldType, ValidationRule
72
+
73
+ schema = Schema("users", [
74
+ Field("id", FieldType.INTEGER, required=True, unique=True),
75
+ Field("email", FieldType.STRING, required=True),
76
+ Field("age", FieldType.INTEGER, min_value=0, max_value=150),
77
+ Field("password", FieldType.STRING, required=True, encrypted=True)
78
+ ])
79
+ ```
80
+
81
+ ## Security Features
82
+
83
+ ### Field Encryption
84
+
85
+ Sensitive data is automatically encrypted when the field is marked with `encrypted=True`:
86
+
87
+ ```python
88
+ # Fields marked as encrypted will be automatically handled
89
+ schema = Schema("users", [
90
+ Field("ssn", FieldType.STRING, encrypted=True),
91
+ Field("credit_card", FieldType.STRING, encrypted=True)
92
+ ])
93
+ ```
94
+
95
+ <!-- ## Contributing
96
+
97
+ We love your input! We want to make contributing to GSheetsDB as easy and transparent as possible, whether it's:
98
+
99
+ - Reporting a bug
100
+ - Discussing the current state of the code
101
+ - Submitting a fix
102
+ - Proposing new features
103
+ - Becoming a maintainer
104
+
105
+ Check out our [Contributing Guide](CONTRIBUTING.md) for ways to get started. -->
106
+
107
+ ## License
108
+
109
+ This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
110
+
111
+ [![PyPI version](https://badge.fury.io/py/gsab.svg)](https://badge.fury.io/py/gsab)
112
+ [![Tests](https://github.com/ajmalaksar25/gsab/actions/workflows/tests.yml/badge.svg)](https://github.com/ajmalaksar25/gsab/actions/workflows/tests.yml)
113
+ [![codecov](https://codecov.io/gh/ajmalaksar25/gsab/branch/main/graph/badge.svg)](https://codecov.io/gh/ajmalaksar25/gsab)
@@ -0,0 +1 @@
1
+ """Authentication package."""
@@ -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
@@ -0,0 +1 @@
1
+ """Core package."""
@@ -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
@@ -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
+