neuronos-capabilities-lib 0.0.2__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.
- neuronos_capabilities_lib-0.0.2/LICENSE +21 -0
- neuronos_capabilities_lib-0.0.2/PKG-INFO +152 -0
- neuronos_capabilities_lib-0.0.2/README.md +121 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/__init__.py +42 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/adapters/__init__.py +3 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/adapters/clerk_adapter.py +87 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/auth_manager.py +237 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/config/__init__.py +3 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/config/auth_config.py +106 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/examples/usage.py +56 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/exceptions/__init__.py +13 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/exceptions/auth_exceptions.py +23 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/ports/__init__.py +3 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/ports/auth_port.py +92 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/tests/__init__.py +1 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/tests/test_clock_skew.py +178 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/README.md +75 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/__init__.py +37 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/adapters/__init__.py +7 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/adapters/aws.py +146 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/adapters/console.py +30 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/adapters/memory.py +143 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/adapters/postgres.py +214 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/adapters/templates.py +73 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/artifact.yaml +64 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/config/__init__.py +3 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/config/notifications_config.py +123 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/entrypoints/__init__.py +1 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/entrypoints/events.py +36 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/entrypoints/sender.py +22 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/exceptions/__init__.py +19 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/exceptions/notifications_exceptions.py +41 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/models.py +88 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/notifications_manager.py +605 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/ports/__init__.py +110 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/tests/__init__.py +0 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/tests/test_aws_adapters.py +93 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/tests/test_notifications_manager.py +198 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/__init__.py +55 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/adapters/__init__.py +3 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/adapters/aws_adapter.py +400 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/artifact.yaml +52 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/config/__init__.py +3 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/config/s3_config.py +91 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/examples/usage.py +86 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/exceptions/__init__.py +21 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/exceptions/s3_exceptions.py +43 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/ports/__init__.py +3 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/ports/s3_port.py +499 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/s3_manager.py +332 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/tests/__init__.py +1 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/__init__.py +53 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/adapters/__init__.py +0 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/adapters/postgres_adapter.py +893 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/artifact.yaml +52 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/config/__init__.py +0 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/config/sql_config.py +175 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/examples/testing_library_execution.py +167 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/exceptions/__init__.py +0 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/exceptions/sql_exceptions.py +117 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/migrations/__init__.py +0 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/migrations/alembic_manager.py +617 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/models/__init__.py +9 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/models/base.py +118 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/ports/__init__.py +0 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/ports/sql_port.py +940 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/sql_manager.py +495 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/tests/__init__.py +0 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/utils/__init__.py +0 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/utils/query_builder.py +238 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/utils/singleton.py +120 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/__init__.py +58 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities/__version__.py +2 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities_lib.egg-info/PKG-INFO +152 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities_lib.egg-info/SOURCES.txt +79 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities_lib.egg-info/dependency_links.txt +1 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities_lib.egg-info/requires.txt +17 -0
- neuronos_capabilities_lib-0.0.2/neuronos_capabilities_lib.egg-info/top_level.txt +1 -0
- neuronos_capabilities_lib-0.0.2/pyproject.toml +57 -0
- neuronos_capabilities_lib-0.0.2/setup.cfg +4 -0
- neuronos_capabilities_lib-0.0.2/tests/test_package_import.py +88 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NeuronOS
|
|
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,152 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: neuronos-capabilities-lib
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: NeuronOS Python Capabilities Library - Provider-agnostic backend capabilities following hexagonal architecture
|
|
5
|
+
Author-email: NeuronOS <tech@neuron-studios.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/neuronos-platform/backend_capabilities_python
|
|
7
|
+
Project-URL: Repository, https://github.com/neuronos-platform/backend_capabilities_python
|
|
8
|
+
Keywords: neuronos,capabilities,hexagonal-architecture,sqlmanager,s3manager,notifications
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: sqlalchemy<3.0.0,>=2.0.23
|
|
17
|
+
Requires-Dist: psycopg2-binary<3.0.0,>=2.9.9
|
|
18
|
+
Requires-Dist: asyncpg<1.0.0,>=0.29.0
|
|
19
|
+
Requires-Dist: alembic<2.0.0,>=1.13.1
|
|
20
|
+
Requires-Dist: typing-extensions>=4.9.0
|
|
21
|
+
Requires-Dist: boto3<2.0.0,>=1.34.0
|
|
22
|
+
Requires-Dist: botocore<2.0.0,>=1.34.0
|
|
23
|
+
Provides-Extra: notifications
|
|
24
|
+
Requires-Dist: jinja2>=3.1; extra == "notifications"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.4.3; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.21.1; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
29
|
+
Requires-Dist: twine>=5.1.1; extra == "dev"
|
|
30
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
31
|
+
|
|
32
|
+
# NeuronOS Capabilities Library
|
|
33
|
+
|
|
34
|
+
A collection of provider-agnostic backend capabilities for Python applications, following hexagonal architecture principles.
|
|
35
|
+
|
|
36
|
+
Each capability exposes well-defined ports and adapters, making implementations swappable without changing your application code.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install neuronos-capabilities-lib
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The import name is `neuronos_capabilities` (`from neuronos_capabilities import ...`).
|
|
45
|
+
|
|
46
|
+
> **Moved from `neuronos-capabilities`.** Versions up to 0.0.1.5 were published as
|
|
47
|
+
> `neuronos-capabilities`; from 0.0.2 on the package is `neuronos-capabilities-lib`. The code and
|
|
48
|
+
> imports are the same. Before switching a local environment, run
|
|
49
|
+
> `pip uninstall neuronos-capabilities` (both names install the same `neuronos_capabilities/` folder).
|
|
50
|
+
|
|
51
|
+
## What is a Capability?
|
|
52
|
+
|
|
53
|
+
A capability is a self-contained backend module that follows hexagonal architecture (ports and adapters pattern):
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Capability/
|
|
57
|
+
├── ports/ # Abstract interfaces — defines WHAT operations exist
|
|
58
|
+
├── adapters/ # Provider implementations — defines HOW they work
|
|
59
|
+
├── config/ # Configuration management
|
|
60
|
+
├── exceptions/ # Custom exception hierarchy
|
|
61
|
+
└── [name]_manager.py # Public API — single entrypoint for consumers
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
You interact only with the Manager. The port defines the contract, and adapters implement it for specific providers. Switching from PostgreSQL to MySQL, or from Clerk to Auth0, is just a config change.
|
|
65
|
+
|
|
66
|
+
## Available Capabilities
|
|
67
|
+
|
|
68
|
+
| Capability | Description | Providers | Docs |
|
|
69
|
+
|------------|-------------|-----------|------|
|
|
70
|
+
| **SQLManager** | Database management with ORM, migrations, connection pooling | PostgreSQL | [docs/sql-manager.md](./docs/sql-manager.md) |
|
|
71
|
+
| **AuthManager** | JWT authentication and role-based authorization for FastAPI | Clerk | [docs/auth-manager.md](./docs/auth-manager.md) |
|
|
72
|
+
| **S3Manager** | Object storage with CRUD, listing, multipart uploads, presigned URLs | AWS S3 | [docs/s3-manager.md](./docs/s3-manager.md) |
|
|
73
|
+
|
|
74
|
+
### SQLManager — Quick Look
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from neuronos_capabilities import SQLManager
|
|
78
|
+
|
|
79
|
+
config = {"host": "localhost", "database": "myapp", "user": "postgres", "password": "secret"}
|
|
80
|
+
|
|
81
|
+
with SQLManager(config) as db:
|
|
82
|
+
db.create_tables([User])
|
|
83
|
+
user = db.insert(User, {"name": "Alice", "email": "alice@example.com"})
|
|
84
|
+
users = db.query(User, {"name": "Alice"})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### AuthManager — Quick Look
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from neuronos_capabilities import AuthManager
|
|
91
|
+
from fastapi import FastAPI, Depends
|
|
92
|
+
|
|
93
|
+
auth = AuthManager() # reads NEURONOS_AUTH_JWKS_URL from env
|
|
94
|
+
|
|
95
|
+
app = FastAPI()
|
|
96
|
+
|
|
97
|
+
@app.get("/api/protected")
|
|
98
|
+
async def protected(user: dict = Depends(auth.require_auth())):
|
|
99
|
+
return {"user_id": user["sub"]}
|
|
100
|
+
|
|
101
|
+
@app.get("/api/admin")
|
|
102
|
+
async def admin(user: dict = Depends(auth.require_roles(["org:admin"]))):
|
|
103
|
+
return {"message": "Admin access granted"}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### S3Manager — Quick Look
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from neuronos_capabilities import S3Manager
|
|
110
|
+
|
|
111
|
+
s3 = S3Manager({"region": "us-east-1"})
|
|
112
|
+
|
|
113
|
+
# Upload and download
|
|
114
|
+
s3.put_object("my-bucket", "hello.txt", b"Hello World")
|
|
115
|
+
s3.download_file("my-bucket", "hello.txt", "/tmp/hello.txt")
|
|
116
|
+
|
|
117
|
+
# Generate a temporary download link
|
|
118
|
+
url = s3.generate_presigned_download_url("my-bucket", "hello.txt", expiration=900)
|
|
119
|
+
|
|
120
|
+
# Recursive listing
|
|
121
|
+
for obj in s3.walk("my-bucket", "data/"):
|
|
122
|
+
print(f"{obj['Key']} {obj['Size']} bytes")
|
|
123
|
+
|
|
124
|
+
# Multipart upload for large files
|
|
125
|
+
upload_id = s3.create_multipart_upload("my-bucket", "large-file.bin")
|
|
126
|
+
part = s3.upload_part("my-bucket", "large-file.bin", 1, upload_id, chunk)
|
|
127
|
+
s3.complete_multipart_upload("my-bucket", "large-file.bin", upload_id, [part])
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
git clone https://github.com/neuronos/neuronos_python_capabilities_library
|
|
134
|
+
cd neuronos_python_capabilities_library
|
|
135
|
+
pip install -e ".[dev]"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
pytest -v
|
|
140
|
+
pytest --cov=neuronos_capabilities --cov-report=html
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Contributing
|
|
144
|
+
|
|
145
|
+
1. Follow hexagonal architecture: port → adapter → manager
|
|
146
|
+
2. Implement both sync and async methods
|
|
147
|
+
3. Include tests and documentation
|
|
148
|
+
4. Update this README with the new capability
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT — see [LICENSE](./LICENSE)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# NeuronOS Capabilities Library
|
|
2
|
+
|
|
3
|
+
A collection of provider-agnostic backend capabilities for Python applications, following hexagonal architecture principles.
|
|
4
|
+
|
|
5
|
+
Each capability exposes well-defined ports and adapters, making implementations swappable without changing your application code.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install neuronos-capabilities-lib
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The import name is `neuronos_capabilities` (`from neuronos_capabilities import ...`).
|
|
14
|
+
|
|
15
|
+
> **Moved from `neuronos-capabilities`.** Versions up to 0.0.1.5 were published as
|
|
16
|
+
> `neuronos-capabilities`; from 0.0.2 on the package is `neuronos-capabilities-lib`. The code and
|
|
17
|
+
> imports are the same. Before switching a local environment, run
|
|
18
|
+
> `pip uninstall neuronos-capabilities` (both names install the same `neuronos_capabilities/` folder).
|
|
19
|
+
|
|
20
|
+
## What is a Capability?
|
|
21
|
+
|
|
22
|
+
A capability is a self-contained backend module that follows hexagonal architecture (ports and adapters pattern):
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
Capability/
|
|
26
|
+
├── ports/ # Abstract interfaces — defines WHAT operations exist
|
|
27
|
+
├── adapters/ # Provider implementations — defines HOW they work
|
|
28
|
+
├── config/ # Configuration management
|
|
29
|
+
├── exceptions/ # Custom exception hierarchy
|
|
30
|
+
└── [name]_manager.py # Public API — single entrypoint for consumers
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
You interact only with the Manager. The port defines the contract, and adapters implement it for specific providers. Switching from PostgreSQL to MySQL, or from Clerk to Auth0, is just a config change.
|
|
34
|
+
|
|
35
|
+
## Available Capabilities
|
|
36
|
+
|
|
37
|
+
| Capability | Description | Providers | Docs |
|
|
38
|
+
|------------|-------------|-----------|------|
|
|
39
|
+
| **SQLManager** | Database management with ORM, migrations, connection pooling | PostgreSQL | [docs/sql-manager.md](./docs/sql-manager.md) |
|
|
40
|
+
| **AuthManager** | JWT authentication and role-based authorization for FastAPI | Clerk | [docs/auth-manager.md](./docs/auth-manager.md) |
|
|
41
|
+
| **S3Manager** | Object storage with CRUD, listing, multipart uploads, presigned URLs | AWS S3 | [docs/s3-manager.md](./docs/s3-manager.md) |
|
|
42
|
+
|
|
43
|
+
### SQLManager — Quick Look
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from neuronos_capabilities import SQLManager
|
|
47
|
+
|
|
48
|
+
config = {"host": "localhost", "database": "myapp", "user": "postgres", "password": "secret"}
|
|
49
|
+
|
|
50
|
+
with SQLManager(config) as db:
|
|
51
|
+
db.create_tables([User])
|
|
52
|
+
user = db.insert(User, {"name": "Alice", "email": "alice@example.com"})
|
|
53
|
+
users = db.query(User, {"name": "Alice"})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### AuthManager — Quick Look
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from neuronos_capabilities import AuthManager
|
|
60
|
+
from fastapi import FastAPI, Depends
|
|
61
|
+
|
|
62
|
+
auth = AuthManager() # reads NEURONOS_AUTH_JWKS_URL from env
|
|
63
|
+
|
|
64
|
+
app = FastAPI()
|
|
65
|
+
|
|
66
|
+
@app.get("/api/protected")
|
|
67
|
+
async def protected(user: dict = Depends(auth.require_auth())):
|
|
68
|
+
return {"user_id": user["sub"]}
|
|
69
|
+
|
|
70
|
+
@app.get("/api/admin")
|
|
71
|
+
async def admin(user: dict = Depends(auth.require_roles(["org:admin"]))):
|
|
72
|
+
return {"message": "Admin access granted"}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### S3Manager — Quick Look
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from neuronos_capabilities import S3Manager
|
|
79
|
+
|
|
80
|
+
s3 = S3Manager({"region": "us-east-1"})
|
|
81
|
+
|
|
82
|
+
# Upload and download
|
|
83
|
+
s3.put_object("my-bucket", "hello.txt", b"Hello World")
|
|
84
|
+
s3.download_file("my-bucket", "hello.txt", "/tmp/hello.txt")
|
|
85
|
+
|
|
86
|
+
# Generate a temporary download link
|
|
87
|
+
url = s3.generate_presigned_download_url("my-bucket", "hello.txt", expiration=900)
|
|
88
|
+
|
|
89
|
+
# Recursive listing
|
|
90
|
+
for obj in s3.walk("my-bucket", "data/"):
|
|
91
|
+
print(f"{obj['Key']} {obj['Size']} bytes")
|
|
92
|
+
|
|
93
|
+
# Multipart upload for large files
|
|
94
|
+
upload_id = s3.create_multipart_upload("my-bucket", "large-file.bin")
|
|
95
|
+
part = s3.upload_part("my-bucket", "large-file.bin", 1, upload_id, chunk)
|
|
96
|
+
s3.complete_multipart_upload("my-bucket", "large-file.bin", upload_id, [part])
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Development
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git clone https://github.com/neuronos/neuronos_python_capabilities_library
|
|
103
|
+
cd neuronos_python_capabilities_library
|
|
104
|
+
pip install -e ".[dev]"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pytest -v
|
|
109
|
+
pytest --cov=neuronos_capabilities --cov-report=html
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Contributing
|
|
113
|
+
|
|
114
|
+
1. Follow hexagonal architecture: port → adapter → manager
|
|
115
|
+
2. Implement both sync and async methods
|
|
116
|
+
3. Include tests and documentation
|
|
117
|
+
4. Update this README with the new capability
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT — see [LICENSE](./LICENSE)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AuthManager - Authentication & Authorization Capability for NeuronOS.
|
|
3
|
+
|
|
4
|
+
A provider-agnostic auth capability following hexagonal architecture principles.
|
|
5
|
+
Provides FastAPI decorators for JWT-based authentication and role-based access control.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
from neuronos_capabilities import AuthManager
|
|
9
|
+
|
|
10
|
+
auth = AuthManager({
|
|
11
|
+
"provider": "clerk",
|
|
12
|
+
"jwks_url": "https://your-app.clerk.accounts.dev/.well-known/jwks.json",
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
@app.get("/api/protected")
|
|
16
|
+
def protected(user: dict = Depends(auth.require_auth())):
|
|
17
|
+
return {"user_id": user["sub"]}
|
|
18
|
+
|
|
19
|
+
@app.get("/api/admin")
|
|
20
|
+
def admin(user: dict = Depends(auth.require_roles(["org:admin"]))):
|
|
21
|
+
return {"message": "Admin access granted"}
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from .auth_manager import AuthManager
|
|
25
|
+
from .config.auth_config import AuthConfig
|
|
26
|
+
from .exceptions.auth_exceptions import (
|
|
27
|
+
AuthManagerError,
|
|
28
|
+
ConfigurationError,
|
|
29
|
+
TokenError,
|
|
30
|
+
AuthorizationError,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"AuthManager",
|
|
35
|
+
"AuthConfig",
|
|
36
|
+
"AuthManagerError",
|
|
37
|
+
"ConfigurationError",
|
|
38
|
+
"TokenError",
|
|
39
|
+
"AuthorizationError",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clerk authentication adapter.
|
|
3
|
+
|
|
4
|
+
Implements AuthPort using Clerk's JWKS endpoint for JWT verification.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
import jwt
|
|
10
|
+
from jwt import PyJWKClient
|
|
11
|
+
|
|
12
|
+
from ..config.auth_config import AuthConfig
|
|
13
|
+
from ..ports.auth_port import AuthPort
|
|
14
|
+
from ..exceptions.auth_exceptions import TokenError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ClerkAdapter(AuthPort):
|
|
18
|
+
"""
|
|
19
|
+
Clerk-based authentication adapter.
|
|
20
|
+
|
|
21
|
+
Verifies JWTs against Clerk's JWKS endpoint and extracts
|
|
22
|
+
role/organization claims from the token payload.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, config: AuthConfig):
|
|
26
|
+
self._config = config
|
|
27
|
+
self._jwks_client = PyJWKClient(config.jwks_url)
|
|
28
|
+
|
|
29
|
+
def _decode_token(self, token: str) -> Dict[str, Any]:
|
|
30
|
+
"""Shared decode logic for sync and async paths."""
|
|
31
|
+
try:
|
|
32
|
+
signing_key = self._jwks_client.get_signing_key_from_jwt(token)
|
|
33
|
+
|
|
34
|
+
decode_options = {"verify_audience": False}
|
|
35
|
+
kwargs: Dict[str, Any] = {
|
|
36
|
+
"jwt": token,
|
|
37
|
+
"key": signing_key.key,
|
|
38
|
+
"algorithms": self._config.algorithms,
|
|
39
|
+
"options": decode_options,
|
|
40
|
+
# Clock tolerance on exp/nbf/iat. Without it, PyJWT rejects a
|
|
41
|
+
# token whose `iat` is a single second ahead of this host
|
|
42
|
+
# ("The token is not yet valid (iat)", api_jwt.py: `iat > now +
|
|
43
|
+
# leeway`). Clerk stamps `iat` on its own servers, so a host
|
|
44
|
+
# running slightly behind fails EVERY request, and the client
|
|
45
|
+
# cannot compensate — re-minting yields a later `iat` still.
|
|
46
|
+
"leeway": self._config.leeway_seconds,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if self._config.issuer:
|
|
50
|
+
kwargs["issuer"] = self._config.issuer
|
|
51
|
+
if self._config.audience:
|
|
52
|
+
kwargs["audience"] = self._config.audience
|
|
53
|
+
decode_options["verify_audience"] = True
|
|
54
|
+
|
|
55
|
+
return jwt.decode(**kwargs)
|
|
56
|
+
except jwt.ExpiredSignatureError:
|
|
57
|
+
raise TokenError("Token has expired")
|
|
58
|
+
except jwt.InvalidTokenError as e:
|
|
59
|
+
raise TokenError(f"Invalid token: {str(e)}")
|
|
60
|
+
except Exception as e:
|
|
61
|
+
raise TokenError(f"Token verification failed: {str(e)}")
|
|
62
|
+
|
|
63
|
+
def verify_token(self, token: str) -> Dict[str, Any]:
|
|
64
|
+
return self._decode_token(token)
|
|
65
|
+
|
|
66
|
+
async def verify_token_async(self, token: str) -> Dict[str, Any]:
|
|
67
|
+
# PyJWKClient doesn't have native async; delegate to sync.
|
|
68
|
+
# For high-throughput, consider caching the JWKS keys.
|
|
69
|
+
return self._decode_token(token)
|
|
70
|
+
|
|
71
|
+
def has_role(self, payload: Dict[str, Any], roles: List[str]) -> bool:
|
|
72
|
+
# Clerk puts custom roles in public_metadata.rol when no org is configured,
|
|
73
|
+
# and in org_role when the user is scoped to an organization.
|
|
74
|
+
# Check both locations so the same decorator works either way.
|
|
75
|
+
org_role = payload.get("org_role")
|
|
76
|
+
if org_role and org_role in roles:
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
metadata = payload.get("public_metadata") or payload.get("metadata") or {}
|
|
80
|
+
user_rol = metadata.get("rol", "")
|
|
81
|
+
return user_rol in roles
|
|
82
|
+
|
|
83
|
+
def get_org_id(self, payload: Dict[str, Any]) -> Optional[str]:
|
|
84
|
+
return payload.get("org_id")
|
|
85
|
+
|
|
86
|
+
def get_user_id(self, payload: Dict[str, Any]) -> Optional[str]:
|
|
87
|
+
return payload.get("sub")
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AuthManager - Public API for authentication and authorization.
|
|
3
|
+
|
|
4
|
+
Provides FastAPI-compatible dependency decorators for protecting endpoints
|
|
5
|
+
with JWT verification and role-based access control.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, List, Optional, Union
|
|
9
|
+
|
|
10
|
+
from fastapi import Depends, HTTPException
|
|
11
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
12
|
+
|
|
13
|
+
from .config.auth_config import AuthConfig
|
|
14
|
+
from .ports.auth_port import AuthPort
|
|
15
|
+
from .adapters.clerk_adapter import ClerkAdapter
|
|
16
|
+
from .exceptions.auth_exceptions import (
|
|
17
|
+
AuthManagerError,
|
|
18
|
+
ConfigurationError,
|
|
19
|
+
TokenError,
|
|
20
|
+
AuthorizationError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_security = HTTPBearer()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AuthManager:
|
|
28
|
+
"""
|
|
29
|
+
Public API for authentication and authorization.
|
|
30
|
+
|
|
31
|
+
Provides FastAPI dependency functions that can be used with Depends()
|
|
32
|
+
to protect endpoints with JWT verification and role-based access control.
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
auth = AuthManager({"jwks_url": os.getenv("NEURONOS_AUTH_JWKS_URL")})
|
|
36
|
+
|
|
37
|
+
@app.get("/api/protected")
|
|
38
|
+
def protected(user: dict = Depends(auth.require_auth())):
|
|
39
|
+
return {"user_id": user["sub"]}
|
|
40
|
+
|
|
41
|
+
@app.get("/api/admin")
|
|
42
|
+
def admin_only(user: dict = Depends(auth.require_roles(["org:admin"]))):
|
|
43
|
+
return {"message": "Welcome admin"}
|
|
44
|
+
|
|
45
|
+
@app.get("/api/tenant")
|
|
46
|
+
def tenant_only(user: dict = Depends(auth.require_org("org_2abc123"))):
|
|
47
|
+
return {"message": "Org access granted"}
|
|
48
|
+
|
|
49
|
+
@app.get("/api/coordinator")
|
|
50
|
+
def coordinator(user: dict = Depends(auth.require_org_role("org_2abc123", ["org:coordinator"]))):
|
|
51
|
+
return {"message": "Org coordinator access"}
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
_ADAPTERS = {
|
|
55
|
+
"clerk": ClerkAdapter,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def __init__(self, config: Optional[Union[AuthConfig, Dict[str, Any]]] = None):
|
|
59
|
+
"""
|
|
60
|
+
Initialize AuthManager with configuration.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
config: AuthConfig object or dictionary with auth parameters.
|
|
64
|
+
Sensitive values fall back to environment variables.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ConfigurationError: If configuration is invalid or missing
|
|
68
|
+
"""
|
|
69
|
+
if config is None:
|
|
70
|
+
# Attempt to build config entirely from env variables
|
|
71
|
+
config = AuthConfig()
|
|
72
|
+
elif isinstance(config, dict):
|
|
73
|
+
try:
|
|
74
|
+
config = AuthConfig(**config)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
raise ConfigurationError(f"Invalid configuration: {str(e)}")
|
|
77
|
+
|
|
78
|
+
self.config = config
|
|
79
|
+
self._adapter: AuthPort = self._create_adapter(config)
|
|
80
|
+
|
|
81
|
+
def _create_adapter(self, config: AuthConfig) -> AuthPort:
|
|
82
|
+
adapter_cls = self._ADAPTERS.get(config.provider)
|
|
83
|
+
if not adapter_cls:
|
|
84
|
+
raise ConfigurationError(
|
|
85
|
+
f"Unsupported provider: {config.provider}. "
|
|
86
|
+
f"Supported: {', '.join(self._ADAPTERS.keys())}"
|
|
87
|
+
)
|
|
88
|
+
return adapter_cls(config)
|
|
89
|
+
|
|
90
|
+
# ===== FastAPI Dependency Factories =====
|
|
91
|
+
|
|
92
|
+
def require_auth(self):
|
|
93
|
+
"""
|
|
94
|
+
FastAPI dependency that verifies the JWT and returns the decoded payload.
|
|
95
|
+
|
|
96
|
+
Returns 401 if token is missing or invalid.
|
|
97
|
+
|
|
98
|
+
Usage:
|
|
99
|
+
@app.get("/protected")
|
|
100
|
+
def endpoint(user: dict = Depends(auth.require_auth())):
|
|
101
|
+
...
|
|
102
|
+
"""
|
|
103
|
+
adapter = self._adapter
|
|
104
|
+
|
|
105
|
+
async def _dependency(
|
|
106
|
+
credentials: HTTPAuthorizationCredentials = Depends(_security),
|
|
107
|
+
) -> Dict[str, Any]:
|
|
108
|
+
try:
|
|
109
|
+
return await adapter.verify_token_async(credentials.credentials)
|
|
110
|
+
except TokenError as e:
|
|
111
|
+
raise HTTPException(status_code=401, detail=str(e))
|
|
112
|
+
|
|
113
|
+
return _dependency
|
|
114
|
+
|
|
115
|
+
def require_roles(self, roles: List[str]):
|
|
116
|
+
"""
|
|
117
|
+
FastAPI dependency that verifies JWT and checks the user has one of the given roles.
|
|
118
|
+
|
|
119
|
+
Returns 401 if token is invalid, 403 if role is not matched.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
roles: List of acceptable roles (e.g., ["coordinador", "admin"])
|
|
123
|
+
|
|
124
|
+
Usage:
|
|
125
|
+
@app.get("/admin")
|
|
126
|
+
def endpoint(user: dict = Depends(auth.require_roles(["admin"]))):
|
|
127
|
+
...
|
|
128
|
+
"""
|
|
129
|
+
adapter = self._adapter
|
|
130
|
+
|
|
131
|
+
async def _dependency(
|
|
132
|
+
credentials: HTTPAuthorizationCredentials = Depends(_security),
|
|
133
|
+
) -> Dict[str, Any]:
|
|
134
|
+
try:
|
|
135
|
+
payload = await adapter.verify_token_async(credentials.credentials)
|
|
136
|
+
except TokenError as e:
|
|
137
|
+
raise HTTPException(status_code=401, detail=str(e))
|
|
138
|
+
|
|
139
|
+
if not adapter.has_role(payload, roles):
|
|
140
|
+
raise HTTPException(
|
|
141
|
+
status_code=403,
|
|
142
|
+
detail=f"Requires one of roles: {', '.join(roles)}",
|
|
143
|
+
)
|
|
144
|
+
return payload
|
|
145
|
+
|
|
146
|
+
return _dependency
|
|
147
|
+
|
|
148
|
+
def require_org(self, org_id: str):
|
|
149
|
+
"""
|
|
150
|
+
FastAPI dependency that verifies JWT and checks the user belongs to a specific org.
|
|
151
|
+
|
|
152
|
+
Returns 401 if token is invalid, 403 if org doesn't match.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
org_id: Required organization ID (e.g., "org_2abc123")
|
|
156
|
+
|
|
157
|
+
Usage:
|
|
158
|
+
@app.get("/tenant")
|
|
159
|
+
def endpoint(user: dict = Depends(auth.require_org("org_2abc123"))):
|
|
160
|
+
...
|
|
161
|
+
"""
|
|
162
|
+
adapter = self._adapter
|
|
163
|
+
|
|
164
|
+
async def _dependency(
|
|
165
|
+
credentials: HTTPAuthorizationCredentials = Depends(_security),
|
|
166
|
+
) -> Dict[str, Any]:
|
|
167
|
+
try:
|
|
168
|
+
payload = await adapter.verify_token_async(credentials.credentials)
|
|
169
|
+
except TokenError as e:
|
|
170
|
+
raise HTTPException(status_code=401, detail=str(e))
|
|
171
|
+
|
|
172
|
+
if adapter.get_org_id(payload) != org_id:
|
|
173
|
+
raise HTTPException(
|
|
174
|
+
status_code=403,
|
|
175
|
+
detail=f"Access restricted to organization '{org_id}'.",
|
|
176
|
+
)
|
|
177
|
+
return payload
|
|
178
|
+
|
|
179
|
+
return _dependency
|
|
180
|
+
|
|
181
|
+
def require_org_role(self, org_id: str, roles: List[str]):
|
|
182
|
+
"""
|
|
183
|
+
FastAPI dependency that verifies JWT, org membership, AND role in one step.
|
|
184
|
+
|
|
185
|
+
Returns 401 if token is invalid, 403 if org or role doesn't match.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
org_id: Required organization ID
|
|
189
|
+
roles: List of acceptable roles within that org
|
|
190
|
+
|
|
191
|
+
Usage:
|
|
192
|
+
@app.get("/org-coordinator")
|
|
193
|
+
def endpoint(user: dict = Depends(auth.require_org_role("org_2abc123", ["org:coordinator"]))):
|
|
194
|
+
...
|
|
195
|
+
"""
|
|
196
|
+
adapter = self._adapter
|
|
197
|
+
|
|
198
|
+
async def _dependency(
|
|
199
|
+
credentials: HTTPAuthorizationCredentials = Depends(_security),
|
|
200
|
+
) -> Dict[str, Any]:
|
|
201
|
+
try:
|
|
202
|
+
payload = await adapter.verify_token_async(credentials.credentials)
|
|
203
|
+
except TokenError as e:
|
|
204
|
+
raise HTTPException(status_code=401, detail=str(e))
|
|
205
|
+
|
|
206
|
+
if adapter.get_org_id(payload) != org_id:
|
|
207
|
+
raise HTTPException(
|
|
208
|
+
status_code=403,
|
|
209
|
+
detail=f"Access restricted to organization '{org_id}'.",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if not adapter.has_role(payload, roles):
|
|
213
|
+
raise HTTPException(
|
|
214
|
+
status_code=403,
|
|
215
|
+
detail=f"Requires one of roles: {', '.join(roles)}",
|
|
216
|
+
)
|
|
217
|
+
return payload
|
|
218
|
+
|
|
219
|
+
return _dependency
|
|
220
|
+
|
|
221
|
+
# ===== Direct API (non-decorator usage) =====
|
|
222
|
+
|
|
223
|
+
def verify_token(self, token: str) -> Dict[str, Any]:
|
|
224
|
+
"""Verify and decode a JWT token (synchronous)."""
|
|
225
|
+
return self._adapter.verify_token(token)
|
|
226
|
+
|
|
227
|
+
async def verify_token_async(self, token: str) -> Dict[str, Any]:
|
|
228
|
+
"""Verify and decode a JWT token (asynchronous)."""
|
|
229
|
+
return await self._adapter.verify_token_async(token)
|
|
230
|
+
|
|
231
|
+
def get_user_id(self, payload: Dict[str, Any]) -> Optional[str]:
|
|
232
|
+
"""Extract user ID from decoded payload."""
|
|
233
|
+
return self._adapter.get_user_id(payload)
|
|
234
|
+
|
|
235
|
+
def get_org_id(self, payload: Dict[str, Any]) -> Optional[str]:
|
|
236
|
+
"""Extract organization ID from decoded payload."""
|
|
237
|
+
return self._adapter.get_org_id(payload)
|