caronte-sdk 0.2.4__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.
@@ -0,0 +1,41 @@
1
+ # Env files
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+
6
+ # Node
7
+ node_modules/
8
+ dist/
9
+ *.tgz
10
+ *.tsbuildinfo
11
+ npm-debug.log*
12
+
13
+ # Angular
14
+ .angular/
15
+
16
+ # Python
17
+ __pycache__/
18
+ *.pyc
19
+ *.pyo
20
+ *.egg-info/
21
+ .pytest_cache/
22
+ .venv/
23
+ venv/
24
+
25
+ # OS
26
+ .DS_Store
27
+ Thumbs.db
28
+
29
+ # IDE
30
+ .vscode/
31
+ .idea/
32
+
33
+ hermes/docs/demo/
34
+ hermes/docs/storybook/
35
+ hermes/docs/figma/
36
+ hermes/docs/documentation.json
37
+ hermes/docs/website/src/compiled-content/*
38
+ !hermes/docs/website/src/compiled-content/.gitkeep
39
+
40
+ # Clone de referência do shadcn-ui/ui upstream — não é nosso repo, não deve virar gitlink.
41
+ /shadcn/
@@ -0,0 +1,26 @@
1
+ CC0 1.0 Universal
2
+
3
+ Statement of Purpose
4
+
5
+ The laws of most jurisdictions throughout the world automatically confer
6
+ exclusive Copyright and Related Rights (defined below) upon the creator and
7
+ subsequent owner(s) of an original work of authorship and/or a database
8
+ (each, a "Work").
9
+
10
+ Certain owners wish to permanently relinquish those rights to a Work for the
11
+ purpose of contributing to a commons of creative, cultural and scientific works
12
+ that the public can reliably and without fear of infringement build upon,
13
+ modify, incorporate in other works, cite, and distribute, as freely as
14
+ possible, without legal restriction.
15
+
16
+ To the greatest extent permitted by, but not in contravention of, applicable
17
+ law, Affirmer hereby overtly, fully, permanently, irrevocably and
18
+ unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
19
+ and Related Rights and associated claims and causes of action, in the Work.
20
+
21
+ Should any part of this dedication be judged legally invalid or ineffective
22
+ under applicable law, the dedication shall be preserved to the maximum extent
23
+ permitted by law.
24
+
25
+ For more information, please see:
26
+ https://creativecommons.org/publicdomain/zero/1.0/
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: caronte-sdk
3
+ Version: 0.2.4
4
+ Summary: Python client for the Argos authorizer (OIDC + RBAC)
5
+ Project-URL: Homepage, https://github.com/xlucvvs/argos-python
6
+ Project-URL: Repository, https://github.com/xlucvvs/argos-python
7
+ Project-URL: Bug Tracker, https://github.com/xlucvvs/argos-python/issues
8
+ Author-email: Lucas Ribeiro <lucasribeiro.sec@gmail.com>
9
+ License: CC0 1.0 Universal
10
+
11
+ Statement of Purpose
12
+
13
+ The laws of most jurisdictions throughout the world automatically confer
14
+ exclusive Copyright and Related Rights (defined below) upon the creator and
15
+ subsequent owner(s) of an original work of authorship and/or a database
16
+ (each, a "Work").
17
+
18
+ Certain owners wish to permanently relinquish those rights to a Work for the
19
+ purpose of contributing to a commons of creative, cultural and scientific works
20
+ that the public can reliably and without fear of infringement build upon,
21
+ modify, incorporate in other works, cite, and distribute, as freely as
22
+ possible, without legal restriction.
23
+
24
+ To the greatest extent permitted by, but not in contravention of, applicable
25
+ law, Affirmer hereby overtly, fully, permanently, irrevocably and
26
+ unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
27
+ and Related Rights and associated claims and causes of action, in the Work.
28
+
29
+ Should any part of this dedication be judged legally invalid or ineffective
30
+ under applicable law, the dedication shall be preserved to the maximum extent
31
+ permitted by law.
32
+
33
+ For more information, please see:
34
+ https://creativecommons.org/publicdomain/zero/1.0/
35
+ License-File: LICENSE
36
+ Keywords: argos,auth,authorization,jwt,oidc,rbac
37
+ Classifier: Development Status :: 4 - Beta
38
+ Classifier: Framework :: Django
39
+ Classifier: Framework :: FastAPI
40
+ Classifier: Framework :: Flask
41
+ Classifier: Intended Audience :: Developers
42
+ Classifier: License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
43
+ Classifier: Programming Language :: Python :: 3
44
+ Classifier: Programming Language :: Python :: 3.11
45
+ Classifier: Programming Language :: Python :: 3.12
46
+ Classifier: Programming Language :: Python :: 3.13
47
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
48
+ Classifier: Topic :: Security
49
+ Requires-Python: >=3.11
50
+ Requires-Dist: httpx>=0.27
51
+ Requires-Dist: pyjwt[crypto]>=2.8
52
+ Requires-Dist: python-jose[cryptography]>=3.3
53
+ Provides-Extra: dev
54
+ Requires-Dist: cryptography>=42; extra == 'dev'
55
+ Requires-Dist: django>=4.0; extra == 'dev'
56
+ Requires-Dist: flask>=2.0; extra == 'dev'
57
+ Requires-Dist: httpx>=0.27; extra == 'dev'
58
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
59
+ Requires-Dist: pytest>=8; extra == 'dev'
60
+ Requires-Dist: respx>=0.21; extra == 'dev'
61
+ Provides-Extra: django
62
+ Requires-Dist: django>=4.0; extra == 'django'
63
+ Provides-Extra: fastapi
64
+ Requires-Dist: fastapi>=0.111; extra == 'fastapi'
65
+ Requires-Dist: starlette>=0.37; extra == 'fastapi'
66
+ Provides-Extra: flask
67
+ Requires-Dist: flask>=2.0; extra == 'flask'
68
+ Provides-Extra: strawberry
69
+ Requires-Dist: strawberry-graphql>=0.235; extra == 'strawberry'
70
+ Description-Content-Type: text/markdown
71
+
72
+ # argos
73
+
74
+ Python client for the **Argos** authorizer — handles app authentication,
75
+ JWT validation, permission checking and automatic operation sync.
76
+
77
+ ## Installation
78
+
79
+ ```bash
80
+ pip install argos
81
+ # With FastAPI support
82
+ pip install "argos[fastapi]"
83
+ ```
84
+
85
+ ## Quick start with FastAPI
86
+
87
+ ```python
88
+ from fastapi import FastAPI, Request
89
+ from argos.adapters.fastapi import Argos, operation
90
+
91
+ app = FastAPI()
92
+
93
+ argos = Argos(
94
+ app,
95
+ authorizer_url="http://localhost:4000/api",
96
+ app_id="your-app-uuid",
97
+ secret="your-app-secret",
98
+ )
99
+
100
+
101
+ @app.get("/public")
102
+ @operation(id="public.info", level="public")
103
+ async def public_info():
104
+ """Anyone can call this — no token needed."""
105
+ return {"message": "Hello, world!"}
106
+
107
+
108
+ @app.get("/items")
109
+ @operation(id="items.list", level="private")
110
+ async def list_items(request: Request):
111
+ """Requires any authenticated user (token with non-empty groups)."""
112
+ user = request.state.user # TokenClaims
113
+ return {"user": user.sub, "items": []}
114
+
115
+
116
+ @app.delete("/items/{item_id}")
117
+ @operation(id="items.delete", level="protected")
118
+ async def delete_item(item_id: str, request: Request):
119
+ """Requires a user whose group has explicit permission for items.delete."""
120
+ return {"deleted": item_id}
121
+ ```
122
+
123
+ ## Standalone client
124
+
125
+ ```python
126
+ import asyncio
127
+ from argos import ArgosClient, OperationDescriptor
128
+
129
+ client = ArgosClient(
130
+ authorizer_url="http://localhost:4000/api",
131
+ app_id="your-app-uuid",
132
+ secret="your-app-secret",
133
+ )
134
+
135
+ client.register_operation(
136
+ OperationDescriptor(identifier="items.list", method="read", level="private")
137
+ )
138
+
139
+ async def main():
140
+ await client.startup()
141
+
142
+ # Validate a token from an incoming request
143
+ claims = await client.validate_token("<bearer-token>")
144
+
145
+ # Check whether the token has permission
146
+ allowed = client.check_permission(claims, "items.list", "read")
147
+ print(f"Allowed: {allowed}")
148
+
149
+ asyncio.run(main())
150
+ ```
151
+
152
+ ## Operation levels
153
+
154
+ | Level | Who can access |
155
+ |-------------|----------------|
156
+ | `public` | Everyone — no token required |
157
+ | `private` | Any authenticated user (token with at least one group) |
158
+ | `protected` | Only users whose groups intersect the operation's `allowed_groups` |
159
+
160
+ ## Method auto-detection
161
+
162
+ The `@operation` decorator infers the method from the function name:
163
+
164
+ | Function name prefix/keyword | Detected method |
165
+ |------------------------------|-----------------|
166
+ | `get_`, `list_`, `fetch_`, `read_` | `read` |
167
+ | `delete_`, `remove_`, `destroy_` | `delete` |
168
+ | contains `stream`, `subscribe`, `watch` | `stream` |
169
+ | anything else | `write` |
170
+
171
+ ## Configuration
172
+
173
+ | Parameter | Description |
174
+ |-----------|-------------|
175
+ | `authorizer_url` | Base URL of Argos, including the API prefix (e.g. `http://host/api`) |
176
+ | `app_id` | UUID of the app registered in `auth.apps` |
177
+ | `secret` | Plain-text app secret (never committed — use env vars) |
178
+
179
+ ```python
180
+ import os
181
+ from argos.adapters.fastapi import Argos
182
+
183
+ argos = Argos(
184
+ app,
185
+ authorizer_url=os.environ["AUTHORIZER_URL"],
186
+ app_id=os.environ["APP_ID"],
187
+ secret=os.environ["APP_SECRET"],
188
+ )
189
+ ```
@@ -0,0 +1,118 @@
1
+ # argos
2
+
3
+ Python client for the **Argos** authorizer — handles app authentication,
4
+ JWT validation, permission checking and automatic operation sync.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ pip install argos
10
+ # With FastAPI support
11
+ pip install "argos[fastapi]"
12
+ ```
13
+
14
+ ## Quick start with FastAPI
15
+
16
+ ```python
17
+ from fastapi import FastAPI, Request
18
+ from argos.adapters.fastapi import Argos, operation
19
+
20
+ app = FastAPI()
21
+
22
+ argos = Argos(
23
+ app,
24
+ authorizer_url="http://localhost:4000/api",
25
+ app_id="your-app-uuid",
26
+ secret="your-app-secret",
27
+ )
28
+
29
+
30
+ @app.get("/public")
31
+ @operation(id="public.info", level="public")
32
+ async def public_info():
33
+ """Anyone can call this — no token needed."""
34
+ return {"message": "Hello, world!"}
35
+
36
+
37
+ @app.get("/items")
38
+ @operation(id="items.list", level="private")
39
+ async def list_items(request: Request):
40
+ """Requires any authenticated user (token with non-empty groups)."""
41
+ user = request.state.user # TokenClaims
42
+ return {"user": user.sub, "items": []}
43
+
44
+
45
+ @app.delete("/items/{item_id}")
46
+ @operation(id="items.delete", level="protected")
47
+ async def delete_item(item_id: str, request: Request):
48
+ """Requires a user whose group has explicit permission for items.delete."""
49
+ return {"deleted": item_id}
50
+ ```
51
+
52
+ ## Standalone client
53
+
54
+ ```python
55
+ import asyncio
56
+ from argos import ArgosClient, OperationDescriptor
57
+
58
+ client = ArgosClient(
59
+ authorizer_url="http://localhost:4000/api",
60
+ app_id="your-app-uuid",
61
+ secret="your-app-secret",
62
+ )
63
+
64
+ client.register_operation(
65
+ OperationDescriptor(identifier="items.list", method="read", level="private")
66
+ )
67
+
68
+ async def main():
69
+ await client.startup()
70
+
71
+ # Validate a token from an incoming request
72
+ claims = await client.validate_token("<bearer-token>")
73
+
74
+ # Check whether the token has permission
75
+ allowed = client.check_permission(claims, "items.list", "read")
76
+ print(f"Allowed: {allowed}")
77
+
78
+ asyncio.run(main())
79
+ ```
80
+
81
+ ## Operation levels
82
+
83
+ | Level | Who can access |
84
+ |-------------|----------------|
85
+ | `public` | Everyone — no token required |
86
+ | `private` | Any authenticated user (token with at least one group) |
87
+ | `protected` | Only users whose groups intersect the operation's `allowed_groups` |
88
+
89
+ ## Method auto-detection
90
+
91
+ The `@operation` decorator infers the method from the function name:
92
+
93
+ | Function name prefix/keyword | Detected method |
94
+ |------------------------------|-----------------|
95
+ | `get_`, `list_`, `fetch_`, `read_` | `read` |
96
+ | `delete_`, `remove_`, `destroy_` | `delete` |
97
+ | contains `stream`, `subscribe`, `watch` | `stream` |
98
+ | anything else | `write` |
99
+
100
+ ## Configuration
101
+
102
+ | Parameter | Description |
103
+ |-----------|-------------|
104
+ | `authorizer_url` | Base URL of Argos, including the API prefix (e.g. `http://host/api`) |
105
+ | `app_id` | UUID of the app registered in `auth.apps` |
106
+ | `secret` | Plain-text app secret (never committed — use env vars) |
107
+
108
+ ```python
109
+ import os
110
+ from argos.adapters.fastapi import Argos
111
+
112
+ argos = Argos(
113
+ app,
114
+ authorizer_url=os.environ["AUTHORIZER_URL"],
115
+ app_id=os.environ["APP_ID"],
116
+ secret=os.environ["APP_SECRET"],
117
+ )
118
+ ```
@@ -0,0 +1,27 @@
1
+ """argos — Python client for the Argos authorizer."""
2
+
3
+ from .client import ArgosClient
4
+ from .decorators import operation
5
+ from .exceptions import (
6
+ ArgosAuthError,
7
+ ArgosConfigError,
8
+ ArgosError,
9
+ ArgosForbiddenError,
10
+ ArgosSyncError,
11
+ ArgosTokenError,
12
+ )
13
+ from .models import OperationDescriptor, OperationWithGroups, TokenClaims
14
+
15
+ __all__ = [
16
+ "ArgosClient",
17
+ "OperationDescriptor",
18
+ "OperationWithGroups",
19
+ "TokenClaims",
20
+ "ArgosError",
21
+ "ArgosAuthError",
22
+ "ArgosTokenError",
23
+ "ArgosForbiddenError",
24
+ "ArgosSyncError",
25
+ "ArgosConfigError",
26
+ "operation",
27
+ ]
@@ -0,0 +1 @@
1
+ # argos adapters
@@ -0,0 +1,145 @@
1
+ """Django adapter for argos."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any, Callable
7
+
8
+ from ..client import ArgosClient
9
+ from ..decorators import get_registry, operation # re-export
10
+ from ..exceptions import ArgosTokenError
11
+ from ..models import TokenClaims
12
+
13
+ __all__ = ["ArgosMiddleware", "operation"]
14
+
15
+
16
+ def _extract_token(request: Any) -> str:
17
+ """Extract Bearer token from HTTP_AUTHORIZATION."""
18
+ auth: str = request.META.get("HTTP_AUTHORIZATION", "")
19
+ scheme, _, token_str = auth.partition(" ")
20
+ return token_str.strip() if scheme.lower() == "bearer" else ""
21
+
22
+
23
+ def _operation_info(view_func: Any) -> tuple[str, str, str] | None:
24
+ """Return ``(identifier, op_method, level)`` from view metadata, or ``None``."""
25
+ # Try the function itself, then __wrapped__ (functools.wraps)
26
+ for candidate in (view_func, getattr(view_func, "__wrapped__", None)):
27
+ if candidate is None:
28
+ continue
29
+ identifier = getattr(candidate, "__argos_id__", None)
30
+ op_method = getattr(candidate, "__argos_method__", None)
31
+ level = getattr(candidate, "__argos_level__", None)
32
+ if identifier and op_method and level:
33
+ return (identifier, op_method, level)
34
+ return None
35
+
36
+
37
+ class ArgosMiddleware:
38
+ """Django WSGI middleware for argos.
39
+
40
+ Uses Django's ``process_view`` hook — which runs *after* URL resolution
41
+ and receives the matched view function directly — so ``@operation``
42
+ metadata is always available regardless of middleware order.
43
+
44
+ Add to ``MIDDLEWARE`` in ``settings.py``::
45
+
46
+ MIDDLEWARE = [
47
+ ...
48
+ "argos.adapters.django.ArgosMiddleware",
49
+ ...
50
+ ]
51
+
52
+ Required Django settings::
53
+
54
+ ARGOS_AUTHORIZER_URL = "http://localhost:4000/api"
55
+ ARGOS_APP_ID = "<uuid>"
56
+ ARGOS_SECRET = "<secret>"
57
+
58
+ Mark views with :func:`argos.decorators.operation`::
59
+
60
+ from django.http import JsonResponse
61
+ from argos.adapters.django import operation
62
+
63
+ @operation(id="items.list", level="private")
64
+ def list_items(request):
65
+ user = request.argos_user # TokenClaims
66
+ return JsonResponse({"user": user.sub})
67
+
68
+ Args:
69
+ get_response: The next middleware or view callable (injected by Django).
70
+ """
71
+
72
+ def __init__(self, get_response: Callable[[Any], Any]) -> None:
73
+ from django.conf import settings
74
+
75
+ authorizer_url: str = getattr(settings, "ARGOS_AUTHORIZER_URL", "")
76
+ realm_id: str = getattr(settings, "ARGOS_REALM_ID", "")
77
+ app_id: str = getattr(settings, "ARGOS_APP_ID", "")
78
+ secret: str = getattr(settings, "ARGOS_SECRET", "")
79
+
80
+ if not (authorizer_url and realm_id and app_id and secret):
81
+ raise RuntimeError(
82
+ "argos requires ARGOS_AUTHORIZER_URL, ARGOS_REALM_ID, "
83
+ "ARGOS_APP_ID and ARGOS_SECRET in Django settings."
84
+ )
85
+
86
+ self.get_response = get_response
87
+ self.client = ArgosClient(authorizer_url, realm_id, app_id, secret)
88
+
89
+ for op in get_registry():
90
+ self.client.register_operation(op)
91
+ asyncio.run(self.client.startup())
92
+
93
+ def __call__(self, request: Any) -> Any:
94
+ """Pass the request through — auth is enforced in process_view."""
95
+ return self.get_response(request)
96
+
97
+ def process_view(
98
+ self,
99
+ request: Any,
100
+ view_func: Any,
101
+ view_args: Any,
102
+ view_kwargs: Any,
103
+ ) -> Any | None:
104
+ """Enforce authentication and permissions before the view runs.
105
+
106
+ Called by Django after URL resolution. Returns ``None`` to let the
107
+ view proceed, or an :class:`~django.http.HttpResponse` to
108
+ short-circuit the request.
109
+ """
110
+ from django.http import JsonResponse
111
+
112
+ op_info = _operation_info(view_func)
113
+
114
+ # No argos annotation — pass through
115
+ if op_info is None:
116
+ return None
117
+
118
+ identifier, op_method, level = op_info
119
+
120
+ # Public — no token required
121
+ if level == "public":
122
+ return None
123
+
124
+ token_str = _extract_token(request)
125
+
126
+ if not token_str:
127
+ return JsonResponse(
128
+ {"error": "unauthorized", "detail": "Bearer token required."}, status=401
129
+ )
130
+
131
+ # Validate token (sync wrapper)
132
+ try:
133
+ claims: TokenClaims = asyncio.run(self.client.validate_token(token_str))
134
+ except ArgosTokenError as exc:
135
+ return JsonResponse({"error": "unauthorized", "detail": str(exc)}, status=401)
136
+
137
+ # Check permission
138
+ if not self.client.check_permission(claims, identifier, op_method):
139
+ return JsonResponse(
140
+ {"error": "forbidden", "detail": "Insufficient permissions."}, status=403
141
+ )
142
+
143
+ # Inject claims — available in views as request.argos_user
144
+ request.argos_user = claims
145
+ return None