neutronapi 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.
- neutronapi-0.1.0/LICENSE +21 -0
- neutronapi-0.1.0/MANIFEST.in +7 -0
- neutronapi-0.1.0/PKG-INFO +202 -0
- neutronapi-0.1.0/README.md +170 -0
- neutronapi-0.1.0/neutronapi/__init__.py +21 -0
- neutronapi-0.1.0/neutronapi/application.py +113 -0
- neutronapi-0.1.0/neutronapi/authentication/__init__.py +9 -0
- neutronapi-0.1.0/neutronapi/authentication/base.py +34 -0
- neutronapi-0.1.0/neutronapi/background.py +400 -0
- neutronapi-0.1.0/neutronapi/base.py +914 -0
- neutronapi-0.1.0/neutronapi/cli.py +135 -0
- neutronapi-0.1.0/neutronapi/commands/__init__.py +2 -0
- neutronapi-0.1.0/neutronapi/commands/migrate.py +89 -0
- neutronapi-0.1.0/neutronapi/commands/shell.py +105 -0
- neutronapi-0.1.0/neutronapi/commands/start.py +219 -0
- neutronapi-0.1.0/neutronapi/commands/startapp.py +50 -0
- neutronapi-0.1.0/neutronapi/commands/startproject.py +87 -0
- neutronapi-0.1.0/neutronapi/commands/test.py +336 -0
- neutronapi-0.1.0/neutronapi/db/__init__.py +33 -0
- neutronapi-0.1.0/neutronapi/db/connection.py +138 -0
- neutronapi-0.1.0/neutronapi/db/fields.py +915 -0
- neutronapi-0.1.0/neutronapi/db/migrations.py +1286 -0
- neutronapi-0.1.0/neutronapi/db/models.py +147 -0
- neutronapi-0.1.0/neutronapi/db/providers/__init__.py +26 -0
- neutronapi-0.1.0/neutronapi/db/providers/base.py +44 -0
- neutronapi-0.1.0/neutronapi/db/providers/postgres.py +343 -0
- neutronapi-0.1.0/neutronapi/db/providers/sqlite.py +454 -0
- neutronapi-0.1.0/neutronapi/db/queryset.py +925 -0
- neutronapi-0.1.0/neutronapi/encoders.py +14 -0
- neutronapi-0.1.0/neutronapi/exceptions.py +21 -0
- neutronapi-0.1.0/neutronapi/middleware/__init__.py +0 -0
- neutronapi-0.1.0/neutronapi/middleware/allowed_hosts.py +116 -0
- neutronapi-0.1.0/neutronapi/middleware/cors.py +108 -0
- neutronapi-0.1.0/neutronapi/middleware/routing.py +211 -0
- neutronapi-0.1.0/neutronapi/multipart.py +377 -0
- neutronapi-0.1.0/neutronapi/openapi/__init__.py +0 -0
- neutronapi-0.1.0/neutronapi/openapi/openapi.py +805 -0
- neutronapi-0.1.0/neutronapi/openapi/swagger.py +457 -0
- neutronapi-0.1.0/neutronapi.egg-info/PKG-INFO +202 -0
- neutronapi-0.1.0/neutronapi.egg-info/SOURCES.txt +44 -0
- neutronapi-0.1.0/neutronapi.egg-info/dependency_links.txt +1 -0
- neutronapi-0.1.0/neutronapi.egg-info/entry_points.txt +2 -0
- neutronapi-0.1.0/neutronapi.egg-info/requires.txt +11 -0
- neutronapi-0.1.0/neutronapi.egg-info/top_level.txt +1 -0
- neutronapi-0.1.0/pyproject.toml +48 -0
- neutronapi-0.1.0/setup.cfg +4 -0
neutronapi-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Aaron Kazah
|
|
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.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: neutronapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: High-performance Python framework built directly on uvicorn with built-in database models, migrations, and background tasks. Django that was built async-first.
|
|
5
|
+
Author-email: Aaron Kazah <aaron@neutronapi.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: api,framework,async,django,fastapi,uvicorn,orm,migrations,background-tasks
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Framework :: AsyncIO
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: uvicorn[standard]>=0.23.0
|
|
23
|
+
Requires-Dist: aiosqlite>=0.19.0
|
|
24
|
+
Requires-Dist: PyJWT>=2.8.0
|
|
25
|
+
Provides-Extra: postgres
|
|
26
|
+
Requires-Dist: asyncpg>=0.29.0; extra == "postgres"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: httpx>=0.24.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# NeutronAPI
|
|
34
|
+
|
|
35
|
+
**High-performance Python framework built directly on uvicorn with built-in database models, migrations, and background tasks. If you want Django that was built async-first, this is for you.**
|
|
36
|
+
|
|
37
|
+
Batteries included async API framework with command-line management.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install neutronapi
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# 1. Create project
|
|
49
|
+
neutronapi startproject blog
|
|
50
|
+
cd blog
|
|
51
|
+
|
|
52
|
+
# 2. Create an app
|
|
53
|
+
python manage.py startapp posts
|
|
54
|
+
|
|
55
|
+
# 3. Start server
|
|
56
|
+
python manage.py start # Dev mode (auto-reload)
|
|
57
|
+
|
|
58
|
+
# 4. Test
|
|
59
|
+
python manage.py test
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Getting Started Tutorial
|
|
63
|
+
|
|
64
|
+
**1. Create Project**
|
|
65
|
+
```bash
|
|
66
|
+
neutronapi startproject blog
|
|
67
|
+
cd blog
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**2. Create App Module**
|
|
71
|
+
```bash
|
|
72
|
+
python manage.py startapp posts
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**3. Configure in `apps/settings.py`**
|
|
76
|
+
```python
|
|
77
|
+
import os
|
|
78
|
+
|
|
79
|
+
# ASGI application entry point (required for server)
|
|
80
|
+
ENTRY = "apps.entry:app" # module:variable format
|
|
81
|
+
|
|
82
|
+
# Database
|
|
83
|
+
DATABASES = {
|
|
84
|
+
'default': {
|
|
85
|
+
'ENGINE': 'aiosqlite',
|
|
86
|
+
'NAME': 'db.sqlite3',
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**4. Create API in `apps/posts/api.py`**
|
|
92
|
+
```python
|
|
93
|
+
from neutronapi.base import API
|
|
94
|
+
|
|
95
|
+
class PostAPI(API):
|
|
96
|
+
name = "posts"
|
|
97
|
+
|
|
98
|
+
@API.endpoint("/posts", methods=["GET"])
|
|
99
|
+
async def list_posts(self, scope, receive, send, **kwargs):
|
|
100
|
+
posts = [{"id": 1, "title": "Hello World"}]
|
|
101
|
+
return await self.response(posts)
|
|
102
|
+
|
|
103
|
+
@API.endpoint("/posts", methods=["POST"])
|
|
104
|
+
async def create_post(self, scope, receive, send, **kwargs):
|
|
105
|
+
# Get request data from scope["body"]
|
|
106
|
+
return await self.response({"id": 2, "title": "New Post"})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**5. Register API in `apps/entry.py`**
|
|
110
|
+
```python
|
|
111
|
+
from neutronapi.application import Application
|
|
112
|
+
from apps.posts.api import PostAPI
|
|
113
|
+
|
|
114
|
+
app = Application({
|
|
115
|
+
"posts": PostAPI()
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**6. Start Server**
|
|
120
|
+
```bash
|
|
121
|
+
python manage.py start
|
|
122
|
+
# Visit: http://127.0.0.1:8000/posts
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Project Structure
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
myproject/
|
|
129
|
+
├── manage.py # Management commands
|
|
130
|
+
├── apps/
|
|
131
|
+
│ ├── __init__.py
|
|
132
|
+
│ ├── settings.py # Configuration
|
|
133
|
+
│ └── entry.py # ASGI application
|
|
134
|
+
└── db.sqlite3 # Database
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Background Tasks
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from neutronapi.background import Task, TaskFrequency
|
|
141
|
+
|
|
142
|
+
class CleanupTask(Task):
|
|
143
|
+
name = "cleanup"
|
|
144
|
+
frequency = TaskFrequency.MINUTELY
|
|
145
|
+
|
|
146
|
+
async def run(self, **kwargs):
|
|
147
|
+
print("Cleaning up logs...")
|
|
148
|
+
|
|
149
|
+
# Add to application
|
|
150
|
+
app = Application(
|
|
151
|
+
apis={"ping": PingAPI()},
|
|
152
|
+
tasks={"cleanup": CleanupTask()}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
## Database Models
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from neutronapi.db.models import Model
|
|
159
|
+
from neutronapi.db.fields import CharField, IntegerField, DateTimeField
|
|
160
|
+
|
|
161
|
+
class User(Model):
|
|
162
|
+
name = CharField(max_length=100)
|
|
163
|
+
age = IntegerField()
|
|
164
|
+
created_at = DateTimeField(auto_now_add=True)
|
|
165
|
+
|
|
166
|
+
# Table name inferred from class name (user -> users)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Server Commands
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Development (auto-reload, localhost)
|
|
173
|
+
python manage.py start
|
|
174
|
+
|
|
175
|
+
# Production (multi-worker, optimized)
|
|
176
|
+
python manage.py start --production
|
|
177
|
+
|
|
178
|
+
# Custom configuration
|
|
179
|
+
python manage.py start --host 0.0.0.0 --port 8080 --workers 4
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Testing
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# SQLite (default)
|
|
186
|
+
python manage.py test
|
|
187
|
+
|
|
188
|
+
# PostgreSQL (auto Docker bootstrap)
|
|
189
|
+
DATABASE_PROVIDER=asyncpg python manage.py test
|
|
190
|
+
|
|
191
|
+
# Specific tests
|
|
192
|
+
python manage.py test app.tests.test_models.TestUser.test_creation
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Commands
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
python manage.py start # Start server
|
|
199
|
+
python manage.py test # Run tests
|
|
200
|
+
python manage.py migrate # Run migrations
|
|
201
|
+
python manage.py startapp posts # Create new app
|
|
202
|
+
```
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# NeutronAPI
|
|
2
|
+
|
|
3
|
+
**High-performance Python framework built directly on uvicorn with built-in database models, migrations, and background tasks. If you want Django that was built async-first, this is for you.**
|
|
4
|
+
|
|
5
|
+
Batteries included async API framework with command-line management.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install neutronapi
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# 1. Create project
|
|
17
|
+
neutronapi startproject blog
|
|
18
|
+
cd blog
|
|
19
|
+
|
|
20
|
+
# 2. Create an app
|
|
21
|
+
python manage.py startapp posts
|
|
22
|
+
|
|
23
|
+
# 3. Start server
|
|
24
|
+
python manage.py start # Dev mode (auto-reload)
|
|
25
|
+
|
|
26
|
+
# 4. Test
|
|
27
|
+
python manage.py test
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Getting Started Tutorial
|
|
31
|
+
|
|
32
|
+
**1. Create Project**
|
|
33
|
+
```bash
|
|
34
|
+
neutronapi startproject blog
|
|
35
|
+
cd blog
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**2. Create App Module**
|
|
39
|
+
```bash
|
|
40
|
+
python manage.py startapp posts
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**3. Configure in `apps/settings.py`**
|
|
44
|
+
```python
|
|
45
|
+
import os
|
|
46
|
+
|
|
47
|
+
# ASGI application entry point (required for server)
|
|
48
|
+
ENTRY = "apps.entry:app" # module:variable format
|
|
49
|
+
|
|
50
|
+
# Database
|
|
51
|
+
DATABASES = {
|
|
52
|
+
'default': {
|
|
53
|
+
'ENGINE': 'aiosqlite',
|
|
54
|
+
'NAME': 'db.sqlite3',
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**4. Create API in `apps/posts/api.py`**
|
|
60
|
+
```python
|
|
61
|
+
from neutronapi.base import API
|
|
62
|
+
|
|
63
|
+
class PostAPI(API):
|
|
64
|
+
name = "posts"
|
|
65
|
+
|
|
66
|
+
@API.endpoint("/posts", methods=["GET"])
|
|
67
|
+
async def list_posts(self, scope, receive, send, **kwargs):
|
|
68
|
+
posts = [{"id": 1, "title": "Hello World"}]
|
|
69
|
+
return await self.response(posts)
|
|
70
|
+
|
|
71
|
+
@API.endpoint("/posts", methods=["POST"])
|
|
72
|
+
async def create_post(self, scope, receive, send, **kwargs):
|
|
73
|
+
# Get request data from scope["body"]
|
|
74
|
+
return await self.response({"id": 2, "title": "New Post"})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**5. Register API in `apps/entry.py`**
|
|
78
|
+
```python
|
|
79
|
+
from neutronapi.application import Application
|
|
80
|
+
from apps.posts.api import PostAPI
|
|
81
|
+
|
|
82
|
+
app = Application({
|
|
83
|
+
"posts": PostAPI()
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**6. Start Server**
|
|
88
|
+
```bash
|
|
89
|
+
python manage.py start
|
|
90
|
+
# Visit: http://127.0.0.1:8000/posts
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Project Structure
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
myproject/
|
|
97
|
+
├── manage.py # Management commands
|
|
98
|
+
├── apps/
|
|
99
|
+
│ ├── __init__.py
|
|
100
|
+
│ ├── settings.py # Configuration
|
|
101
|
+
│ └── entry.py # ASGI application
|
|
102
|
+
└── db.sqlite3 # Database
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Background Tasks
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from neutronapi.background import Task, TaskFrequency
|
|
109
|
+
|
|
110
|
+
class CleanupTask(Task):
|
|
111
|
+
name = "cleanup"
|
|
112
|
+
frequency = TaskFrequency.MINUTELY
|
|
113
|
+
|
|
114
|
+
async def run(self, **kwargs):
|
|
115
|
+
print("Cleaning up logs...")
|
|
116
|
+
|
|
117
|
+
# Add to application
|
|
118
|
+
app = Application(
|
|
119
|
+
apis={"ping": PingAPI()},
|
|
120
|
+
tasks={"cleanup": CleanupTask()}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
## Database Models
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from neutronapi.db.models import Model
|
|
127
|
+
from neutronapi.db.fields import CharField, IntegerField, DateTimeField
|
|
128
|
+
|
|
129
|
+
class User(Model):
|
|
130
|
+
name = CharField(max_length=100)
|
|
131
|
+
age = IntegerField()
|
|
132
|
+
created_at = DateTimeField(auto_now_add=True)
|
|
133
|
+
|
|
134
|
+
# Table name inferred from class name (user -> users)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Server Commands
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# Development (auto-reload, localhost)
|
|
141
|
+
python manage.py start
|
|
142
|
+
|
|
143
|
+
# Production (multi-worker, optimized)
|
|
144
|
+
python manage.py start --production
|
|
145
|
+
|
|
146
|
+
# Custom configuration
|
|
147
|
+
python manage.py start --host 0.0.0.0 --port 8080 --workers 4
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Testing
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# SQLite (default)
|
|
154
|
+
python manage.py test
|
|
155
|
+
|
|
156
|
+
# PostgreSQL (auto Docker bootstrap)
|
|
157
|
+
DATABASE_PROVIDER=asyncpg python manage.py test
|
|
158
|
+
|
|
159
|
+
# Specific tests
|
|
160
|
+
python manage.py test app.tests.test_models.TestUser.test_creation
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Commands
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
python manage.py start # Start server
|
|
167
|
+
python manage.py test # Run tests
|
|
168
|
+
python manage.py migrate # Run migrations
|
|
169
|
+
python manage.py startapp posts # Create new app
|
|
170
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""NeutronAPI - High-performance Python framework built directly on uvicorn.
|
|
2
|
+
|
|
3
|
+
If you want Django that was built async-first, this is for you.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
|
|
8
|
+
from .base import API, Response, Endpoint
|
|
9
|
+
from .application import Application
|
|
10
|
+
from .background import Background, Task, TaskFrequency, TaskPriority
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'API',
|
|
14
|
+
'Response',
|
|
15
|
+
'Endpoint',
|
|
16
|
+
'Application',
|
|
17
|
+
'Background',
|
|
18
|
+
'Task',
|
|
19
|
+
'TaskFrequency',
|
|
20
|
+
'TaskPriority',
|
|
21
|
+
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from typing import Dict, Optional, Callable, List, Any
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
from neutronapi.base import API
|
|
5
|
+
from neutronapi.middleware.cors import CORS
|
|
6
|
+
from neutronapi.middleware.routing import RoutingMiddleware
|
|
7
|
+
from neutronapi.middleware.allowed_hosts import AllowedHostsMiddleware
|
|
8
|
+
from neutronapi.background import Background, TaskFrequency, TaskPriority
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Application:
|
|
12
|
+
"""ASGI application that composes APIs + middleware + optional background tasks.
|
|
13
|
+
|
|
14
|
+
Prefer using this class directly. The create_application() helper remains for compatibility.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
apis: Dict[str, API],
|
|
20
|
+
*,
|
|
21
|
+
tasks: Optional[Dict[str, Any]] = None,
|
|
22
|
+
version: str = "1.0.0",
|
|
23
|
+
allowed_hosts: Optional[List[str]] = None,
|
|
24
|
+
static_hosts: Optional[List[str]] = None,
|
|
25
|
+
static_resolver: Optional[Callable] = None,
|
|
26
|
+
cors_allow_all: bool = True,
|
|
27
|
+
) -> None:
|
|
28
|
+
self.apis = apis
|
|
29
|
+
self.version = version
|
|
30
|
+
|
|
31
|
+
# Simple handler that routes to APIs
|
|
32
|
+
async def app(scope, receive, send):
|
|
33
|
+
if scope["type"] == "http":
|
|
34
|
+
path = scope.get("path", "/").lstrip("/")
|
|
35
|
+
|
|
36
|
+
# Check if path matches any API
|
|
37
|
+
if path in self.apis:
|
|
38
|
+
api = self.apis[path]
|
|
39
|
+
await api.handle(scope, receive, send)
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
# Default 404 for unmatched paths
|
|
43
|
+
await send({
|
|
44
|
+
"type": "http.response.start",
|
|
45
|
+
"status": 404,
|
|
46
|
+
"headers": [[b"content-type", b"text/plain"]],
|
|
47
|
+
})
|
|
48
|
+
await send({
|
|
49
|
+
"type": "http.response.body",
|
|
50
|
+
"body": b"Not Found",
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
# Skip hosts middleware if no allowed_hosts specified (for testing)
|
|
54
|
+
if allowed_hosts:
|
|
55
|
+
hosts_app = AllowedHostsMiddleware(app, allowed_hosts=allowed_hosts)
|
|
56
|
+
else:
|
|
57
|
+
hosts_app = app
|
|
58
|
+
cors_wrapped = CORS(hosts_app, allow_all_origins=cors_allow_all)
|
|
59
|
+
self.app = RoutingMiddleware(
|
|
60
|
+
default_app=cors_wrapped,
|
|
61
|
+
static_hosts=static_hosts,
|
|
62
|
+
static_resolver=static_resolver,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# lifecycle hooks
|
|
66
|
+
self.app.on_startup = []
|
|
67
|
+
self.app.on_shutdown = []
|
|
68
|
+
|
|
69
|
+
# Expose lifecycle hooks on Application instance for compatibility
|
|
70
|
+
self.on_startup = self.app.on_startup
|
|
71
|
+
self.on_shutdown = self.app.on_shutdown
|
|
72
|
+
|
|
73
|
+
# Handle tasks dict - clean API-like pattern
|
|
74
|
+
if tasks:
|
|
75
|
+
from neutronapi.background import Background
|
|
76
|
+
self.background = Background()
|
|
77
|
+
|
|
78
|
+
# Register all tasks
|
|
79
|
+
for name, task in tasks.items():
|
|
80
|
+
self.background.register_task(task)
|
|
81
|
+
|
|
82
|
+
async def _start_background():
|
|
83
|
+
await self.background.start()
|
|
84
|
+
|
|
85
|
+
async def _stop_background():
|
|
86
|
+
await self.background.stop()
|
|
87
|
+
|
|
88
|
+
self.app.on_startup.append(_start_background)
|
|
89
|
+
self.app.on_shutdown.append(_stop_background)
|
|
90
|
+
|
|
91
|
+
async def __call__(self, scope, receive, send, **kwargs):
|
|
92
|
+
return await self.app(scope, receive, send, **kwargs)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def create_application(
|
|
96
|
+
apis: Dict[str, API],
|
|
97
|
+
static_hosts: Optional[List[str]] = None,
|
|
98
|
+
static_resolver: Optional[Callable] = None,
|
|
99
|
+
allowed_hosts: Optional[List[str]] = None,
|
|
100
|
+
version: str = "1.0.0",
|
|
101
|
+
expose_docs: bool = False, # kept for compatibility; no-op
|
|
102
|
+
):
|
|
103
|
+
"""Compatibility wrapper that returns an Application instance.
|
|
104
|
+
|
|
105
|
+
Docs are not injected automatically; pass your own docs API if desired.
|
|
106
|
+
"""
|
|
107
|
+
return Application(
|
|
108
|
+
apis,
|
|
109
|
+
version=version,
|
|
110
|
+
allowed_hosts=allowed_hosts,
|
|
111
|
+
static_hosts=static_hosts,
|
|
112
|
+
static_resolver=static_resolver,
|
|
113
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import bcrypt
|
|
2
|
+
import asyncio
|
|
3
|
+
from typing import Any, List, Optional
|
|
4
|
+
import abc
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Authentication(abc.ABC):
|
|
8
|
+
@classmethod
|
|
9
|
+
@abc.abstractmethod
|
|
10
|
+
async def authenticate(cls, email: str, password: str) -> Optional[Any]:
|
|
11
|
+
raise NotImplementedError("Subclasses must implement authenticate")
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
@abc.abstractmethod
|
|
15
|
+
async def authorize(cls, scope: List[str]) -> bool:
|
|
16
|
+
raise NotImplementedError("Subclasses must implement authorize")
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
async def hash_password(password: str) -> str:
|
|
20
|
+
def _hash():
|
|
21
|
+
salt = bcrypt.gensalt()
|
|
22
|
+
hashed = bcrypt.hashpw(password.encode("utf-8"), salt)
|
|
23
|
+
return hashed.decode("utf-8")
|
|
24
|
+
|
|
25
|
+
return await asyncio.to_thread(_hash)
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
async def check_password(hashed_password: str, plain_password: str) -> bool:
|
|
29
|
+
def _check():
|
|
30
|
+
return bcrypt.checkpw(
|
|
31
|
+
plain_password.encode("utf-8"), hashed_password.encode("utf-8")
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return await asyncio.to_thread(_check)
|