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.
Files changed (81) hide show
  1. neuronos_capabilities_lib-0.0.2/LICENSE +21 -0
  2. neuronos_capabilities_lib-0.0.2/PKG-INFO +152 -0
  3. neuronos_capabilities_lib-0.0.2/README.md +121 -0
  4. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/__init__.py +42 -0
  5. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/adapters/__init__.py +3 -0
  6. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/adapters/clerk_adapter.py +87 -0
  7. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/auth_manager.py +237 -0
  8. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/config/__init__.py +3 -0
  9. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/config/auth_config.py +106 -0
  10. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/examples/usage.py +56 -0
  11. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/exceptions/__init__.py +13 -0
  12. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/exceptions/auth_exceptions.py +23 -0
  13. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/ports/__init__.py +3 -0
  14. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/ports/auth_port.py +92 -0
  15. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/tests/__init__.py +1 -0
  16. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/AuthManager/tests/test_clock_skew.py +178 -0
  17. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/README.md +75 -0
  18. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/__init__.py +37 -0
  19. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/adapters/__init__.py +7 -0
  20. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/adapters/aws.py +146 -0
  21. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/adapters/console.py +30 -0
  22. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/adapters/memory.py +143 -0
  23. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/adapters/postgres.py +214 -0
  24. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/adapters/templates.py +73 -0
  25. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/artifact.yaml +64 -0
  26. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/config/__init__.py +3 -0
  27. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/config/notifications_config.py +123 -0
  28. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/entrypoints/__init__.py +1 -0
  29. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/entrypoints/events.py +36 -0
  30. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/entrypoints/sender.py +22 -0
  31. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/exceptions/__init__.py +19 -0
  32. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/exceptions/notifications_exceptions.py +41 -0
  33. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/models.py +88 -0
  34. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/notifications_manager.py +605 -0
  35. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/ports/__init__.py +110 -0
  36. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/tests/__init__.py +0 -0
  37. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/tests/test_aws_adapters.py +93 -0
  38. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/NotificationsManager/tests/test_notifications_manager.py +198 -0
  39. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/__init__.py +55 -0
  40. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/adapters/__init__.py +3 -0
  41. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/adapters/aws_adapter.py +400 -0
  42. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/artifact.yaml +52 -0
  43. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/config/__init__.py +3 -0
  44. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/config/s3_config.py +91 -0
  45. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/examples/usage.py +86 -0
  46. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/exceptions/__init__.py +21 -0
  47. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/exceptions/s3_exceptions.py +43 -0
  48. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/ports/__init__.py +3 -0
  49. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/ports/s3_port.py +499 -0
  50. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/s3_manager.py +332 -0
  51. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/S3Manager/tests/__init__.py +1 -0
  52. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/__init__.py +53 -0
  53. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/adapters/__init__.py +0 -0
  54. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/adapters/postgres_adapter.py +893 -0
  55. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/artifact.yaml +52 -0
  56. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/config/__init__.py +0 -0
  57. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/config/sql_config.py +175 -0
  58. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/examples/testing_library_execution.py +167 -0
  59. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/exceptions/__init__.py +0 -0
  60. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/exceptions/sql_exceptions.py +117 -0
  61. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/migrations/__init__.py +0 -0
  62. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/migrations/alembic_manager.py +617 -0
  63. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/models/__init__.py +9 -0
  64. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/models/base.py +118 -0
  65. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/ports/__init__.py +0 -0
  66. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/ports/sql_port.py +940 -0
  67. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/sql_manager.py +495 -0
  68. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/tests/__init__.py +0 -0
  69. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/utils/__init__.py +0 -0
  70. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/utils/query_builder.py +238 -0
  71. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/SQLManager/utils/singleton.py +120 -0
  72. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/__init__.py +58 -0
  73. neuronos_capabilities_lib-0.0.2/neuronos_capabilities/__version__.py +2 -0
  74. neuronos_capabilities_lib-0.0.2/neuronos_capabilities_lib.egg-info/PKG-INFO +152 -0
  75. neuronos_capabilities_lib-0.0.2/neuronos_capabilities_lib.egg-info/SOURCES.txt +79 -0
  76. neuronos_capabilities_lib-0.0.2/neuronos_capabilities_lib.egg-info/dependency_links.txt +1 -0
  77. neuronos_capabilities_lib-0.0.2/neuronos_capabilities_lib.egg-info/requires.txt +17 -0
  78. neuronos_capabilities_lib-0.0.2/neuronos_capabilities_lib.egg-info/top_level.txt +1 -0
  79. neuronos_capabilities_lib-0.0.2/pyproject.toml +57 -0
  80. neuronos_capabilities_lib-0.0.2/setup.cfg +4 -0
  81. 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,3 @@
1
+ from .clerk_adapter import ClerkAdapter
2
+
3
+ __all__ = ["ClerkAdapter"]
@@ -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)
@@ -0,0 +1,3 @@
1
+ from .auth_config import AuthConfig
2
+
3
+ __all__ = ["AuthConfig"]