jengolabs-auth 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.
@@ -0,0 +1,11 @@
1
+ node_modules
2
+ dist
3
+ .next
4
+ .turbo
5
+ .env
6
+ .env.local
7
+ .env.*.local
8
+ *.log
9
+ .DS_Store
10
+ coverage
11
+ .vercel
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: jengolabs-auth
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Jengo Auth — session verification, FastAPI middleware, and role-based access control
5
+ Project-URL: Repository, https://github.com/jengolabs/jen-auth
6
+ Project-URL: Documentation, https://github.com/jengolabs/jen-auth/blob/main/docs/dx/DX.md
7
+ Author: Jengo Labs
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Framework :: FastAPI
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: httpx>=0.27.0
21
+ Requires-Dist: pydantic>=2.0.0
22
+ Provides-Extra: all
23
+ Requires-Dist: fastapi>=0.100.0; extra == 'all'
24
+ Provides-Extra: dev
25
+ Requires-Dist: fastapi>=0.100.0; extra == 'dev'
26
+ Requires-Dist: mypy>=1.13.0; extra == 'dev'
27
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.8.0; extra == 'dev'
30
+ Requires-Dist: uvicorn>=0.30.0; extra == 'dev'
31
+ Provides-Extra: fastapi
32
+ Requires-Dist: fastapi>=0.100.0; extra == 'fastapi'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # jengolabs-auth
36
+
37
+ Python SDK for [Jengo Auth](https://github.com/jengolabs/jen-auth) — session verification, FastAPI middleware, and role-based access control.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install jengolabs-auth[fastapi]
43
+ ```
44
+
45
+ ## Quick Start (FastAPI)
46
+
47
+ ### Option 1: Dependency Injection
48
+
49
+ ```python
50
+ from fastapi import FastAPI, Depends
51
+ from jengolabs_auth import AuthClient, AuthClientConfig
52
+ from jengolabs_auth.fastapi import require_auth, require_app_role
53
+
54
+ auth_client = AuthClient(AuthClientConfig(
55
+ auth_server_url="http://localhost:4000",
56
+ tenant_api_key="your-tenant-api-key",
57
+ app_slug="my-service",
58
+ cache_ttl_seconds=30,
59
+ ))
60
+
61
+ app = FastAPI()
62
+
63
+ @app.get("/api/me")
64
+ async def get_me(session=require_auth(auth_client)):
65
+ return {"id": session.user.id, "email": session.user.email}
66
+
67
+ @app.delete("/api/resource/{resource_id}")
68
+ async def delete_resource(
69
+ resource_id: str,
70
+ session=require_app_role("admin", auth_client=auth_client),
71
+ ):
72
+ return {"deleted": resource_id}
73
+ ```
74
+
75
+ ### Option 2: Middleware
76
+
77
+ ```python
78
+ from fastapi import FastAPI, Request
79
+ from jengolabs_auth import AuthClient, AuthClientConfig
80
+ from jengolabs_auth.fastapi import JengoAuthMiddleware
81
+
82
+ auth_client = AuthClient(AuthClientConfig(
83
+ auth_server_url="http://localhost:4000",
84
+ tenant_api_key="your-tenant-api-key",
85
+ app_slug="my-service",
86
+ ))
87
+
88
+ app = FastAPI()
89
+ app.add_middleware(
90
+ JengoAuthMiddleware,
91
+ auth_client=auth_client,
92
+ public_paths=["/health", "/docs", "/openapi.json"],
93
+ )
94
+
95
+ @app.get("/api/me")
96
+ async def get_me(request: Request):
97
+ user = request.state.auth_user
98
+ return {"id": user.id, "email": user.email}
99
+ ```
100
+
101
+ ### Option 3: Manual Verification
102
+
103
+ ```python
104
+ from jengolabs_auth import AuthClient, AuthClientConfig, AuthenticationError
105
+
106
+ auth_client = AuthClient(AuthClientConfig(
107
+ auth_server_url="http://localhost:4000",
108
+ tenant_api_key="your-tenant-api-key",
109
+ app_slug="my-service",
110
+ ))
111
+
112
+ async def verify_token(token: str):
113
+ try:
114
+ session = await auth_client.verify_session(f"Bearer {token}")
115
+ return session.user
116
+ except AuthenticationError:
117
+ return None
118
+ ```
119
+
120
+ ## Models
121
+
122
+ All models are Pydantic v2 and support both snake_case and camelCase fields:
123
+
124
+ | Model | Fields |
125
+ |---|---|
126
+ | `AuthUser` | id, email, name, email_verified, image, role, created_at, updated_at |
127
+ | `AuthSessionData` | id, token, expires_at |
128
+ | `AuthAppGrant` | app_slug, role, granted_at |
129
+ | `AuthSessionWithGrant` | user, session, app_grant, organization |
130
+
131
+ ## Session Caching
132
+
133
+ Enable in-memory caching to reduce auth server calls:
134
+
135
+ ```python
136
+ auth_client = AuthClient(AuthClientConfig(
137
+ auth_server_url="http://localhost:4000",
138
+ tenant_api_key="your-tenant-api-key",
139
+ app_slug="my-service",
140
+ cache_ttl_seconds=30, # cache sessions for 30 seconds
141
+ ))
142
+ ```
143
+
144
+ ## License
145
+
146
+ MIT
@@ -0,0 +1,112 @@
1
+ # jengolabs-auth
2
+
3
+ Python SDK for [Jengo Auth](https://github.com/jengolabs/jen-auth) — session verification, FastAPI middleware, and role-based access control.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install jengolabs-auth[fastapi]
9
+ ```
10
+
11
+ ## Quick Start (FastAPI)
12
+
13
+ ### Option 1: Dependency Injection
14
+
15
+ ```python
16
+ from fastapi import FastAPI, Depends
17
+ from jengolabs_auth import AuthClient, AuthClientConfig
18
+ from jengolabs_auth.fastapi import require_auth, require_app_role
19
+
20
+ auth_client = AuthClient(AuthClientConfig(
21
+ auth_server_url="http://localhost:4000",
22
+ tenant_api_key="your-tenant-api-key",
23
+ app_slug="my-service",
24
+ cache_ttl_seconds=30,
25
+ ))
26
+
27
+ app = FastAPI()
28
+
29
+ @app.get("/api/me")
30
+ async def get_me(session=require_auth(auth_client)):
31
+ return {"id": session.user.id, "email": session.user.email}
32
+
33
+ @app.delete("/api/resource/{resource_id}")
34
+ async def delete_resource(
35
+ resource_id: str,
36
+ session=require_app_role("admin", auth_client=auth_client),
37
+ ):
38
+ return {"deleted": resource_id}
39
+ ```
40
+
41
+ ### Option 2: Middleware
42
+
43
+ ```python
44
+ from fastapi import FastAPI, Request
45
+ from jengolabs_auth import AuthClient, AuthClientConfig
46
+ from jengolabs_auth.fastapi import JengoAuthMiddleware
47
+
48
+ auth_client = AuthClient(AuthClientConfig(
49
+ auth_server_url="http://localhost:4000",
50
+ tenant_api_key="your-tenant-api-key",
51
+ app_slug="my-service",
52
+ ))
53
+
54
+ app = FastAPI()
55
+ app.add_middleware(
56
+ JengoAuthMiddleware,
57
+ auth_client=auth_client,
58
+ public_paths=["/health", "/docs", "/openapi.json"],
59
+ )
60
+
61
+ @app.get("/api/me")
62
+ async def get_me(request: Request):
63
+ user = request.state.auth_user
64
+ return {"id": user.id, "email": user.email}
65
+ ```
66
+
67
+ ### Option 3: Manual Verification
68
+
69
+ ```python
70
+ from jengolabs_auth import AuthClient, AuthClientConfig, AuthenticationError
71
+
72
+ auth_client = AuthClient(AuthClientConfig(
73
+ auth_server_url="http://localhost:4000",
74
+ tenant_api_key="your-tenant-api-key",
75
+ app_slug="my-service",
76
+ ))
77
+
78
+ async def verify_token(token: str):
79
+ try:
80
+ session = await auth_client.verify_session(f"Bearer {token}")
81
+ return session.user
82
+ except AuthenticationError:
83
+ return None
84
+ ```
85
+
86
+ ## Models
87
+
88
+ All models are Pydantic v2 and support both snake_case and camelCase fields:
89
+
90
+ | Model | Fields |
91
+ |---|---|
92
+ | `AuthUser` | id, email, name, email_verified, image, role, created_at, updated_at |
93
+ | `AuthSessionData` | id, token, expires_at |
94
+ | `AuthAppGrant` | app_slug, role, granted_at |
95
+ | `AuthSessionWithGrant` | user, session, app_grant, organization |
96
+
97
+ ## Session Caching
98
+
99
+ Enable in-memory caching to reduce auth server calls:
100
+
101
+ ```python
102
+ auth_client = AuthClient(AuthClientConfig(
103
+ auth_server_url="http://localhost:4000",
104
+ tenant_api_key="your-tenant-api-key",
105
+ app_slug="my-service",
106
+ cache_ttl_seconds=30, # cache sessions for 30 seconds
107
+ ))
108
+ ```
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,24 @@
1
+ from jengolabs_auth.client import AuthClient, AuthClientConfig
2
+ from jengolabs_auth.errors import AuthenticationError, AuthorizationError, AuthTransportError
3
+ from jengolabs_auth.models import (
4
+ AuthAppGrant,
5
+ AuthOrganization,
6
+ AuthSession,
7
+ AuthSessionData,
8
+ AuthSessionWithGrant,
9
+ AuthUser,
10
+ )
11
+
12
+ __all__ = [
13
+ "AuthClient",
14
+ "AuthClientConfig",
15
+ "AuthenticationError",
16
+ "AuthTransportError",
17
+ "AuthorizationError",
18
+ "AuthAppGrant",
19
+ "AuthOrganization",
20
+ "AuthSession",
21
+ "AuthSessionData",
22
+ "AuthSessionWithGrant",
23
+ "AuthUser",
24
+ ]
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from dataclasses import dataclass, field
5
+ from typing import Callable
6
+
7
+ import httpx
8
+
9
+ from jengolabs_auth.errors import AuthenticationError, AuthTransportError
10
+ from jengolabs_auth.models import AuthSessionWithGrant
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class AuthClientConfig:
15
+ auth_server_url: str
16
+ tenant_api_key: str
17
+ app_slug: str
18
+ cache_ttl_seconds: float = 0
19
+ timeout_seconds: float = 5.0
20
+ on_error: Callable[[Exception], None] | None = None
21
+
22
+
23
+ @dataclass
24
+ class _CacheEntry:
25
+ session: AuthSessionWithGrant
26
+ expires_at: float
27
+
28
+
29
+ class AuthClient:
30
+ def __init__(self, config: AuthClientConfig) -> None:
31
+ self._config = config
32
+ self._cache: dict[str, _CacheEntry] = {}
33
+ self._http = httpx.AsyncClient(
34
+ base_url=config.auth_server_url,
35
+ timeout=config.timeout_seconds,
36
+ )
37
+
38
+ async def verify_session(self, bearer_token_or_cookie: str) -> AuthSessionWithGrant:
39
+ cached = self._get_from_cache(bearer_token_or_cookie)
40
+ if cached is not None:
41
+ return cached
42
+
43
+ is_bearer = bearer_token_or_cookie.startswith("Bearer ")
44
+ headers: dict[str, str] = {
45
+ "X-Tenant-Key": self._config.tenant_api_key,
46
+ "X-App-Slug": self._config.app_slug,
47
+ }
48
+
49
+ if is_bearer:
50
+ headers["Authorization"] = bearer_token_or_cookie
51
+ else:
52
+ headers["Cookie"] = bearer_token_or_cookie
53
+
54
+ try:
55
+ response = await self._http.get("/api/auth/session", headers=headers)
56
+ except httpx.HTTPError as exc:
57
+ error = AuthTransportError()
58
+ if self._config.on_error:
59
+ self._config.on_error(error)
60
+ raise error from exc
61
+
62
+ if response.status_code != 200:
63
+ error = AuthenticationError("Invalid or expired session")
64
+ if self._config.on_error:
65
+ self._config.on_error(error)
66
+ raise error
67
+
68
+ session = AuthSessionWithGrant.model_validate(response.json())
69
+ self._set_cache(bearer_token_or_cookie, session)
70
+ return session
71
+
72
+ def clear_cache(self) -> None:
73
+ self._cache.clear()
74
+
75
+ async def close(self) -> None:
76
+ await self._http.aclose()
77
+
78
+ async def __aenter__(self) -> AuthClient:
79
+ return self
80
+
81
+ async def __aexit__(self, *_: object) -> None:
82
+ await self.close()
83
+
84
+ def _get_from_cache(self, key: str) -> AuthSessionWithGrant | None:
85
+ if self._config.cache_ttl_seconds <= 0:
86
+ return None
87
+
88
+ entry = self._cache.get(key)
89
+ if entry is None:
90
+ return None
91
+
92
+ if time.monotonic() > entry.expires_at:
93
+ del self._cache[key]
94
+ return None
95
+
96
+ return entry.session
97
+
98
+ def _set_cache(self, key: str, session: AuthSessionWithGrant) -> None:
99
+ if self._config.cache_ttl_seconds <= 0:
100
+ return
101
+
102
+ self._cache[key] = _CacheEntry(
103
+ session=session,
104
+ expires_at=time.monotonic() + self._config.cache_ttl_seconds,
105
+ )
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class AuthenticationError(Exception):
5
+ status_code = 401
6
+
7
+ def __init__(self, message: str = "Authentication required") -> None:
8
+ self.message = message
9
+ super().__init__(self.message)
10
+
11
+
12
+ class AuthTransportError(AuthenticationError):
13
+ """Raised when the auth server cannot be reached (network / timeout).
14
+
15
+ Distinct from AuthenticationError so callers can return 503 instead
16
+ of 401 and avoid leaking connectivity details to clients.
17
+ """
18
+
19
+ status_code = 503
20
+
21
+ def __init__(self, message: str = "Authentication service unavailable") -> None:
22
+ super().__init__(message)
23
+
24
+
25
+ class AuthorizationError(Exception):
26
+ status_code = 403
27
+
28
+ def __init__(self, message: str = "Insufficient permissions") -> None:
29
+ self.message = message
30
+ super().__init__(self.message)
@@ -0,0 +1,13 @@
1
+ from jengolabs_auth.fastapi.dependencies import (
2
+ AuthDependencies,
3
+ require_app_role,
4
+ require_auth,
5
+ )
6
+ from jengolabs_auth.fastapi.middleware import JengoAuthMiddleware
7
+
8
+ __all__ = [
9
+ "AuthDependencies",
10
+ "JengoAuthMiddleware",
11
+ "require_app_role",
12
+ "require_auth",
13
+ ]
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable
4
+
5
+ from fastapi import Depends, Request
6
+ from fastapi.exceptions import HTTPException
7
+
8
+ from jengolabs_auth.client import AuthClient
9
+ from jengolabs_auth.errors import AuthenticationError, AuthorizationError, AuthTransportError
10
+ from jengolabs_auth.models import AuthSessionWithGrant
11
+
12
+
13
+ class AuthDependencies:
14
+ def __init__(self, auth_client: AuthClient) -> None:
15
+ self._auth_client = auth_client
16
+
17
+ async def get_session(self, request: Request) -> AuthSessionWithGrant:
18
+ credential = _extract_credential(request)
19
+ if credential is None:
20
+ raise HTTPException(status_code=401, detail="Authentication required")
21
+
22
+ try:
23
+ session = await self._auth_client.verify_session(credential)
24
+ except AuthTransportError as exc:
25
+ raise HTTPException(
26
+ status_code=503, detail="Authentication service unavailable"
27
+ ) from exc
28
+ except AuthenticationError as exc:
29
+ raise HTTPException(status_code=401, detail="Invalid or expired session") from exc
30
+
31
+ if session.app_grant is None:
32
+ raise HTTPException(status_code=403, detail="Access denied for this application")
33
+
34
+ return session
35
+
36
+ @property
37
+ def dependency(self) -> Callable[..., AuthSessionWithGrant]:
38
+ return self.get_session
39
+
40
+
41
+ def require_auth(auth_client: AuthClient) -> Callable[..., AuthSessionWithGrant]:
42
+ deps = AuthDependencies(auth_client)
43
+ return Depends(deps.get_session)
44
+
45
+
46
+ def require_app_role(
47
+ *allowed_roles: str,
48
+ auth_client: AuthClient,
49
+ ) -> Callable[..., AuthSessionWithGrant]:
50
+ deps = AuthDependencies(auth_client)
51
+
52
+ async def _check_role(
53
+ request: Request,
54
+ ) -> AuthSessionWithGrant:
55
+ session = await deps.get_session(request)
56
+
57
+ if session.app_grant is None or session.app_grant.role not in allowed_roles:
58
+ raise HTTPException(
59
+ status_code=403,
60
+ detail=f"Required role: {' or '.join(allowed_roles)}",
61
+ )
62
+
63
+ return session
64
+
65
+ return Depends(_check_role)
66
+
67
+
68
+ def _extract_credential(request: Request) -> str | None:
69
+ auth_header = request.headers.get("Authorization")
70
+ if auth_header and auth_header.startswith("Bearer "):
71
+ return auth_header
72
+
73
+ cookie_header = request.headers.get("Cookie")
74
+ if cookie_header:
75
+ return cookie_header
76
+
77
+ return None
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
4
+ from starlette.requests import Request
5
+ from starlette.responses import JSONResponse, Response
6
+
7
+ from jengolabs_auth.client import AuthClient
8
+ from jengolabs_auth.errors import AuthenticationError
9
+
10
+
11
+ class JengoAuthMiddleware(BaseHTTPMiddleware):
12
+ def __init__(
13
+ self,
14
+ app: object,
15
+ auth_client: AuthClient,
16
+ public_paths: list[str] | None = None,
17
+ ) -> None:
18
+ super().__init__(app) # type: ignore[arg-type]
19
+ self._auth_client = auth_client
20
+ self._public_paths = public_paths or []
21
+
22
+ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
23
+ if self._is_public(request.url.path):
24
+ return await call_next(request)
25
+
26
+ credential = self._extract_credential(request)
27
+ if credential is None:
28
+ return JSONResponse({"error": "Authentication required"}, status_code=401)
29
+
30
+ try:
31
+ session = await self._auth_client.verify_session(credential)
32
+ except AuthenticationError:
33
+ return JSONResponse({"error": "Invalid or expired session"}, status_code=401)
34
+
35
+ if session.app_grant is None:
36
+ return JSONResponse({"error": "Access denied for this application"}, status_code=403)
37
+
38
+ request.state.auth_user = session.user
39
+ request.state.auth_session = session.session
40
+ request.state.auth_app_grant = session.app_grant
41
+ request.state.auth_organization = session.organization
42
+
43
+ return await call_next(request)
44
+
45
+ def _is_public(self, path: str) -> bool:
46
+ return any(path.startswith(p) for p in self._public_paths)
47
+
48
+ @staticmethod
49
+ def _extract_credential(request: Request) -> str | None:
50
+ auth_header = request.headers.get("Authorization")
51
+ if auth_header and auth_header.startswith("Bearer "):
52
+ return auth_header
53
+
54
+ cookie_header = request.headers.get("Cookie")
55
+ if cookie_header:
56
+ return cookie_header
57
+
58
+ return None
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class AuthUser(BaseModel):
7
+ id: str
8
+ email: str
9
+ name: str
10
+ email_verified: bool = Field(alias="emailVerified")
11
+ image: str | None = None
12
+ role: str | None = None
13
+ created_at: str = Field(alias="createdAt")
14
+ updated_at: str = Field(alias="updatedAt")
15
+
16
+ model_config = {"populate_by_name": True}
17
+
18
+
19
+ class AuthSessionData(BaseModel):
20
+ id: str
21
+ token: str
22
+ expires_at: str = Field(alias="expiresAt")
23
+
24
+ model_config = {"populate_by_name": True}
25
+
26
+
27
+ class AuthAppGrant(BaseModel):
28
+ app_slug: str = Field(alias="appSlug")
29
+ role: str
30
+ granted_at: str = Field(alias="grantedAt")
31
+
32
+ model_config = {"populate_by_name": True}
33
+
34
+
35
+ class AuthOrganizationInfo(BaseModel):
36
+ id: str
37
+ name: str
38
+ role: str
39
+
40
+
41
+ class AuthSession(BaseModel):
42
+ user: AuthUser
43
+ session: AuthSessionData
44
+
45
+
46
+ class AuthSessionWithGrant(AuthSession):
47
+ app_grant: AuthAppGrant | None = Field(default=None, alias="appGrant")
48
+ organization: AuthOrganizationInfo | None = None
49
+
50
+ model_config = {"populate_by_name": True}
51
+
52
+
53
+ class AuthOrganization(BaseModel):
54
+ id: str
55
+ name: str
56
+ slug: str
57
+ role: str
58
+ members: list[OrganizationMember]
59
+
60
+
61
+ class OrganizationMember(BaseModel):
62
+ user_id: str = Field(alias="userId")
63
+ role: str
64
+
65
+ model_config = {"populate_by_name": True}
66
+
67
+
68
+ AuthOrganization.model_rebuild()
File without changes
@@ -0,0 +1,63 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "jengolabs-auth"
7
+ version = "0.1.0"
8
+ description = "Python SDK for Jengo Auth — session verification, FastAPI middleware, and role-based access control"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Jengo Labs" }]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Framework :: FastAPI",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = [
26
+ "httpx>=0.27.0",
27
+ "pydantic>=2.0.0",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ fastapi = ["fastapi>=0.100.0"]
32
+ all = ["fastapi>=0.100.0"]
33
+ dev = [
34
+ "fastapi>=0.100.0",
35
+ "uvicorn>=0.30.0",
36
+ "pytest>=8.0.0",
37
+ "pytest-asyncio>=0.24.0",
38
+ "ruff>=0.8.0",
39
+ "mypy>=1.13.0",
40
+ ]
41
+
42
+ [project.urls]
43
+ Repository = "https://github.com/jengolabs/jen-auth"
44
+ Documentation = "https://github.com/jengolabs/jen-auth/blob/main/docs/dx/DX.md"
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["jengolabs_auth"]
48
+
49
+ [tool.ruff]
50
+ target-version = "py310"
51
+ line-length = 100
52
+
53
+ [tool.ruff.lint]
54
+ select = ["E", "F", "I", "UP", "B", "SIM", "TCH"]
55
+
56
+ [tool.mypy]
57
+ python_version = "3.10"
58
+ strict = true
59
+ warn_return_any = true
60
+ warn_unused_configs = true
61
+
62
+ [tool.pytest.ini_options]
63
+ asyncio_mode = "auto"