fastapi-identity-model 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,166 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
130
+
131
+ # PyCharm
132
+ .idea
133
+
134
+ *.env*
135
+ # Allow test fixture env files (no secrets — local Docker only)
136
+ !.env.node-oidc
137
+ *.crt
138
+ *.key
139
+ *.pfx
140
+ .secrets.baseline
141
+
142
+ # Claude Code runtime state
143
+ .claude/mcp.json
144
+ .claude/worktrees/
145
+ .claude/task-state.md
146
+ .claude/task-state*.md
147
+ .claude/review-*.md
148
+ .claude/review-*.patch
149
+ .claude/pre-review-sha.txt
150
+
151
+ # Conformance plan export zips + per-test RP logs (binary evidence artifacts;
152
+ # preserved via CI upload-artifact, not committed)
153
+ conformance/results/hosted/*.zip
154
+ conformance/results/hosted/rp-logs/
155
+
156
+ # Ralph orchestrator runtime state
157
+ .ralph/
158
+ PROMPT.md
159
+
160
+ # Terraform
161
+ infra/**/.terraform/
162
+ infra/**/*.tfstate*
163
+ infra/**/*.tfstate.backup
164
+ infra/**/*.tfvars
165
+ infra/**/access_key.json
166
+ infra/**/expired_token.txt
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+
3
+ All notable changes to `fastapi-identity-model` are documented here. This project
4
+ adheres to [Semantic Versioning](https://semver.org/) and is released
5
+ independently of the core `py-identity-model` library.
6
+
7
+ ## 0.1.0 (2026-07-08)
8
+
9
+ Initial release. Extracted and hardened from the `py-identity-model` FastAPI
10
+ example, then driven through the OIDF conformance suite: the RP router passes
11
+ the same local Basic RP, Config RP, and Form Post RP plans the core library is
12
+ certified against (13/13, 5/5, 13/13 — regression stage, see #437).
13
+
14
+ ### Added
15
+ - `TokenValidationMiddleware` — resource-server Bearer-token validation that
16
+ attaches a `ClaimsPrincipal` to `request.state`.
17
+ - `Depends` helpers: `get_current_user`, `get_claims`, `get_token`,
18
+ `get_claim_value`, `get_claim_values`, `require_claim`, `require_scope`.
19
+ - `build_oidc_router` — mountable relying-party login flow (authorization code +
20
+ PKCE) with state/nonce, ID-token validation, and UserInfo `sub` verification.
21
+ - `POST /callback` on the RP router — OAuth 2.0 `form_post` response mode,
22
+ sharing the exact GET validation path (parsed with stdlib `parse_qsl`; no
23
+ `python-multipart` dependency). Requires the session cookie to be issued
24
+ with `same_site="none"` in real browsers.
25
+ - `build_oidc_router(fetch_userinfo=False)` — skip the UserInfo round-trip and
26
+ anchor identity on the validated ID token.
27
+ - `OIDCSettings` — typed configuration with `from_env()`.
28
+ - `TokenManager` — access-token refresh lifecycle over the native async refresh grant.
29
+
30
+ ### Changed vs. the former example
31
+ - Middleware returns **503** for a discovery/JWKS/network fault and **500** for a
32
+ genuinely unexpected error, instead of masking either as a 401.
33
+ - `TokenManager` uses the async discovery + native `aio.refresh_token` grant
34
+ (removes a blocking sync call in async context and a hand-rolled token POST).
35
+
36
+ ### Security
37
+ - The RP router rejects a discovery document whose issuer does not match the
38
+ URL it was retrieved from (OIDC Discovery 1.0 §4.3 issuer mix-up defense).
39
+ - The RP router refuses to establish a session when the token response has no
40
+ ID token, and enforces the UserInfo `sub` match as a hard gate (a mismatch
41
+ fails the login instead of being swallowed).
42
+ - Transient login-flow state (state/nonce/PKCE verifier) is kept under a
43
+ separate session key from the identity and is single-use (popped on callback),
44
+ so an in-flight login is never read as an authenticated identity.
45
+ - `TokenValidationMiddleware` rejects an ID token presented as an access token,
46
+ requires a non-empty `audience` (a `None` audience skips `aud` enforcement for
47
+ aud-less tokens), lets CORS preflight through, and matches excluded subpaths.
48
+ - `POST /auth/logout` (was `GET`) so a cross-site request cannot force logout.
49
+ - `TokenManager` refreshes under an `asyncio.Lock` (no concurrent-refresh
50
+ token-family invalidation) and treats `expires_in=0` as already expired.
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-identity-model
3
+ Version: 0.1.0
4
+ Summary: FastAPI OIDC/OAuth2 middleware and relying-party router, built on py-identity-model
5
+ Project-URL: Homepage, https://github.com/jamescrowley321/py-identity-model
6
+ Project-URL: Repository, https://github.com/jamescrowley321/py-identity-model
7
+ Project-URL: Issues, https://github.com/jamescrowley321/py-identity-model/issues
8
+ Author-email: jamescrowley321 <jamescrowley151@gmail.com>
9
+ License-Expression: Apache-2.0
10
+ Keywords: authentication,fastapi,jwt,middleware,oauth2,oidc,openid-connect
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Framework :: FastAPI
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Security
20
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.12
23
+ Requires-Dist: fastapi>=0.115.0
24
+ Requires-Dist: itsdangerous>=2.0
25
+ Requires-Dist: py-identity-model>=3.1.0
26
+ Requires-Dist: starlette>=0.49.1
27
+ Provides-Extra: server
28
+ Requires-Dist: uvicorn[standard]>=0.23.0; extra == 'server'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # fastapi-identity-model
32
+
33
+ OIDC/OAuth2 integration for [FastAPI](https://fastapi.tiangolo.com/), built on
34
+ [`py-identity-model`](https://pypi.org/project/py-identity-model/) — an OpenID
35
+ Foundation–certified relying-party library.
36
+
37
+ It gives you two composable pieces:
38
+
39
+ | Piece | Use case |
40
+ |-------|----------|
41
+ | **`TokenValidationMiddleware`** + `Depends` helpers | Protect an **API / resource server** that receives `Authorization: Bearer <token>` |
42
+ | **`build_oidc_router`** | Add a browser **login flow** (authorization code + PKCE) to your app (relying party) |
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install fastapi-identity-model
48
+ # with a server for the demo:
49
+ pip install "fastapi-identity-model[server]"
50
+ ```
51
+
52
+ ## Resource server — validate incoming Bearer tokens
53
+
54
+ ```python
55
+ from fastapi import FastAPI, Depends
56
+ from fastapi_identity_model import (
57
+ TokenValidationMiddleware, OIDCSettings, get_current_user, require_scope,
58
+ )
59
+
60
+ settings = OIDCSettings.from_env() # OIDC_DISCOVERY_URL, OIDC_CLIENT_ID, OIDC_REDIRECT_URI, ...
61
+ app = FastAPI()
62
+ app.add_middleware(
63
+ TokenValidationMiddleware,
64
+ discovery_url=settings.discovery_url,
65
+ audience=settings.audience,
66
+ excluded_paths=settings.excluded_paths,
67
+ )
68
+
69
+ @app.get("/api/me")
70
+ async def me(user = Depends(get_current_user)):
71
+ return {"sub": user.identity.name, "authenticated": user.identity.is_authenticated}
72
+
73
+ @app.get("/api/data", dependencies=[Depends(require_scope("api.read"))])
74
+ async def data():
75
+ return {"data": "protected"}
76
+ ```
77
+
78
+ The middleware validates the token (signature, issuer, audience, expiry) via
79
+ `py-identity-model` and attaches a `ClaimsPrincipal` to `request.state.user`.
80
+ Invalid tokens → **401**; an unexpected server-side failure → **500** (never
81
+ masked as a 401).
82
+
83
+ ## Relying party — browser login flow
84
+
85
+ ```python
86
+ import os
87
+
88
+ from fastapi import FastAPI, Request
89
+ from starlette.middleware.sessions import SessionMiddleware
90
+ from fastapi_identity_model import OIDCSettings, build_oidc_router
91
+
92
+ settings = OIDCSettings(
93
+ discovery_url="https://api.descope.com/v1/apps/<project_id>/.well-known/openid-configuration",
94
+ client_id="<client_id>",
95
+ redirect_uri="http://localhost:8000/auth/callback",
96
+ scope="openid profile email",
97
+ )
98
+
99
+ app = FastAPI()
100
+ # Use a strong secret from the environment — never a committed literal, or
101
+ # anyone can forge a session cookie. same_site="lax" is required so the
102
+ # provider's redirect back to /auth/callback carries the session cookie.
103
+ app.add_middleware(
104
+ SessionMiddleware,
105
+ secret_key=os.environ["SESSION_SECRET"],
106
+ same_site="lax",
107
+ https_only=True, # behind TLS
108
+ )
109
+ app.include_router(build_oidc_router(settings), prefix="/auth")
110
+
111
+ @app.get("/me")
112
+ async def me(request: Request):
113
+ return request.session.get("oidc", {}) # {"sub", "claims", "userinfo"} after login
114
+ ```
115
+
116
+ Routes added: `GET /auth/login` → provider, `GET /auth/callback` (code exchange,
117
+ ID-token validation, **nonce check**, UserInfo `sub` verification), `POST /auth/logout`.
118
+
119
+ ### Session & security
120
+
121
+ The router uses Starlette's `SessionMiddleware`, which **signs but does not
122
+ encrypt** the cookie. Stored identity claims are tamper-proof but readable by the
123
+ client. Raw tokens are stored **only** when you pass `build_oidc_router(settings,
124
+ store_tokens=True)` — enable that only with an encrypted or server-side session
125
+ store. For production, back the session with a server-side store.
126
+
127
+ ## Token refresh
128
+
129
+ ```python
130
+ from fastapi_identity_model import TokenManager
131
+
132
+ tm = TokenManager(discovery_url=..., client_id=..., client_secret=...)
133
+ tm.set_tokens(access_token=..., refresh_token=..., expires_in=3600)
134
+ access = await tm.get_access_token() # auto-refreshes shortly before expiry
135
+ ```
136
+
137
+ ## License
138
+
139
+ Apache-2.0 — see the repository `LICENSE`.
@@ -0,0 +1,109 @@
1
+ # fastapi-identity-model
2
+
3
+ OIDC/OAuth2 integration for [FastAPI](https://fastapi.tiangolo.com/), built on
4
+ [`py-identity-model`](https://pypi.org/project/py-identity-model/) — an OpenID
5
+ Foundation–certified relying-party library.
6
+
7
+ It gives you two composable pieces:
8
+
9
+ | Piece | Use case |
10
+ |-------|----------|
11
+ | **`TokenValidationMiddleware`** + `Depends` helpers | Protect an **API / resource server** that receives `Authorization: Bearer <token>` |
12
+ | **`build_oidc_router`** | Add a browser **login flow** (authorization code + PKCE) to your app (relying party) |
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install fastapi-identity-model
18
+ # with a server for the demo:
19
+ pip install "fastapi-identity-model[server]"
20
+ ```
21
+
22
+ ## Resource server — validate incoming Bearer tokens
23
+
24
+ ```python
25
+ from fastapi import FastAPI, Depends
26
+ from fastapi_identity_model import (
27
+ TokenValidationMiddleware, OIDCSettings, get_current_user, require_scope,
28
+ )
29
+
30
+ settings = OIDCSettings.from_env() # OIDC_DISCOVERY_URL, OIDC_CLIENT_ID, OIDC_REDIRECT_URI, ...
31
+ app = FastAPI()
32
+ app.add_middleware(
33
+ TokenValidationMiddleware,
34
+ discovery_url=settings.discovery_url,
35
+ audience=settings.audience,
36
+ excluded_paths=settings.excluded_paths,
37
+ )
38
+
39
+ @app.get("/api/me")
40
+ async def me(user = Depends(get_current_user)):
41
+ return {"sub": user.identity.name, "authenticated": user.identity.is_authenticated}
42
+
43
+ @app.get("/api/data", dependencies=[Depends(require_scope("api.read"))])
44
+ async def data():
45
+ return {"data": "protected"}
46
+ ```
47
+
48
+ The middleware validates the token (signature, issuer, audience, expiry) via
49
+ `py-identity-model` and attaches a `ClaimsPrincipal` to `request.state.user`.
50
+ Invalid tokens → **401**; an unexpected server-side failure → **500** (never
51
+ masked as a 401).
52
+
53
+ ## Relying party — browser login flow
54
+
55
+ ```python
56
+ import os
57
+
58
+ from fastapi import FastAPI, Request
59
+ from starlette.middleware.sessions import SessionMiddleware
60
+ from fastapi_identity_model import OIDCSettings, build_oidc_router
61
+
62
+ settings = OIDCSettings(
63
+ discovery_url="https://api.descope.com/v1/apps/<project_id>/.well-known/openid-configuration",
64
+ client_id="<client_id>",
65
+ redirect_uri="http://localhost:8000/auth/callback",
66
+ scope="openid profile email",
67
+ )
68
+
69
+ app = FastAPI()
70
+ # Use a strong secret from the environment — never a committed literal, or
71
+ # anyone can forge a session cookie. same_site="lax" is required so the
72
+ # provider's redirect back to /auth/callback carries the session cookie.
73
+ app.add_middleware(
74
+ SessionMiddleware,
75
+ secret_key=os.environ["SESSION_SECRET"],
76
+ same_site="lax",
77
+ https_only=True, # behind TLS
78
+ )
79
+ app.include_router(build_oidc_router(settings), prefix="/auth")
80
+
81
+ @app.get("/me")
82
+ async def me(request: Request):
83
+ return request.session.get("oidc", {}) # {"sub", "claims", "userinfo"} after login
84
+ ```
85
+
86
+ Routes added: `GET /auth/login` → provider, `GET /auth/callback` (code exchange,
87
+ ID-token validation, **nonce check**, UserInfo `sub` verification), `POST /auth/logout`.
88
+
89
+ ### Session & security
90
+
91
+ The router uses Starlette's `SessionMiddleware`, which **signs but does not
92
+ encrypt** the cookie. Stored identity claims are tamper-proof but readable by the
93
+ client. Raw tokens are stored **only** when you pass `build_oidc_router(settings,
94
+ store_tokens=True)` — enable that only with an encrypted or server-side session
95
+ store. For production, back the session with a server-side store.
96
+
97
+ ## Token refresh
98
+
99
+ ```python
100
+ from fastapi_identity_model import TokenManager
101
+
102
+ tm = TokenManager(discovery_url=..., client_id=..., client_secret=...)
103
+ tm.set_tokens(access_token=..., refresh_token=..., expires_in=3600)
104
+ access = await tm.get_access_token() # auto-refreshes shortly before expiry
105
+ ```
106
+
107
+ ## License
108
+
109
+ Apache-2.0 — see the repository `LICENSE`.
@@ -0,0 +1,49 @@
1
+ """fastapi-identity-model: OIDC/OAuth2 integration for FastAPI.
2
+
3
+ Built on `py-identity-model`. Provides:
4
+
5
+ - ``TokenValidationMiddleware`` — validate incoming Bearer tokens (resource server).
6
+ - ``build_oidc_router`` — a mountable authorization-code + PKCE login flow (RP).
7
+ - ``Depends``-based helpers — ``get_current_user``, ``require_scope``, ``require_claim`` …
8
+ - ``TokenManager`` — access-token refresh lifecycle.
9
+ """
10
+
11
+ from importlib.metadata import PackageNotFoundError, version
12
+
13
+ from .config import OIDCSettings
14
+ from .dependencies import (
15
+ Claims,
16
+ CurrentUser,
17
+ get_claim_value,
18
+ get_claim_values,
19
+ get_claims,
20
+ get_current_user,
21
+ get_token,
22
+ require_claim,
23
+ require_scope,
24
+ )
25
+ from .middleware import TokenValidationMiddleware
26
+ from .rp import build_oidc_router
27
+ from .token_manager import TokenManager
28
+
29
+
30
+ try:
31
+ __version__ = version("fastapi-identity-model")
32
+ except PackageNotFoundError: # pragma: no cover - only during local, uninstalled use
33
+ __version__ = "0.0.0"
34
+
35
+ __all__ = [
36
+ "Claims",
37
+ "CurrentUser",
38
+ "OIDCSettings",
39
+ "TokenManager",
40
+ "TokenValidationMiddleware",
41
+ "build_oidc_router",
42
+ "get_claim_value",
43
+ "get_claim_values",
44
+ "get_claims",
45
+ "get_current_user",
46
+ "get_token",
47
+ "require_claim",
48
+ "require_scope",
49
+ ]
@@ -0,0 +1,83 @@
1
+ """Typed configuration for the FastAPI OIDC integration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ import os
7
+
8
+
9
+ def _default_excluded_paths() -> list[str]:
10
+ return ["/docs", "/openapi.json", "/health"]
11
+
12
+
13
+ @dataclass
14
+ class OIDCSettings:
15
+ """Configuration shared by the RP login router and the resource-server middleware.
16
+
17
+ Attributes:
18
+ discovery_url: OpenID Connect discovery document URL of the provider.
19
+ client_id: OAuth2 client identifier.
20
+ redirect_uri: Absolute callback URL registered with the provider
21
+ (must match the router's ``/callback`` route). Only required for the
22
+ login router; a resource-server-only deployment may leave it empty.
23
+ client_secret: Client secret; omit for public/PKCE clients.
24
+ scope: Space-delimited scopes requested at authorization.
25
+ audience: Expected ``aud`` for the resource-server middleware. Defaults
26
+ to ``client_id`` when not set.
27
+ post_login_redirect: Where ``/callback`` redirects after a successful login.
28
+ post_logout_redirect: Where ``/logout`` redirects after clearing the session.
29
+ excluded_paths: Paths the resource-server middleware skips (health/docs).
30
+ """
31
+
32
+ discovery_url: str
33
+ client_id: str
34
+ redirect_uri: str = ""
35
+ client_secret: str | None = None
36
+ scope: str = "openid profile email"
37
+ audience: str | None = None
38
+ post_login_redirect: str = "/"
39
+ post_logout_redirect: str = "/"
40
+ excluded_paths: list[str] = field(default_factory=_default_excluded_paths)
41
+
42
+ def __post_init__(self) -> None:
43
+ # Validate required fields are non-empty even on direct construction
44
+ # (from_env guards its own inputs, but OIDCSettings(...) did not).
45
+ for name in ("discovery_url", "client_id"):
46
+ if not getattr(self, name):
47
+ raise ValueError(f"OIDCSettings requires a non-empty {name}")
48
+ # The middleware validates the ID/access token audience; default it to
49
+ # the client_id, which is the audience Descope and most OPs mint.
50
+ if self.audience is None:
51
+ self.audience = self.client_id
52
+
53
+ @classmethod
54
+ def from_env(cls, prefix: str = "OIDC_") -> OIDCSettings:
55
+ """Build settings from environment variables (e.g. ``OIDC_DISCOVERY_URL``).
56
+
57
+ Required: ``{prefix}DISCOVERY_URL``, ``{prefix}CLIENT_ID``. ``REDIRECT_URI``
58
+ is required only for the login router; a resource-server-only deployment
59
+ may omit it. Others fall back to the dataclass defaults.
60
+ """
61
+
62
+ def _req(name: str) -> str:
63
+ value = os.environ.get(f"{prefix}{name}")
64
+ if not value:
65
+ raise ValueError(f"Missing required env var {prefix}{name}")
66
+ return value
67
+
68
+ excluded = os.environ.get(f"{prefix}EXCLUDED_PATHS")
69
+ return cls(
70
+ discovery_url=_req("DISCOVERY_URL"),
71
+ client_id=_req("CLIENT_ID"),
72
+ redirect_uri=os.environ.get(f"{prefix}REDIRECT_URI", ""),
73
+ client_secret=os.environ.get(f"{prefix}CLIENT_SECRET"),
74
+ scope=os.environ.get(f"{prefix}SCOPE", "openid profile email"),
75
+ audience=os.environ.get(f"{prefix}AUDIENCE"),
76
+ post_login_redirect=os.environ.get(f"{prefix}POST_LOGIN_REDIRECT", "/"),
77
+ post_logout_redirect=os.environ.get(f"{prefix}POST_LOGOUT_REDIRECT", "/"),
78
+ excluded_paths=(
79
+ [p.strip() for p in excluded.split(",") if p.strip()]
80
+ if excluded
81
+ else _default_excluded_paths()
82
+ ),
83
+ )