abs-integration-core 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.
- abs_integration_core-0.1.0/PKG-INFO +176 -0
- abs_integration_core-0.1.0/README.md +158 -0
- abs_integration_core-0.1.0/abs_integration_core/__init__.py +0 -0
- abs_integration_core-0.1.0/abs_integration_core/models/__init__.py +3 -0
- abs_integration_core-0.1.0/abs_integration_core/models/integration_model.py +14 -0
- abs_integration_core-0.1.0/abs_integration_core/repository/__init__.py +3 -0
- abs_integration_core-0.1.0/abs_integration_core/repository/integration_repository.py +61 -0
- abs_integration_core-0.1.0/abs_integration_core/schema/__init__.py +17 -0
- abs_integration_core-0.1.0/abs_integration_core/schema/common_schema.py +17 -0
- abs_integration_core-0.1.0/abs_integration_core/schema/integration_schema.py +42 -0
- abs_integration_core-0.1.0/abs_integration_core/service/__init__.py +3 -0
- abs_integration_core-0.1.0/abs_integration_core/service/base_service.py +125 -0
- abs_integration_core-0.1.0/abs_integration_core/utils/encryption.py +98 -0
- abs_integration_core-0.1.0/pyproject.toml +26 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: abs-integration-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Core utilities for building OAuth-based integrations in FastAPI applications
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: AutoBridgeSystems
|
|
7
|
+
Author-email: info@autobridgesystems.com
|
|
8
|
+
Requires-Python: >=3.13,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Requires-Dist: abs-exception-core (>=0.1.2,<0.2.0)
|
|
13
|
+
Requires-Dist: abs-repository-core (>=0.2.2,<0.3.0)
|
|
14
|
+
Requires-Dist: fastapi (>=0.95.0,<2.0.0)
|
|
15
|
+
Requires-Dist: pydantic (>=2.0.0,<3.0.0)
|
|
16
|
+
Requires-Dist: sqlalchemy (>=2.0.0,<3.0.0)
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Integration Core
|
|
20
|
+
|
|
21
|
+
A core library for building and managing OAuth-based third-party integrations in FastAPI applications.
|
|
22
|
+
|
|
23
|
+
## Overview
|
|
24
|
+
|
|
25
|
+
`abs-integration-core` provides a set of reusable components to simplify the implementation of OAuth-based integrations with third-party services. It includes models, schemas, repositories, and a base service class that can be extended for specific integration providers.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- **Integration Model**: SQLAlchemy model for storing OAuth tokens and integration metadata
|
|
30
|
+
- **Standard Schemas**: Pydantic models for integration data validation and serialization
|
|
31
|
+
- **Repository Layer**: Data access layer for CRUD operations on integration records
|
|
32
|
+
- **Base Service**: Abstract base class for implementing integration services for different providers
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install abs-integration-core
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Dependencies
|
|
41
|
+
|
|
42
|
+
This package depends on:
|
|
43
|
+
|
|
44
|
+
- `fastapi`: For API routing and endpoint handling
|
|
45
|
+
- `sqlalchemy`: For database ORM functionality
|
|
46
|
+
- `pydantic`: For data validation
|
|
47
|
+
- `abs-exception-core`: For standardized exception handling
|
|
48
|
+
- `abs-repository-core`: For base repository pattern implementation
|
|
49
|
+
- `abs-auth-rbac-core`: For authentication and base models
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
### Models
|
|
54
|
+
|
|
55
|
+
The core model represents an OAuth integration with a third-party provider:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from abs_integration_core.models import Integration
|
|
59
|
+
|
|
60
|
+
# The Integration model includes:
|
|
61
|
+
# - provider_name: String(255)
|
|
62
|
+
# - access_token: Text
|
|
63
|
+
# - refresh_token: Text
|
|
64
|
+
# - expires_in: Integer
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Schemas
|
|
68
|
+
|
|
69
|
+
Various Pydantic schemas are available for request/response handling:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from abs_integration_core import (
|
|
73
|
+
TokenData,
|
|
74
|
+
IsConnectedResponse,
|
|
75
|
+
CreateIntegration,
|
|
76
|
+
UpdateIntegration,
|
|
77
|
+
ResponseSchema
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Example: Create a standard API response
|
|
81
|
+
response = ResponseSchema(
|
|
82
|
+
status=200,
|
|
83
|
+
message="Integration created successfully",
|
|
84
|
+
data=IsConnectedResponse(provider="sharepoint", connected=True)
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Repository
|
|
89
|
+
|
|
90
|
+
The `IntegrationRepository` provides data access methods:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from abs_integration_core import IntegrationRepository
|
|
94
|
+
|
|
95
|
+
# Initialize repository with a database session factory
|
|
96
|
+
repo = IntegrationRepository(db_session)
|
|
97
|
+
|
|
98
|
+
# Available methods:
|
|
99
|
+
# - create_integration(integration_data)
|
|
100
|
+
# - update_integration(integration_id, update_data)
|
|
101
|
+
# - get_by_provider(provider_name)
|
|
102
|
+
# - get_all()
|
|
103
|
+
# - delete_by_provider(provider_name)
|
|
104
|
+
# - refresh_token(provider_name, new_access_token, new_refresh_token, new_expires_in)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Base Service
|
|
108
|
+
|
|
109
|
+
Extend the `IntegrationBaseService` to implement provider-specific integration services:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from abs_integration_core import IntegrationBaseService
|
|
113
|
+
|
|
114
|
+
class SharepointIntegrationService(IntegrationBaseService):
|
|
115
|
+
def __init__(self, provider_name, integration_repository):
|
|
116
|
+
super().__init__(provider_name, integration_repository)
|
|
117
|
+
|
|
118
|
+
def get_auth_url(self):
|
|
119
|
+
# Implementation for generating OAuth URL
|
|
120
|
+
|
|
121
|
+
async def get_token_data(self, code):
|
|
122
|
+
# Implementation for exchanging code for tokens
|
|
123
|
+
|
|
124
|
+
async def handle_oauth_callback(self, code):
|
|
125
|
+
# Implementation for processing OAuth callback
|
|
126
|
+
|
|
127
|
+
async def refresh_token(self):
|
|
128
|
+
# Implementation for refreshing tokens
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Implementing a New Integration
|
|
132
|
+
|
|
133
|
+
To implement a new integration provider:
|
|
134
|
+
|
|
135
|
+
1. Create a new service class that extends `IntegrationBaseService`
|
|
136
|
+
2. Implement the required abstract methods:
|
|
137
|
+
- `get_auth_url()`
|
|
138
|
+
- `get_token_data(code)`
|
|
139
|
+
- `handle_oauth_callback(code)`
|
|
140
|
+
- `refresh_token()`
|
|
141
|
+
3. Register your service in your FastAPI application
|
|
142
|
+
4. Create API routes to initiate auth flow, handle callbacks, etc.
|
|
143
|
+
|
|
144
|
+
## Example: Creating API Routes
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from fastapi import APIRouter, Depends
|
|
148
|
+
from abs_integration_core import ResponseSchema, IsConnectedResponse
|
|
149
|
+
|
|
150
|
+
router = APIRouter(prefix="/integration", tags=["integration"])
|
|
151
|
+
|
|
152
|
+
@router.get("/{provider_name}/connect")
|
|
153
|
+
async def integration_connect(
|
|
154
|
+
service: IntegrationBaseService = Depends(get_integration_service)
|
|
155
|
+
):
|
|
156
|
+
auth_data = service.get_auth_url()
|
|
157
|
+
return RedirectResponse(url=auth_data["auth_url"])
|
|
158
|
+
|
|
159
|
+
@router.get("/{provider_name}/callback")
|
|
160
|
+
async def integration_callback(
|
|
161
|
+
code: str,
|
|
162
|
+
service: IntegrationBaseService = Depends(get_integration_service)
|
|
163
|
+
):
|
|
164
|
+
token_data = await service.handle_oauth_callback(code)
|
|
165
|
+
return ResponseSchema(
|
|
166
|
+
data=IsConnectedResponse(
|
|
167
|
+
provider=service.provider_name,
|
|
168
|
+
connected=True
|
|
169
|
+
),
|
|
170
|
+
message=f"Integration connected successfully"
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Integration Core
|
|
2
|
+
|
|
3
|
+
A core library for building and managing OAuth-based third-party integrations in FastAPI applications.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`abs-integration-core` provides a set of reusable components to simplify the implementation of OAuth-based integrations with third-party services. It includes models, schemas, repositories, and a base service class that can be extended for specific integration providers.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Integration Model**: SQLAlchemy model for storing OAuth tokens and integration metadata
|
|
12
|
+
- **Standard Schemas**: Pydantic models for integration data validation and serialization
|
|
13
|
+
- **Repository Layer**: Data access layer for CRUD operations on integration records
|
|
14
|
+
- **Base Service**: Abstract base class for implementing integration services for different providers
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install abs-integration-core
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Dependencies
|
|
23
|
+
|
|
24
|
+
This package depends on:
|
|
25
|
+
|
|
26
|
+
- `fastapi`: For API routing and endpoint handling
|
|
27
|
+
- `sqlalchemy`: For database ORM functionality
|
|
28
|
+
- `pydantic`: For data validation
|
|
29
|
+
- `abs-exception-core`: For standardized exception handling
|
|
30
|
+
- `abs-repository-core`: For base repository pattern implementation
|
|
31
|
+
- `abs-auth-rbac-core`: For authentication and base models
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### Models
|
|
36
|
+
|
|
37
|
+
The core model represents an OAuth integration with a third-party provider:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from abs_integration_core.models import Integration
|
|
41
|
+
|
|
42
|
+
# The Integration model includes:
|
|
43
|
+
# - provider_name: String(255)
|
|
44
|
+
# - access_token: Text
|
|
45
|
+
# - refresh_token: Text
|
|
46
|
+
# - expires_in: Integer
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Schemas
|
|
50
|
+
|
|
51
|
+
Various Pydantic schemas are available for request/response handling:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from abs_integration_core import (
|
|
55
|
+
TokenData,
|
|
56
|
+
IsConnectedResponse,
|
|
57
|
+
CreateIntegration,
|
|
58
|
+
UpdateIntegration,
|
|
59
|
+
ResponseSchema
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Example: Create a standard API response
|
|
63
|
+
response = ResponseSchema(
|
|
64
|
+
status=200,
|
|
65
|
+
message="Integration created successfully",
|
|
66
|
+
data=IsConnectedResponse(provider="sharepoint", connected=True)
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Repository
|
|
71
|
+
|
|
72
|
+
The `IntegrationRepository` provides data access methods:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from abs_integration_core import IntegrationRepository
|
|
76
|
+
|
|
77
|
+
# Initialize repository with a database session factory
|
|
78
|
+
repo = IntegrationRepository(db_session)
|
|
79
|
+
|
|
80
|
+
# Available methods:
|
|
81
|
+
# - create_integration(integration_data)
|
|
82
|
+
# - update_integration(integration_id, update_data)
|
|
83
|
+
# - get_by_provider(provider_name)
|
|
84
|
+
# - get_all()
|
|
85
|
+
# - delete_by_provider(provider_name)
|
|
86
|
+
# - refresh_token(provider_name, new_access_token, new_refresh_token, new_expires_in)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Base Service
|
|
90
|
+
|
|
91
|
+
Extend the `IntegrationBaseService` to implement provider-specific integration services:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from abs_integration_core import IntegrationBaseService
|
|
95
|
+
|
|
96
|
+
class SharepointIntegrationService(IntegrationBaseService):
|
|
97
|
+
def __init__(self, provider_name, integration_repository):
|
|
98
|
+
super().__init__(provider_name, integration_repository)
|
|
99
|
+
|
|
100
|
+
def get_auth_url(self):
|
|
101
|
+
# Implementation for generating OAuth URL
|
|
102
|
+
|
|
103
|
+
async def get_token_data(self, code):
|
|
104
|
+
# Implementation for exchanging code for tokens
|
|
105
|
+
|
|
106
|
+
async def handle_oauth_callback(self, code):
|
|
107
|
+
# Implementation for processing OAuth callback
|
|
108
|
+
|
|
109
|
+
async def refresh_token(self):
|
|
110
|
+
# Implementation for refreshing tokens
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Implementing a New Integration
|
|
114
|
+
|
|
115
|
+
To implement a new integration provider:
|
|
116
|
+
|
|
117
|
+
1. Create a new service class that extends `IntegrationBaseService`
|
|
118
|
+
2. Implement the required abstract methods:
|
|
119
|
+
- `get_auth_url()`
|
|
120
|
+
- `get_token_data(code)`
|
|
121
|
+
- `handle_oauth_callback(code)`
|
|
122
|
+
- `refresh_token()`
|
|
123
|
+
3. Register your service in your FastAPI application
|
|
124
|
+
4. Create API routes to initiate auth flow, handle callbacks, etc.
|
|
125
|
+
|
|
126
|
+
## Example: Creating API Routes
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from fastapi import APIRouter, Depends
|
|
130
|
+
from abs_integration_core import ResponseSchema, IsConnectedResponse
|
|
131
|
+
|
|
132
|
+
router = APIRouter(prefix="/integration", tags=["integration"])
|
|
133
|
+
|
|
134
|
+
@router.get("/{provider_name}/connect")
|
|
135
|
+
async def integration_connect(
|
|
136
|
+
service: IntegrationBaseService = Depends(get_integration_service)
|
|
137
|
+
):
|
|
138
|
+
auth_data = service.get_auth_url()
|
|
139
|
+
return RedirectResponse(url=auth_data["auth_url"])
|
|
140
|
+
|
|
141
|
+
@router.get("/{provider_name}/callback")
|
|
142
|
+
async def integration_callback(
|
|
143
|
+
code: str,
|
|
144
|
+
service: IntegrationBaseService = Depends(get_integration_service)
|
|
145
|
+
):
|
|
146
|
+
token_data = await service.handle_oauth_callback(code)
|
|
147
|
+
return ResponseSchema(
|
|
148
|
+
data=IsConnectedResponse(
|
|
149
|
+
provider=service.provider_name,
|
|
150
|
+
connected=True
|
|
151
|
+
),
|
|
152
|
+
message=f"Integration connected successfully"
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from sqlalchemy import Column, String, Integer, Text
|
|
2
|
+
from sqlalchemy.orm import relationship
|
|
3
|
+
|
|
4
|
+
from abs_repository_core.models import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Integration(BaseModel):
|
|
8
|
+
"""Integration model"""
|
|
9
|
+
__tablename__ = "gov_integrations"
|
|
10
|
+
|
|
11
|
+
provider_name = Column(String(255), nullable=False)
|
|
12
|
+
access_token = Column(Text, nullable=False)
|
|
13
|
+
refresh_token = Column(Text, nullable=False)
|
|
14
|
+
expires_in = Column(Integer, nullable=False)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from abs_repository_core.repository import BaseRepository
|
|
2
|
+
from abs_integration_core.models import Integration
|
|
3
|
+
from typing import Callable
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
from abs_integration_core.schema import CreateIntegration
|
|
6
|
+
from abs_integration_core.schema import TokenData
|
|
7
|
+
|
|
8
|
+
class IntegrationRepository(BaseRepository):
|
|
9
|
+
def __init__(self, db: Callable[..., Session]):
|
|
10
|
+
self.db = db
|
|
11
|
+
super().__init__(db, Integration)
|
|
12
|
+
|
|
13
|
+
def create_integration(self, integration_data: CreateIntegration) -> Integration:
|
|
14
|
+
"""
|
|
15
|
+
Create a new integration record.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
integration_data: Integration data including provider_name, access_token, etc.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
The created integration object
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
DuplicatedError: If integration with same provider already exists
|
|
25
|
+
"""
|
|
26
|
+
new_integration = Integration(
|
|
27
|
+
provider_name=integration_data.provider_name,
|
|
28
|
+
access_token=integration_data.access_token,
|
|
29
|
+
refresh_token=integration_data.refresh_token,
|
|
30
|
+
expires_in=integration_data.expires_in
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
integration = super().create(new_integration)
|
|
34
|
+
return integration
|
|
35
|
+
|
|
36
|
+
def refresh_token(
|
|
37
|
+
self,
|
|
38
|
+
provider_name: str,
|
|
39
|
+
token_data: TokenData
|
|
40
|
+
) -> Integration:
|
|
41
|
+
"""
|
|
42
|
+
Update token information for a specific integration.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
provider_name: The integration provider name
|
|
46
|
+
token_data: The data to update
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
The updated integration object
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
NotFoundError: If integration doesn't exist
|
|
53
|
+
"""
|
|
54
|
+
integration = super().read_by_attr(
|
|
55
|
+
attr="provider_name",
|
|
56
|
+
value=provider_name
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
super().update(integration.id, token_data)
|
|
60
|
+
|
|
61
|
+
return integration
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .common_schema import ResponseSchema
|
|
2
|
+
from .integration_schema import (
|
|
3
|
+
TokenData,
|
|
4
|
+
Integration,
|
|
5
|
+
IsConnectedResponse,
|
|
6
|
+
CreateIntegration,
|
|
7
|
+
UpdateIntegration
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ResponseSchema",
|
|
12
|
+
"TokenData",
|
|
13
|
+
"Integration",
|
|
14
|
+
"IsConnectedResponse",
|
|
15
|
+
"CreateIntegration",
|
|
16
|
+
"UpdateIntegration"
|
|
17
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Generic, Optional, TypeVar
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from fastapi import status
|
|
4
|
+
|
|
5
|
+
T = TypeVar('T')
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ResponseSchema(BaseModel, Generic[T]):
|
|
9
|
+
"""
|
|
10
|
+
Standard response schema for all API endpoints
|
|
11
|
+
"""
|
|
12
|
+
status: int = status.HTTP_200_OK
|
|
13
|
+
message: str = "Success"
|
|
14
|
+
data: Optional[T] = None
|
|
15
|
+
|
|
16
|
+
class Config:
|
|
17
|
+
arbitrary_types_allowed = True
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from abs_repository_core.schemas import ModelBaseInfo, make_optional
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TokenData(BaseModel):
|
|
7
|
+
access_token: str
|
|
8
|
+
refresh_token: str
|
|
9
|
+
expires_in: int
|
|
10
|
+
|
|
11
|
+
class Config:
|
|
12
|
+
extra = "allow"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Integration(make_optional(ModelBaseInfo), TokenData):
|
|
16
|
+
provider_name: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class IsConnectedResponse(BaseModel):
|
|
20
|
+
provider: str
|
|
21
|
+
connected: bool
|
|
22
|
+
|
|
23
|
+
class Config:
|
|
24
|
+
extra = "allow"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CreateIntegration(BaseModel):
|
|
28
|
+
"""Model for creating a new integration"""
|
|
29
|
+
provider_name: str
|
|
30
|
+
access_token: str
|
|
31
|
+
refresh_token: str
|
|
32
|
+
expires_in: int
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class UpdateIntegration(BaseModel):
|
|
36
|
+
"""Model for updating an existing integration"""
|
|
37
|
+
access_token: Optional[str] = None
|
|
38
|
+
refresh_token: Optional[str] = None
|
|
39
|
+
expires_in: Optional[int] = None
|
|
40
|
+
|
|
41
|
+
class Config:
|
|
42
|
+
extra = "ignore"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Dict, Optional, List
|
|
3
|
+
from abs_integration_core.schema import TokenData, Integration
|
|
4
|
+
from abs_integration_core.repository import IntegrationRepository
|
|
5
|
+
from abs_exception_core.exceptions import NotFoundError
|
|
6
|
+
from abs_repository_core.services.base_service import BaseService
|
|
7
|
+
from abs_integration_core.utils.encryption import Encryption
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IntegrationBaseService(ABC, BaseService):
|
|
11
|
+
"""
|
|
12
|
+
Base abstract class for all integration services.
|
|
13
|
+
Any integration service should inherit from this class and implement its methods.
|
|
14
|
+
"""
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
provider_name: str,
|
|
18
|
+
integration_repository: IntegrationRepository,
|
|
19
|
+
encryption: Encryption
|
|
20
|
+
):
|
|
21
|
+
self.provider_name = provider_name
|
|
22
|
+
self.encryption = encryption
|
|
23
|
+
super().__init__(integration_repository)
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def get_auth_url(self, state: Optional[Dict] = None) -> Dict[str, str]:
|
|
27
|
+
"""
|
|
28
|
+
Generate an authentication URL for OAuth flow.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
state: Optional state dictionary to include in the OAuth flow
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A dictionary containing the auth URL and other necessary information
|
|
35
|
+
"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
async def get_token_data(self, code: str) -> TokenData:
|
|
40
|
+
"""
|
|
41
|
+
Exchange authorization code for token data.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
code: The authorization code from OAuth callback
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
TokenData object with access_token, refresh_token and expires_in
|
|
48
|
+
"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
async def handle_oauth_callback(self, code: str) -> TokenData:
|
|
53
|
+
"""
|
|
54
|
+
Handle the OAuth callback and store tokens.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
code: The authorization code from OAuth callback
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
TokenData object
|
|
61
|
+
"""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
async def refresh_token(self) -> Optional[TokenData]:
|
|
66
|
+
"""
|
|
67
|
+
Refresh the access token using the refresh token.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Updated TokenData if successful, None otherwise
|
|
71
|
+
"""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def get_query_by_provider(self):
|
|
75
|
+
return super().get_by_attr(
|
|
76
|
+
attr="provider_name",
|
|
77
|
+
value=self.provider_name
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def get_integration(self) -> Optional[TokenData]:
|
|
81
|
+
"""
|
|
82
|
+
Get integration data.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
TokenData if integration exists, None otherwise
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
integration = self.get_query_by_provider()
|
|
89
|
+
return integration
|
|
90
|
+
except Exception:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def get_all_integrations(self) -> List[Integration]:
|
|
94
|
+
"""
|
|
95
|
+
Get all integrations.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
List of TokenData objects
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
integrations = super().get_list()
|
|
102
|
+
return integrations
|
|
103
|
+
except Exception:
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
def delete_integration(self) -> bool:
|
|
107
|
+
"""
|
|
108
|
+
Delete an integration.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
True if deleted, False otherwise
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
integration = self.get_query_by_provider()
|
|
115
|
+
super().remove_by_id(integration.id)
|
|
116
|
+
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
except NotFoundError:
|
|
120
|
+
# If the integration doesn't exist, consider it "deleted"
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
print(f"Error deleting integration: {str(e)}")
|
|
125
|
+
return False
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from cryptography.fernet import Fernet
|
|
2
|
+
import base64
|
|
3
|
+
import hashlib
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Encryption:
|
|
8
|
+
def __init__(self, key: Optional[str] = None):
|
|
9
|
+
self.key = key
|
|
10
|
+
|
|
11
|
+
# Encryption key management
|
|
12
|
+
def get_encryption_key(self):
|
|
13
|
+
"""
|
|
14
|
+
Get encryption key from environment variable or generate one if not exists
|
|
15
|
+
|
|
16
|
+
This should ideally be set in environment variables and consistent across restarts
|
|
17
|
+
Returns:
|
|
18
|
+
A valid Fernet key (32 url-safe base64-encoded bytes)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
if not self.key:
|
|
22
|
+
# For demo only - in production, you should set a permanent key
|
|
23
|
+
# and store it securely in environment variables or a secret manager
|
|
24
|
+
self.key = Fernet.generate_key().decode()
|
|
25
|
+
print(f"WARNING: Generated temporary encryption key: {self.key}")
|
|
26
|
+
print("Set this in your environment as TOKEN_ENCRYPTION_KEY")
|
|
27
|
+
return self.key.encode() if isinstance(self.key, str) else self.key
|
|
28
|
+
|
|
29
|
+
# If key is not in valid Fernet format, convert it to a valid key
|
|
30
|
+
try:
|
|
31
|
+
# Try to use the key as is - if it's already a valid Fernet key
|
|
32
|
+
if isinstance(self.key, str):
|
|
33
|
+
self.key = self.key.encode()
|
|
34
|
+
|
|
35
|
+
# Test if it's a valid key
|
|
36
|
+
Fernet(self.key)
|
|
37
|
+
return self.key
|
|
38
|
+
except Exception:
|
|
39
|
+
# Not a valid Fernet key - convert to a valid one
|
|
40
|
+
# Use SHA-256 to get a consistent length, then encode in base64
|
|
41
|
+
if isinstance(self.key, str):
|
|
42
|
+
self.key = self.key.encode()
|
|
43
|
+
|
|
44
|
+
hashed_key = hashlib.sha256(self.key).digest()
|
|
45
|
+
encoded_key = base64.urlsafe_b64encode(hashed_key)
|
|
46
|
+
|
|
47
|
+
print("WARNING: Converted invalid encryption key to valid Fernet format")
|
|
48
|
+
return encoded_key
|
|
49
|
+
|
|
50
|
+
def encrypt_token(self, token: str) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Encrypt a token string using Fernet symmetric encryption
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
token: The raw token string to encrypt
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
The encrypted token as a base64 string
|
|
59
|
+
"""
|
|
60
|
+
if not token:
|
|
61
|
+
return token
|
|
62
|
+
|
|
63
|
+
# Get the encryption key
|
|
64
|
+
key = self.get_encryption_key()
|
|
65
|
+
|
|
66
|
+
# Create a Fernet cipher
|
|
67
|
+
cipher = Fernet(key)
|
|
68
|
+
|
|
69
|
+
# Encrypt the token (which must be bytes)
|
|
70
|
+
encrypted_token = cipher.encrypt(token.encode())
|
|
71
|
+
|
|
72
|
+
# Return as a string for storage
|
|
73
|
+
return encrypted_token.decode()
|
|
74
|
+
|
|
75
|
+
def decrypt_token(self, encrypted_token: str) -> str:
|
|
76
|
+
"""
|
|
77
|
+
Decrypt a token string that was encrypted with Fernet
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
encrypted_token: The encrypted token as a base64 string
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The original decrypted token string
|
|
84
|
+
"""
|
|
85
|
+
if not encrypted_token:
|
|
86
|
+
return encrypted_token
|
|
87
|
+
|
|
88
|
+
# Get the encryption key
|
|
89
|
+
key = self.get_encryption_key()
|
|
90
|
+
|
|
91
|
+
# Create a Fernet cipher
|
|
92
|
+
cipher = Fernet(key)
|
|
93
|
+
|
|
94
|
+
# Decrypt the token (convert string to bytes first)
|
|
95
|
+
decrypted_token = cipher.decrypt(encrypted_token.encode())
|
|
96
|
+
|
|
97
|
+
# Return as a string
|
|
98
|
+
return decrypted_token.decode()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "abs-integration-core"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Core utilities for building OAuth-based integrations in FastAPI applications"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "AutoBridgeSystems", email = "info@autobridgesystems.com"}
|
|
7
|
+
]
|
|
8
|
+
license = {text = "MIT"}
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.13,<4.0"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"fastapi >=0.95.0,<2.0.0",
|
|
13
|
+
"sqlalchemy >=2.0.0,<3.0.0",
|
|
14
|
+
"pydantic >=2.0.0,<3.0.0",
|
|
15
|
+
"abs-exception-core (>=0.1.2,<0.2.0)",
|
|
16
|
+
"abs-repository-core (>=0.2.2,<0.3.0)",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[tool.poetry]
|
|
20
|
+
packages = [
|
|
21
|
+
{ include = "abs_integration_core" }
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
26
|
+
build-backend = "poetry.core.masonry.api"
|