anzar 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.
- anzar-0.1.0/PKG-INFO +173 -0
- anzar-0.1.0/README.md +160 -0
- anzar-0.1.0/pyproject.toml +31 -0
- anzar-0.1.0/src/anzar/__init__.py +21 -0
- anzar-0.1.0/src/anzar/_api/client.py +37 -0
- anzar-0.1.0/src/anzar/_api/http_interceptor.py +113 -0
- anzar-0.1.0/src/anzar/_auth/authenticator.py +81 -0
- anzar-0.1.0/src/anzar/_auth/middleware.py +0 -0
- anzar-0.1.0/src/anzar/_models/auth.py +29 -0
- anzar-0.1.0/src/anzar/_models/user.py +18 -0
- anzar-0.1.0/src/anzar/_utils/config.py +16 -0
- anzar-0.1.0/src/anzar/_utils/errors.py +5 -0
- anzar-0.1.0/src/anzar/_utils/storage.py +29 -0
- anzar-0.1.0/src/anzar/_utils/types.py +68 -0
- anzar-0.1.0/src/anzar/_utils/validator.py +36 -0
- anzar-0.1.0/src/anzar/py.typed +0 -0
- anzar-0.1.0/src/anzar/types.py +8 -0
anzar-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: anzar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Hakou Guelfen
|
|
6
|
+
Author-email: Hakou Guelfen <hakoudev@gmail.com>
|
|
7
|
+
Requires-Dist: dotenv>=0.9.9
|
|
8
|
+
Requires-Dist: keyring>=25.6.0
|
|
9
|
+
Requires-Dist: pydantic[email]>=2.11.7
|
|
10
|
+
Requires-Dist: requests>=2.32.5
|
|
11
|
+
Requires-Python: >=3.13
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Anzar SDK Documentation
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install anzar
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from anzar import AnzarAuth
|
|
26
|
+
|
|
27
|
+
# Initialize the SDK
|
|
28
|
+
auth = AnzarAuth
|
|
29
|
+
|
|
30
|
+
# Use the authenticated client
|
|
31
|
+
# (Add specific usage examples here)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
### Environment Variables
|
|
37
|
+
|
|
38
|
+
Create a `.env` file in your project root:
|
|
39
|
+
|
|
40
|
+
```env
|
|
41
|
+
# Add your environment variables here
|
|
42
|
+
ANZAR_API_KEY=your_api_key
|
|
43
|
+
ANZAR_BASE_URL=https://api.anzar.com
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## API Reference
|
|
47
|
+
|
|
48
|
+
### AnzarAuth
|
|
49
|
+
|
|
50
|
+
Authentication manager for user login, registration, and session management.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from anzar import AnzarAuth
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### Methods
|
|
57
|
+
|
|
58
|
+
##### `login(email, password)`
|
|
59
|
+
|
|
60
|
+
Authenticate a user with credentials.
|
|
61
|
+
|
|
62
|
+
**Parameters:**
|
|
63
|
+
- `email` (str): User's email
|
|
64
|
+
- `password` (str): User's password
|
|
65
|
+
|
|
66
|
+
**Returns:**
|
|
67
|
+
- `User`: User object on success
|
|
68
|
+
- `Error`: Error object on failure
|
|
69
|
+
|
|
70
|
+
**Example:**
|
|
71
|
+
```python
|
|
72
|
+
result = AnzarAuth.login("user@example.com", "password123")
|
|
73
|
+
if isinstance(result, User):
|
|
74
|
+
print(f"Logged in: {result.username}")
|
|
75
|
+
else:
|
|
76
|
+
print(f"Login failed: {result.error}")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
##### `register(username, email, password)`
|
|
80
|
+
|
|
81
|
+
Register a new user account.
|
|
82
|
+
|
|
83
|
+
**Parameters:**
|
|
84
|
+
- `username` (str): Desired username
|
|
85
|
+
- `email` (str): User's email address
|
|
86
|
+
- `password` (str): User's password
|
|
87
|
+
|
|
88
|
+
**Returns:**
|
|
89
|
+
- `User`: User object on success
|
|
90
|
+
- `Error`: Error object on failure
|
|
91
|
+
|
|
92
|
+
**Example:**
|
|
93
|
+
```python
|
|
94
|
+
result = AnzarAuth.register("newuser", "user@example.com", "password123")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
##### `logout()`
|
|
98
|
+
|
|
99
|
+
Log out the current user.
|
|
100
|
+
|
|
101
|
+
**Returns:**
|
|
102
|
+
- `User`: Empty user object on success
|
|
103
|
+
- `Error`: Error object on failure
|
|
104
|
+
|
|
105
|
+
**Example:**
|
|
106
|
+
```python
|
|
107
|
+
result = AnzarAuth.logout()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
##### `isLoggedIn()`
|
|
111
|
+
|
|
112
|
+
Check if a user is currently logged in.
|
|
113
|
+
|
|
114
|
+
**Returns:**
|
|
115
|
+
- `User`: Current user object if logged in
|
|
116
|
+
- `Error`: Error object if not logged in
|
|
117
|
+
|
|
118
|
+
**Example:**
|
|
119
|
+
```python
|
|
120
|
+
result = AnzarAuth.isLoggedIn()
|
|
121
|
+
if isinstance(result, User):
|
|
122
|
+
print(f"Current user: {result.username}")
|
|
123
|
+
else:
|
|
124
|
+
print("No user logged in")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Error Handling
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from anzar.types import Error
|
|
131
|
+
try:
|
|
132
|
+
result = AnzarAuth.login(email, password)
|
|
133
|
+
except Error as e:
|
|
134
|
+
print(f"Error: {e}")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Examples
|
|
138
|
+
|
|
139
|
+
### Basic Authentication Flow
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from anzar import AnzarAuth
|
|
143
|
+
|
|
144
|
+
# Register new user
|
|
145
|
+
result = AnzarAuth.register("johndoe", "john@example.com", "securepass123")
|
|
146
|
+
if isinstance(result, Error):
|
|
147
|
+
print(f"Registration failed: {result.message}")
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
# Login
|
|
151
|
+
result = AnzarAuth.login("john@example.com", "securepass123")
|
|
152
|
+
if isinstance(result, Error):
|
|
153
|
+
print(f"Login failed: {result.message}")
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
print(f"Welcome {result.username}")
|
|
157
|
+
|
|
158
|
+
# Check login status
|
|
159
|
+
user = AnzarAuth.isLoggedIn()
|
|
160
|
+
if not isinstance(user, Error):
|
|
161
|
+
print(f"Current user: {user.username}")
|
|
162
|
+
|
|
163
|
+
# Logout
|
|
164
|
+
AnzarAuth.logout()
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Contributing
|
|
168
|
+
|
|
169
|
+
Instructions for contributing to the SDK.
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
License information.
|
anzar-0.1.0/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Anzar SDK Documentation
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install anzar
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from anzar import AnzarAuth
|
|
13
|
+
|
|
14
|
+
# Initialize the SDK
|
|
15
|
+
auth = AnzarAuth
|
|
16
|
+
|
|
17
|
+
# Use the authenticated client
|
|
18
|
+
# (Add specific usage examples here)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
### Environment Variables
|
|
24
|
+
|
|
25
|
+
Create a `.env` file in your project root:
|
|
26
|
+
|
|
27
|
+
```env
|
|
28
|
+
# Add your environment variables here
|
|
29
|
+
ANZAR_API_KEY=your_api_key
|
|
30
|
+
ANZAR_BASE_URL=https://api.anzar.com
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## API Reference
|
|
34
|
+
|
|
35
|
+
### AnzarAuth
|
|
36
|
+
|
|
37
|
+
Authentication manager for user login, registration, and session management.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from anzar import AnzarAuth
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
#### Methods
|
|
44
|
+
|
|
45
|
+
##### `login(email, password)`
|
|
46
|
+
|
|
47
|
+
Authenticate a user with credentials.
|
|
48
|
+
|
|
49
|
+
**Parameters:**
|
|
50
|
+
- `email` (str): User's email
|
|
51
|
+
- `password` (str): User's password
|
|
52
|
+
|
|
53
|
+
**Returns:**
|
|
54
|
+
- `User`: User object on success
|
|
55
|
+
- `Error`: Error object on failure
|
|
56
|
+
|
|
57
|
+
**Example:**
|
|
58
|
+
```python
|
|
59
|
+
result = AnzarAuth.login("user@example.com", "password123")
|
|
60
|
+
if isinstance(result, User):
|
|
61
|
+
print(f"Logged in: {result.username}")
|
|
62
|
+
else:
|
|
63
|
+
print(f"Login failed: {result.error}")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
##### `register(username, email, password)`
|
|
67
|
+
|
|
68
|
+
Register a new user account.
|
|
69
|
+
|
|
70
|
+
**Parameters:**
|
|
71
|
+
- `username` (str): Desired username
|
|
72
|
+
- `email` (str): User's email address
|
|
73
|
+
- `password` (str): User's password
|
|
74
|
+
|
|
75
|
+
**Returns:**
|
|
76
|
+
- `User`: User object on success
|
|
77
|
+
- `Error`: Error object on failure
|
|
78
|
+
|
|
79
|
+
**Example:**
|
|
80
|
+
```python
|
|
81
|
+
result = AnzarAuth.register("newuser", "user@example.com", "password123")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
##### `logout()`
|
|
85
|
+
|
|
86
|
+
Log out the current user.
|
|
87
|
+
|
|
88
|
+
**Returns:**
|
|
89
|
+
- `User`: Empty user object on success
|
|
90
|
+
- `Error`: Error object on failure
|
|
91
|
+
|
|
92
|
+
**Example:**
|
|
93
|
+
```python
|
|
94
|
+
result = AnzarAuth.logout()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
##### `isLoggedIn()`
|
|
98
|
+
|
|
99
|
+
Check if a user is currently logged in.
|
|
100
|
+
|
|
101
|
+
**Returns:**
|
|
102
|
+
- `User`: Current user object if logged in
|
|
103
|
+
- `Error`: Error object if not logged in
|
|
104
|
+
|
|
105
|
+
**Example:**
|
|
106
|
+
```python
|
|
107
|
+
result = AnzarAuth.isLoggedIn()
|
|
108
|
+
if isinstance(result, User):
|
|
109
|
+
print(f"Current user: {result.username}")
|
|
110
|
+
else:
|
|
111
|
+
print("No user logged in")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Error Handling
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from anzar.types import Error
|
|
118
|
+
try:
|
|
119
|
+
result = AnzarAuth.login(email, password)
|
|
120
|
+
except Error as e:
|
|
121
|
+
print(f"Error: {e}")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Examples
|
|
125
|
+
|
|
126
|
+
### Basic Authentication Flow
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from anzar import AnzarAuth
|
|
130
|
+
|
|
131
|
+
# Register new user
|
|
132
|
+
result = AnzarAuth.register("johndoe", "john@example.com", "securepass123")
|
|
133
|
+
if isinstance(result, Error):
|
|
134
|
+
print(f"Registration failed: {result.message}")
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
# Login
|
|
138
|
+
result = AnzarAuth.login("john@example.com", "securepass123")
|
|
139
|
+
if isinstance(result, Error):
|
|
140
|
+
print(f"Login failed: {result.message}")
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
print(f"Welcome {result.username}")
|
|
144
|
+
|
|
145
|
+
# Check login status
|
|
146
|
+
user = AnzarAuth.isLoggedIn()
|
|
147
|
+
if not isinstance(user, Error):
|
|
148
|
+
print(f"Current user: {user.username}")
|
|
149
|
+
|
|
150
|
+
# Logout
|
|
151
|
+
AnzarAuth.logout()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Contributing
|
|
155
|
+
|
|
156
|
+
Instructions for contributing to the SDK.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
License information.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "anzar"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Hakou Guelfen", email = "hakoudev@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"dotenv>=0.9.9",
|
|
12
|
+
"keyring>=25.6.0",
|
|
13
|
+
"pydantic[email]>=2.11.7",
|
|
14
|
+
"requests>=2.32.5",
|
|
15
|
+
]
|
|
16
|
+
# packages=["anzar", "anzar.types"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["uv_build>=0.8.4,<0.9.0"]
|
|
21
|
+
build-backend = "uv_build"
|
|
22
|
+
|
|
23
|
+
[dependency-groups]
|
|
24
|
+
dev = []
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
[tool.uv]
|
|
28
|
+
dev-dependencies = [
|
|
29
|
+
"pytest",
|
|
30
|
+
]
|
|
31
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import dotenv
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from ._api.client import HttpClient
|
|
5
|
+
from ._api.http_interceptor import HttpInterceptor
|
|
6
|
+
|
|
7
|
+
_ = dotenv.load_dotenv()
|
|
8
|
+
|
|
9
|
+
logging.basicConfig(
|
|
10
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_anzar_auth():
|
|
15
|
+
from ._auth.authenticator import AuthManager
|
|
16
|
+
|
|
17
|
+
return AuthManager(HttpClient(HttpInterceptor()))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
AnzarAuth = get_anzar_auth()
|
|
21
|
+
__all__ = ["AnzarAuth"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from requests.models import Response
|
|
2
|
+
|
|
3
|
+
from anzar._models.auth import LoginRequest, RegisterRequest
|
|
4
|
+
from anzar._api.http_interceptor import HttpInterceptor
|
|
5
|
+
from anzar._utils.errors import Error
|
|
6
|
+
from anzar._utils.types import T
|
|
7
|
+
from anzar._utils.validator import Validator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HttpClient:
|
|
11
|
+
def __init__(self, http_interceptor: HttpInterceptor):
|
|
12
|
+
self.http_interceptor: HttpInterceptor = http_interceptor
|
|
13
|
+
self.accessToken: str | None = None
|
|
14
|
+
|
|
15
|
+
def get(self, url: str, model_type: type[T]) -> T | Error:
|
|
16
|
+
response: Response = self.http_interceptor.get(url)
|
|
17
|
+
|
|
18
|
+
if response.status_code in (200, 201):
|
|
19
|
+
return model_type.model_validate(response.json())
|
|
20
|
+
else:
|
|
21
|
+
return Error.model_validate(response.json())
|
|
22
|
+
|
|
23
|
+
def post(
|
|
24
|
+
self,
|
|
25
|
+
url: str,
|
|
26
|
+
data: LoginRequest | RegisterRequest | None,
|
|
27
|
+
model_type: type[T],
|
|
28
|
+
) -> T | Error:
|
|
29
|
+
if data:
|
|
30
|
+
response: Response = self.http_interceptor.post(url, json=data.__dict__)
|
|
31
|
+
else:
|
|
32
|
+
response = self.http_interceptor.post(url)
|
|
33
|
+
|
|
34
|
+
if response.status_code in (200, 201):
|
|
35
|
+
return Validator().validate(model_type, response)
|
|
36
|
+
else:
|
|
37
|
+
return Error.model_validate(response.json())
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any, override
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from requests.adapters import HTTPAdapter
|
|
7
|
+
from urllib3.util import Retry
|
|
8
|
+
|
|
9
|
+
from anzar._models.auth import AuthResponse, JWTTokens
|
|
10
|
+
from anzar._utils.config import Config
|
|
11
|
+
from anzar._utils.storage import TokenStorage
|
|
12
|
+
from anzar._utils.types import Token, TokenType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HttpInterceptor(requests.Session):
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
super().__init__()
|
|
18
|
+
self.API_URL: str = os.getenv("API_URL", "http://localhost:3000")
|
|
19
|
+
self.storage: TokenStorage = TokenStorage()
|
|
20
|
+
self.banned_endpoints: list[str] = ["auth/login", "auth/register"]
|
|
21
|
+
self.refresh_endpoints: list[str] = ["auth/logout", "auth/refreshToken"]
|
|
22
|
+
|
|
23
|
+
# Retry logic (optional)
|
|
24
|
+
retries = Retry(
|
|
25
|
+
total=3,
|
|
26
|
+
backoff_factor=1,
|
|
27
|
+
status_forcelist=[500, 502, 503, 504],
|
|
28
|
+
allowed_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
29
|
+
)
|
|
30
|
+
adapter = HTTPAdapter(max_retries=retries)
|
|
31
|
+
self.mount("http://", adapter)
|
|
32
|
+
self.mount("https://", adapter)
|
|
33
|
+
|
|
34
|
+
def __extractTokenFromCache(self, tokenType: TokenType) -> Token | None:
|
|
35
|
+
token = self.storage.load(tokenType.name)
|
|
36
|
+
return Token.new(token, tokenType) if token else None
|
|
37
|
+
|
|
38
|
+
def __isUrlPartOf(self, url: str | bytes, endpoints: list[str]) -> bool:
|
|
39
|
+
for endpoint in endpoints:
|
|
40
|
+
if url == f"{self.API_URL}/{endpoint}":
|
|
41
|
+
return True
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
def __get_appropriate_token(self, url: str | bytes) -> Token | None:
|
|
45
|
+
if self.__isUrlPartOf(url, self.banned_endpoints):
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
if self.__isUrlPartOf(url, self.refresh_endpoints):
|
|
49
|
+
return self.__extractTokenFromCache(TokenType.RefreshToken)
|
|
50
|
+
|
|
51
|
+
return self.__extractTokenFromCache(TokenType.AccessToken)
|
|
52
|
+
|
|
53
|
+
def __handle_auth_response(
|
|
54
|
+
self, url: str | bytes, response: requests.Response
|
|
55
|
+
) -> None:
|
|
56
|
+
if not response.ok:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
if url == f"{self.API_URL}/auth/refreshToken":
|
|
60
|
+
jwtTokens = JWTTokens.model_validate(response.json())
|
|
61
|
+
elif self.__isUrlPartOf(url, self.banned_endpoints):
|
|
62
|
+
auth_response = AuthResponse.model_validate(response.json())
|
|
63
|
+
jwtTokens = JWTTokens(
|
|
64
|
+
accessToken=auth_response.accessToken,
|
|
65
|
+
refreshToken=auth_response.refreshToken,
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
self.storage.save(jwtTokens)
|
|
71
|
+
|
|
72
|
+
def __send_request(
|
|
73
|
+
self, method: str | bytes, url: str | bytes, *args, **kwargs
|
|
74
|
+
) -> requests.Response:
|
|
75
|
+
token = self.__get_appropriate_token(url)
|
|
76
|
+
kwargs["headers"] = Config().headers(token=token)
|
|
77
|
+
return super().request(method, url, timeout=30, *args, **kwargs)
|
|
78
|
+
|
|
79
|
+
@override
|
|
80
|
+
def request(
|
|
81
|
+
self,
|
|
82
|
+
method: str | bytes,
|
|
83
|
+
url: str | bytes,
|
|
84
|
+
_refresh_attempted: bool = False,
|
|
85
|
+
*args: Any,
|
|
86
|
+
**kwargs: Any,
|
|
87
|
+
) -> requests.Response:
|
|
88
|
+
# PRE-REQUEST INTERCEPTION
|
|
89
|
+
logging.info(f"{method} {url}")
|
|
90
|
+
response = self.__send_request(method, url, *args, **kwargs)
|
|
91
|
+
|
|
92
|
+
# POST-RESPONSE INTERCEPTION
|
|
93
|
+
logging.info(f"Response: {response.status_code}")
|
|
94
|
+
if (
|
|
95
|
+
response.status_code == 401
|
|
96
|
+
and not self.__isUrlPartOf(url, self.banned_endpoints)
|
|
97
|
+
and not self.__isUrlPartOf(url, self.refresh_endpoints)
|
|
98
|
+
and not _refresh_attempted
|
|
99
|
+
):
|
|
100
|
+
# header: Content-Type: application/x-www-form-urlencoded
|
|
101
|
+
response = self.request(
|
|
102
|
+
method="POST",
|
|
103
|
+
url=f"{self.API_URL}/auth/refreshToken",
|
|
104
|
+
_refresh_attempted=True,
|
|
105
|
+
*args,
|
|
106
|
+
**kwargs,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
response = self.__send_request(method, url, *args, **kwargs)
|
|
110
|
+
|
|
111
|
+
self.__handle_auth_response(url, response)
|
|
112
|
+
|
|
113
|
+
return response
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from anzar._api.client import HttpClient
|
|
4
|
+
|
|
5
|
+
from anzar._models.auth import AuthResponse
|
|
6
|
+
from anzar._models.user import User
|
|
7
|
+
from anzar._utils.errors import Error
|
|
8
|
+
from anzar._utils.storage import TokenStorage
|
|
9
|
+
from anzar._utils.types import NoType
|
|
10
|
+
from anzar._utils.validator import Validator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthManager:
|
|
14
|
+
def __init__(self, httpClient: HttpClient) -> None:
|
|
15
|
+
self._http_client: HttpClient = httpClient
|
|
16
|
+
|
|
17
|
+
self._API_URL: str = os.getenv("API_URL", "http://localhost:3000")
|
|
18
|
+
assert self._API_URL is not None, "Env was unable to load"
|
|
19
|
+
self.__endpoints: dict[str, str] = {
|
|
20
|
+
"login": f"{self._API_URL}/auth/login",
|
|
21
|
+
"register": f"{self._API_URL}/auth/register",
|
|
22
|
+
"user": f"{self._API_URL}/user",
|
|
23
|
+
"logout": f"{self._API_URL}/auth/logout",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def register(self, username: str, email: str, password: str) -> Error | User:
|
|
27
|
+
req = Validator().construct_register(username, email, password)
|
|
28
|
+
if isinstance(req, Error):
|
|
29
|
+
return req
|
|
30
|
+
|
|
31
|
+
url = self.__endpoints["register"]
|
|
32
|
+
response = self._http_client.post(url, req, AuthResponse)
|
|
33
|
+
|
|
34
|
+
return response.user if isinstance(response, AuthResponse) else response
|
|
35
|
+
|
|
36
|
+
def login(self, email: str, password: str):
|
|
37
|
+
req = Validator().construct_login(email, password)
|
|
38
|
+
if isinstance(req, Error):
|
|
39
|
+
return req
|
|
40
|
+
|
|
41
|
+
url = self.__endpoints["login"]
|
|
42
|
+
response = self._http_client.post(url, req, AuthResponse)
|
|
43
|
+
|
|
44
|
+
return response.user if isinstance(response, AuthResponse) else response
|
|
45
|
+
|
|
46
|
+
def getUser(self):
|
|
47
|
+
url = self.__endpoints["user"]
|
|
48
|
+
response = self._http_client.get(url, User)
|
|
49
|
+
return response
|
|
50
|
+
|
|
51
|
+
def logout(self):
|
|
52
|
+
url = self.__endpoints["logout"]
|
|
53
|
+
response = self._http_client.post(url, None, NoType)
|
|
54
|
+
TokenStorage().clear()
|
|
55
|
+
|
|
56
|
+
return response
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# def main():
|
|
60
|
+
# from pprint import pprint
|
|
61
|
+
# from anzar._api.http_interceptor import HttpInterceptor
|
|
62
|
+
#
|
|
63
|
+
# authManager = AuthManager(HttpClient(HttpInterceptor()))
|
|
64
|
+
#
|
|
65
|
+
# # username, email, password = "Hakou", "hakouguelfen@gmail.com", "hakouguelfen"
|
|
66
|
+
# # user = authManager.register(username, email, password)
|
|
67
|
+
# # pprint(user)
|
|
68
|
+
#
|
|
69
|
+
# # email, password = "hakouguelfen@gmail.com", "hakouguelfen"
|
|
70
|
+
# # user = authManager.login(email, password)
|
|
71
|
+
# # pprint(user)
|
|
72
|
+
#
|
|
73
|
+
# user = authManager.getUser()
|
|
74
|
+
# pprint(user)
|
|
75
|
+
#
|
|
76
|
+
# response = authManager.logout()
|
|
77
|
+
# pprint(response)
|
|
78
|
+
#
|
|
79
|
+
#
|
|
80
|
+
# if __name__ == "__main__":
|
|
81
|
+
# main()
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from pydantic import BaseModel, EmailStr
|
|
2
|
+
from pydantic.dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from .user import User
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass()
|
|
8
|
+
class LoginRequest:
|
|
9
|
+
email: EmailStr
|
|
10
|
+
password: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass()
|
|
14
|
+
class RegisterRequest:
|
|
15
|
+
username: str
|
|
16
|
+
email: EmailStr
|
|
17
|
+
password: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AuthResponse(BaseModel):
|
|
21
|
+
accessToken: str
|
|
22
|
+
refreshToken: str
|
|
23
|
+
user: User
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class JWTTokens(BaseModel):
|
|
27
|
+
accessToken: str
|
|
28
|
+
refreshToken: str
|
|
29
|
+
refreshTokenJti: str | None = None
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pydantic import BaseModel, EmailStr
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Role(str, Enum):
|
|
7
|
+
Admin = "Admin"
|
|
8
|
+
User = "User"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class User(BaseModel):
|
|
12
|
+
# id: str | None = Field(None, alias="_id")
|
|
13
|
+
_id: str | None
|
|
14
|
+
username: str
|
|
15
|
+
email: EmailStr
|
|
16
|
+
role: Role
|
|
17
|
+
isPremium: bool
|
|
18
|
+
accountLocked: bool
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from anzar._utils.types import Header, Token, TokenType
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Config:
|
|
5
|
+
def headers(self, token: Token | None) -> Header:
|
|
6
|
+
default_headers = {
|
|
7
|
+
"Content-Type": "application/json",
|
|
8
|
+
"User-Agent": "Anzar-SDK/1.0",
|
|
9
|
+
}
|
|
10
|
+
if token is not None:
|
|
11
|
+
if token.tokenType == TokenType.AccessToken:
|
|
12
|
+
default_headers["authorization"] = f"Bearer {token.value}"
|
|
13
|
+
if token.tokenType == TokenType.RefreshToken:
|
|
14
|
+
default_headers["x-refresh-token"] = f"Bearer {token.value}"
|
|
15
|
+
|
|
16
|
+
return default_headers
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import keyring
|
|
2
|
+
|
|
3
|
+
from anzar._models.auth import JWTTokens
|
|
4
|
+
from anzar._utils.types import TokenType
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
SERVICE_NAME = "AnzarSDK"
|
|
8
|
+
|
|
9
|
+
ACCESS_TOKEN = TokenType.AccessToken.name
|
|
10
|
+
REFRESH_TOKEN = TokenType.RefreshToken.name
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TokenStorage:
|
|
14
|
+
def save(self, tokens: JWTTokens):
|
|
15
|
+
try:
|
|
16
|
+
keyring.set_password(SERVICE_NAME, ACCESS_TOKEN, tokens.accessToken)
|
|
17
|
+
keyring.set_password(SERVICE_NAME, REFRESH_TOKEN, tokens.refreshToken)
|
|
18
|
+
except Exception as e:
|
|
19
|
+
print(e)
|
|
20
|
+
|
|
21
|
+
def load(self, username: str):
|
|
22
|
+
try:
|
|
23
|
+
return keyring.get_password(SERVICE_NAME, username)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
print(e)
|
|
26
|
+
|
|
27
|
+
def clear(self):
|
|
28
|
+
keyring.set_password(SERVICE_NAME, ACCESS_TOKEN, "")
|
|
29
|
+
keyring.set_password(SERVICE_NAME, REFRESH_TOKEN, "")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum, auto
|
|
4
|
+
from typing import Generic, TypeVar, override
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
Header = Mapping[str, str | bytes | None]
|
|
9
|
+
T = TypeVar("T", bound=BaseModel)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NoType(BaseModel):
|
|
13
|
+
status: str | None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TokenType(Enum):
|
|
17
|
+
AccessToken = auto()
|
|
18
|
+
RefreshToken = auto()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass()
|
|
22
|
+
class Token:
|
|
23
|
+
value: str
|
|
24
|
+
tokenType: TokenType
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def new(cls, value: str, tokenType: TokenType):
|
|
28
|
+
return cls(value, tokenType)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
R = TypeVar("R")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Result(Generic[R]):
|
|
35
|
+
@property
|
|
36
|
+
def is_ok(self) -> bool: ...
|
|
37
|
+
@property
|
|
38
|
+
def is_err(self) -> bool: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Ok(Result[R]):
|
|
42
|
+
def __init__(self, value: R) -> None:
|
|
43
|
+
self.value: R = value
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
@override
|
|
47
|
+
def is_ok(self) -> bool:
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
@override
|
|
52
|
+
def is_err(self) -> bool:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Err(Result[R]):
|
|
57
|
+
def __init__(self, error: str) -> None:
|
|
58
|
+
self.error: str = error
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
@override
|
|
62
|
+
def is_ok(self) -> bool:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
@override
|
|
67
|
+
def is_err(self) -> bool:
|
|
68
|
+
return True
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from pydantic import ValidationError
|
|
2
|
+
from requests import Response
|
|
3
|
+
|
|
4
|
+
from anzar._utils.errors import Error
|
|
5
|
+
from anzar._utils.types import T
|
|
6
|
+
|
|
7
|
+
from anzar._models.auth import LoginRequest, RegisterRequest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Validator:
|
|
11
|
+
def construct_register(
|
|
12
|
+
self, username: str, email: str, password: str
|
|
13
|
+
) -> RegisterRequest | Error:
|
|
14
|
+
try:
|
|
15
|
+
return RegisterRequest(username, email, password)
|
|
16
|
+
|
|
17
|
+
except ValidationError as e:
|
|
18
|
+
ctx = e.errors()[0].get("ctx")
|
|
19
|
+
|
|
20
|
+
reason: str | None = ctx.get("reason") if ctx else None
|
|
21
|
+
return Error(error=reason or "Data is not validated")
|
|
22
|
+
|
|
23
|
+
def construct_login(self, email: str, password: str) -> LoginRequest | Error:
|
|
24
|
+
try:
|
|
25
|
+
return LoginRequest(email, password)
|
|
26
|
+
except ValidationError as e:
|
|
27
|
+
ctx = e.errors()[0].get("ctx")
|
|
28
|
+
|
|
29
|
+
reason: str | None = ctx.get("reason") if ctx else None
|
|
30
|
+
return Error(error=reason or "Data is not validated")
|
|
31
|
+
|
|
32
|
+
def validate(self, model_type: type[T], res: Response) -> T | Error:
|
|
33
|
+
try:
|
|
34
|
+
return model_type.model_validate(res.json())
|
|
35
|
+
except ValidationError as _:
|
|
36
|
+
return Error(error="")
|
|
File without changes
|