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 +21 -0
- gsab-0.1.0/PKG-INFO +140 -0
- gsab-0.1.0/README.md +113 -0
- gsab-0.1.0/auth/__init__.py +1 -0
- gsab-0.1.0/auth/authenticator.py +39 -0
- gsab-0.1.0/core/__init__.py +1 -0
- gsab-0.1.0/core/connection.py +39 -0
- gsab-0.1.0/core/schema.py +233 -0
- gsab-0.1.0/core/sheet_manager.py +470 -0
- gsab-0.1.0/gsab.egg-info/PKG-INFO +140 -0
- gsab-0.1.0/gsab.egg-info/SOURCES.txt +21 -0
- gsab-0.1.0/gsab.egg-info/dependency_links.txt +1 -0
- gsab-0.1.0/gsab.egg-info/requires.txt +13 -0
- gsab-0.1.0/gsab.egg-info/top_level.txt +3 -0
- gsab-0.1.0/pyproject.toml +9 -0
- gsab-0.1.0/setup.cfg +4 -0
- gsab-0.1.0/setup.py +39 -0
- gsab-0.1.0/tests/__init__.py +0 -0
- gsab-0.1.0/tests/test_auth.py +34 -0
- gsab-0.1.0/tests/test_encryption.py +73 -0
- gsab-0.1.0/tests/test_gsheets_db.py +207 -0
- gsab-0.1.0/tests/test_installation.py +38 -0
- gsab-0.1.0/tests/test_sheet_manager.py +171 -0
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
|
+
[](https://badge.fury.io/py/gsab)
|
|
139
|
+
[](https://github.com/ajmalaksar25/gsab/actions/workflows/tests.yml)
|
|
140
|
+
[](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
|
+
[](https://badge.fury.io/py/gsab)
|
|
112
|
+
[](https://github.com/ajmalaksar25/gsab/actions/workflows/tests.yml)
|
|
113
|
+
[](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
|
+
|