diff --git a/.gitignore b/.gitignore index 7c7d5be..d5ebff9 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,9 @@ env/ #Build files dist -docs +docs/_build/ +docs/reference/ +docs/_build/json/ #testfile server.py diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..054717b --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,22 @@ +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +.PHONY: help Makefile html clean + +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +html: + @$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +json: + @$(SPHINXBUILD) -b json "$(SOURCEDIR)" "$(BUILDDIR)/json" $(SPHINXOPTS) $(O) + +clean: + @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + rm -rf reference/ + +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/auth0.css b/docs/_static/auth0.css new file mode 100644 index 0000000..14f76fb --- /dev/null +++ b/docs/_static/auth0.css @@ -0,0 +1,263 @@ +/* ============================================================ + Auth0 brand theme for Sphinx / furo + ============================================================ */ + +/* --- Brand colors (light mode) ----------------------------- */ +:root { + --color-brand-primary: #9921FE; + --color-brand-content: #9921FE; + --color-api-name: #9921FE; + --color-api-pre-name: #9921FE; + --color-link: #9921FE; + --color-link--hover: #7A10D4; + + /* Sidebar: Auth0 dark navy */ + --color-sidebar-background: #16214A; + --color-sidebar-background-border: #1e2d5a; + --color-sidebar-brand-text: #FFFFFF; + --color-sidebar-caption-text: rgba(255,255,255,0.5); + --color-sidebar-link-text: rgba(255,255,255,0.82); + --color-sidebar-link-text--top-level: #BC6DFF; + --color-sidebar-item-background--hover: rgba(153,33,254,0.14); + --color-sidebar-item-background--current: rgba(153,33,254,0.22); + --color-sidebar-search-text: #FFFFFF; + --color-sidebar-search-background: rgba(255,255,255,0.06); + --color-sidebar-search-background--focus: rgba(255,255,255,0.10); + --color-sidebar-search-border: rgba(255,255,255,0.14); + --color-sidebar-search-icon: rgba(255,255,255,0.4); +} + +/* --- Brand colors (dark mode) ------------------------------ */ +body[data-theme="dark"] { + --color-brand-primary: #BC6DFF; + --color-brand-content: #BC6DFF; + --color-api-name: #BC6DFF; + --color-api-pre-name: #BC6DFF; + --color-link: #BC6DFF; + --color-link--hover: #9921FE; +} + +/* ============================================================ + Typography and spacing + ============================================================ */ + +.content-container { + max-width: 860px; +} + +article.bd-article, +div[role="main"] { + line-height: 1.7; + font-size: 1rem; +} + +/* Method/class entries: breathe vertically */ +dl.py { + margin-bottom: 2.5rem; +} + +dl.py + dl.py { + padding-top: 1rem; + border-top: 1px solid var(--color-background-border); +} + +/* ============================================================ + Method and function signature bars + ============================================================ */ + +dt.sig { + border-left: 4px solid var(--color-brand-primary); + background: var(--color-background-secondary); + padding: 0.6rem 0.9rem; + border-radius: 0 0.4rem 0.4rem 0; + font-size: 0.95rem; + margin-bottom: 0.75rem; + overflow-x: auto; +} + +/* Class/function name in signature */ +.sig-name.descname { + color: var(--color-brand-primary); + font-weight: 700; +} + +/* Mute "async" / "classmethod" / "property" property badges */ +em.property { + font-style: normal; + color: var(--color-foreground-secondary); + font-size: 0.82em; + margin-right: 0.25em; + font-weight: 400; +} + +/* Class/function body: indent cleanly */ +dl.py > dd { + padding-left: 1.25rem; + margin-left: 0; + border-left: 2px solid var(--color-background-border); + padding-top: 0.25rem; + padding-bottom: 0.5rem; +} + +/* ============================================================ + Pydantic-style parameter blocks + ============================================================ */ + +/* Outer card */ +dl.field-list { + border: 1px solid var(--color-background-border); + border-radius: 0.5rem; + overflow: hidden; + margin: 1.25rem 0 1.5rem; + font-size: 0.9rem; + box-shadow: 0 1px 3px rgba(0,0,0,0.05); +} + +body[data-theme="dark"] dl.field-list { + box-shadow: none; +} + +/* Section labels: "Parameters" / "Returns" / "Raises" */ +dl.field-list dt { + background: var(--color-background-secondary); + margin: 0; + padding: 0.4rem 0.9rem; + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--color-foreground-secondary); + border-bottom: 1px solid var(--color-background-border); +} + +/* Separator between adjacent sections */ +dl.field-list dd + dt { + border-top: 1px solid var(--color-background-border); +} + +/* Hide the rendered colon after labels */ +dl.field-list dt .colon { display: none; } + +/* Content area */ +dl.field-list dd { + margin: 0; + padding: 0; +} + +/* Single-entry Returns / Raises */ +dl.field-list dd > p { + margin: 0; + padding: 0.55rem 0.9rem; + line-height: 1.6; +} + +/* Multi-parameter list */ +dl.field-list dd > ul.simple { + list-style: none; + padding: 0; + margin: 0; +} + +/* Individual parameter row */ +dl.field-list dd > ul.simple > li { + border-bottom: 1px solid var(--color-background-border); + padding: 0.55rem 0.9rem; + line-height: 1.6; +} + +dl.field-list dd > ul.simple > li:last-child { + border-bottom: none; +} + +dl.field-list dd > ul.simple > li > p { + margin: 0; +} + +/* Parameter name: monospace, brand purple */ +dl.field-list strong { + font-family: var(--font-stack--monospace); + color: var(--color-brand-primary); + font-size: 0.92em; + font-weight: 600; +} + +/* Type annotation: smaller, muted */ +dl.field-list em { + font-style: normal; + font-size: 0.86em; + color: var(--color-foreground-secondary); +} + +/* ============================================================ + Attribute entries (class fields) + ============================================================ */ + +dl.py.attribute > dt.sig { + border-left-color: #7B8CCC; /* softer blue-purple for attributes vs methods */ +} + +dl.py.attribute > dd { + color: var(--color-foreground-secondary); + font-size: 0.93rem; + padding-top: 0.3rem; +} + +/* ============================================================ + Inline code and code blocks + ============================================================ */ + +code.docutils.literal { + background: var(--color-background-secondary); + border: 1px solid var(--color-background-border); + padding: 0.1em 0.35em; + border-radius: 3px; + font-size: 0.9em; +} + +pre { + border-radius: 0.5rem; + font-size: 0.88rem; + line-height: 1.6; +} + +/* ============================================================ + Section headings + ============================================================ */ + +h1 { + font-size: 1.9rem; + font-weight: 700; + border-bottom: 3px solid var(--color-brand-primary); + padding-bottom: 0.4rem; + margin-bottom: 1.25rem; +} + +h2 { + font-size: 1.35rem; + font-weight: 600; + border-bottom: 1px solid var(--color-background-border); + padding-bottom: 0.3rem; + margin-top: 2rem; + margin-bottom: 1rem; +} + +h3 { + font-size: 1.1rem; + font-weight: 600; + color: var(--color-foreground-secondary); + margin-top: 1.5rem; +} + +/* ============================================================ + Admonitions (note, warning, etc.) + ============================================================ */ + +div.admonition { + border-radius: 0.45rem; + border-left-width: 4px; + padding: 0.75rem 1rem; +} + +div.admonition.note { + border-left-color: var(--color-brand-primary); +} diff --git a/docs/_templates/autoapi/python/module.rst b/docs/_templates/autoapi/python/module.rst new file mode 100644 index 0000000..3938406 --- /dev/null +++ b/docs/_templates/autoapi/python/module.rst @@ -0,0 +1,157 @@ +{% if obj.display %} + {% if is_own_page %} +{% set title = obj.short_name | replace('_', ' ') | title %} +{{ title }} +{{ "=" * (title | length) }} + +.. py:module:: {{ obj.name }} + + {% if obj.docstring %} +.. autoapi-nested-parse:: + + {{ obj.docstring|indent(3) }} + + {% endif %} + + {% block submodules %} + {% set visible_subpackages = obj.subpackages|selectattr("display")|list %} + {% set visible_submodules = obj.submodules|selectattr("display")|list %} + {% set visible_submodules = (visible_subpackages + visible_submodules)|sort %} + {% if visible_submodules %} +Submodules +---------- + +.. toctree:: + :maxdepth: 1 + + {% for submodule in visible_submodules %} + {{ submodule.include_path }} + {% endfor %} + + + {% endif %} + {% endblock %} + {% block content %} + {% set visible_children = obj.children|selectattr("display")|list %} + {% if visible_children %} + {% set visible_attributes = visible_children|selectattr("type", "equalto", "data")|list %} + {% if visible_attributes %} + {% if "attribute" in own_page_types or "show-module-summary" in autoapi_options %} +Attributes +---------- + + {% if "attribute" in own_page_types %} +.. toctree:: + :hidden: + + {% for attribute in visible_attributes %} + {{ attribute.include_path }} + {% endfor %} + + {% endif %} +.. autoapisummary:: + + {% for attribute in visible_attributes %} + {{ attribute.id }} + {% endfor %} + {% endif %} + + + {% endif %} + {% set visible_exceptions = visible_children|selectattr("type", "equalto", "exception")|list %} + {% if visible_exceptions %} + {% if "exception" in own_page_types or "show-module-summary" in autoapi_options %} +Exceptions +---------- + + {% if "exception" in own_page_types %} +.. toctree:: + :hidden: + + {% for exception in visible_exceptions %} + {{ exception.include_path }} + {% endfor %} + + {% endif %} +.. autoapisummary:: + + {% for exception in visible_exceptions %} + {{ exception.id }} + {% endfor %} + {% endif %} + + + {% endif %} + {% set visible_classes = visible_children|selectattr("type", "equalto", "class")|list %} + {% if visible_classes %} + {% if "class" in own_page_types or "show-module-summary" in autoapi_options %} +Classes +------- + + {% if "class" in own_page_types %} +.. toctree:: + :hidden: + + {% for klass in visible_classes %} + {{ klass.include_path }} + {% endfor %} + + {% endif %} +.. autoapisummary:: + + {% for klass in visible_classes %} + {{ klass.id }} + {% endfor %} + {% endif %} + + + {% endif %} + {% set visible_functions = visible_children|selectattr("type", "equalto", "function")|list %} + {% if visible_functions %} + {% if "function" in own_page_types or "show-module-summary" in autoapi_options %} +Functions +--------- + + {% if "function" in own_page_types %} +.. toctree:: + :hidden: + + {% for function in visible_functions %} + {{ function.include_path }} + {% endfor %} + + {% endif %} +.. autoapisummary:: + + {% for function in visible_functions %} + {{ function.id }} + {% endfor %} + {% endif %} + + + {% endif %} + {% set this_page_children = visible_children|rejectattr("type", "in", own_page_types)|list %} + {% if this_page_children %} +{{ obj.type|title }} Contents +{{ "-" * obj.type|length }}--------- + + {% for obj_item in this_page_children %} +{{ obj_item.render()|indent(0) }} + {% endfor %} + {% endif %} + {% endif %} + {% endblock %} + {% else %} +.. py:module:: {{ obj.name }} + + {% if obj.docstring %} + .. autoapi-nested-parse:: + + {{ obj.docstring|indent(6) }} + + {% endif %} + {% for obj_item in visible_children %} + {{ obj_item.render()|indent(3) }} + {% endfor %} + {% endif %} +{% endif %} diff --git a/docs/_templates/autoapi/python/package.rst b/docs/_templates/autoapi/python/package.rst new file mode 100644 index 0000000..fb9a649 --- /dev/null +++ b/docs/_templates/autoapi/python/package.rst @@ -0,0 +1 @@ +{% extends "python/module.rst" %} diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..711edd6 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,185 @@ +import json +import os +import sys + +sys.path.insert(0, os.path.abspath("../src")) + +# -- Project info ------------------------------------------------------- + +project = "auth0-fastapi" +author = "Auth0" +copyright = "2024, Auth0" +release = "1.0.0b11" + +# -- Extensions --------------------------------------------------------- + +extensions = [ + "autoapi.extension", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", + "sphinx.ext.intersphinx", +] + +# -- AutoAPI ------------------------------------------------------------ + +autoapi_dirs = ["../src"] +autoapi_type = "python" +autoapi_root = "reference" +autoapi_ignore = ["**/test/**", "**/test_*.py"] + + +def autoapi_skip_member(app, what, name, obj, skip, options): + # Suppress Pydantic's inner Config class (exposes "self is positional-only" noise) + if what == "class" and name.endswith(".Config"): + return True + return skip + + +autoapi_options = [ + "members", + "undoc-members", + "show-inheritance", + "show-module-summary", +] +autoapi_member_order = "groupwise" +autoapi_keep_files = True +autoapi_template_dir = "_templates/autoapi" +autoapi_python_class_content = "class" # exclude auto-generated __init__ docstrings + +# -- Napoleon (docstring style) ----------------------------------------- + +napoleon_google_docstring = True +napoleon_use_param = True +napoleon_use_rtype = True +napoleon_preprocess_types = True + +# -- Intersphinx -------------------------------------------------------- + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "pydantic": ("https://docs.pydantic.dev/latest/", None), +} + +# -- HTML output -------------------------------------------------------- + +html_theme = "furo" +html_title = "auth0-fastapi" +html_static_path = ["_static"] +html_css_files = ["auth0.css"] +html_theme_options = { + "sidebar_hide_name": False, + "navigation_with_keys": True, + "light_css_variables": { + "color-brand-primary": "#9921FE", + "color-brand-content": "#9921FE", + }, + "dark_css_variables": { + "color-brand-primary": "#BC6DFF", + "color-brand-content": "#BC6DFF", + }, +} + +# -- General ------------------------------------------------------------ + +exclude_patterns = ["_build", "_templates", "Thumbs.db", ".DS_Store", "superpowers"] + +# -- Mintlify / docs-v2 post-processing --------------------------------- +# Path prefix used by docs-v2 when the artifact is placed under sdk-artifacts/auth0-fastapi +DOCS_V2_DIRECTORY = "docs/sdk/python/fastapi" + +# Title overrides: map page path (relative to _build/json/) to display title +TITLE_OVERRIDES = { + "reference/auth0_fastapi/config/index": "Auth0Config", + "reference/auth0_fastapi/errors/index": "ConfigurationError", +} + +# Module-level index pages with no content beyond a child list - redundant in Mintlify +REMOVE_PAGES = [ + "reference/auth0_fastapi/index", + "reference/auth0_fastapi/auth/index", + "reference/auth0_fastapi/server/index", + "reference/auth0_fastapi/stores/index", +] + +# Sidebar navigation emitted to navigation.json for docs-v2. +# Update this list when modules are added or removed from the SDK. +DOCS_V2_NAVIGATION = [ + { + "group": "Overview", + "pages": [f"{DOCS_V2_DIRECTORY}/reference/index"], + }, + { + "group": "Auth", + "pages": [f"{DOCS_V2_DIRECTORY}/reference/auth0_fastapi/auth/auth_client/index"], + }, + { + "group": "Config", + "pages": [f"{DOCS_V2_DIRECTORY}/reference/auth0_fastapi/config/index"], + }, + { + "group": "Server", + "pages": [f"{DOCS_V2_DIRECTORY}/reference/auth0_fastapi/server/routes/index"], + }, + { + "group": "Stores", + "pages": [ + f"{DOCS_V2_DIRECTORY}/reference/auth0_fastapi/stores/cookie_transaction_store/index", + f"{DOCS_V2_DIRECTORY}/reference/auth0_fastapi/stores/stateful_state_store/index", + f"{DOCS_V2_DIRECTORY}/reference/auth0_fastapi/stores/stateless_state_store/index", + ], + }, + { + "group": "Errors", + "pages": [f"{DOCS_V2_DIRECTORY}/reference/auth0_fastapi/errors/index"], + }, + { + "group": "Utilities", + "pages": [f"{DOCS_V2_DIRECTORY}/reference/auth0_fastapi/util/index"], + }, +] + + +def _postprocess_json_build(app, exception): + """Run after `sphinx -b json`. Cleans up boilerplate and writes navigation.json.""" + if exception or app.builder.name != "json": + return + + out = app.outdir + + # Apply title overrides + for page_path, title in TITLE_OVERRIDES.items(): + fpath = os.path.join(out, page_path + ".fjson") + if not os.path.exists(fpath): + continue + with open(fpath) as f: + data = json.load(f) + data["title"] = title + with open(fpath, "w") as f: + json.dump(data, f, indent=2) + + # Remove orphan module-level index pages + for page_path in REMOVE_PAGES: + fpath = os.path.join(out, page_path + ".fjson") + if os.path.exists(fpath): + os.remove(fpath) + + # Clean reference/index.fjson: strip sphinx-autoapi boilerplate + index_path = os.path.join(out, "reference", "index.fjson") + if os.path.exists(index_path): + with open(index_path) as f: + data = json.load(f) + data["body"] = "" + with open(index_path, "w") as f: + json.dump(data, f, indent=2) + + # Write navigation.json so docs-v2 does not need to maintain it separately + nav_path = os.path.join(out, "navigation.json") + with open(nav_path, "w") as f: + json.dump({"pages": DOCS_V2_NAVIGATION}, f, indent=2) + + print(f"[auth0-fastapi] Wrote {nav_path}") + + +def setup(app): + app.connect("autoapi-skip-member", autoapi_skip_member) + app.connect("build-finished", _postprocess_json_build) diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..d204d5e --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,17 @@ +auth0-fastapi +============= + +FastAPI SDK for Auth0: session management, login/logout routes, and token acquisition for +web applications. + +.. toctree:: + :maxdepth: 2 + :caption: API Reference + + reference/index + +Indices +------- + +* :ref:`genindex` +* :ref:`modindex` diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..88d4d44 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +sphinx>=7.4,<9 +sphinx-autoapi>=3.4,<4 +furo>=2024.8.6 diff --git a/src/auth0_fastapi/__init__.py b/src/auth0_fastapi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/auth0_fastapi/auth/auth_client.py b/src/auth0_fastapi/auth/auth_client.py index d98a118..189bbe4 100644 --- a/src/auth0_fastapi/auth/auth_client.py +++ b/src/auth0_fastapi/auth/auth_client.py @@ -159,11 +159,14 @@ async def start_link_user( ) -> str: """ Initiates the user linking process. + Options should include: - - connection: connection identifier (e.g. 'google-oauth2') - - connectionScope: (optional) the scope for the connection - - authorizationParams: additional parameters for the /authorize call - - appState: any custom state to track (e.g., a returnTo URL) + + - connection: connection identifier (e.g. 'google-oauth2') + - connectionScope: (optional) the scope for the connection + - authorizationParams: additional parameters for the /authorize call + - appState: any custom state to track (e.g., a returnTo URL) + Returns a URL to redirect the user to for linking. """ return await self.client.start_link_user(options, store_options=store_options) @@ -187,10 +190,13 @@ async def start_unlink_user( ) -> str: """ Initiates the user unlinking process. + Options should include: - - connection: connection identifier (e.g. 'google-oauth2') - - authorizationParams: additional parameters for the /authorize call - - appState: any custom state to track (e.g., a returnTo URL) + + - connection: connection identifier (e.g. 'google-oauth2') + - authorizationParams: additional parameters for the /authorize call + - appState: any custom state to track (e.g., a returnTo URL) + Returns a URL to redirect the user to for unlinking. """ return await self.client.start_unlink_user(options, store_options=store_options) @@ -230,8 +236,9 @@ async def custom_token_exchange( store_options: dict = None, ) -> TokenExchangeResponse: """ - Performs an RFC 8693 token exchange for the given subject token. - Does not create or modify the current session. + Performs an `RFC 8693 `_ token + exchange for the given subject token. Does not create or modify the + current session. Args: options: Subject token details and exchange parameters @@ -242,11 +249,13 @@ async def custom_token_exchange( the domain resolver needs the incoming request. Returns: - The raw TokenExchangeResponse (access_token, expires_in). + :class:`TokenExchangeResponse` with ``access_token`` and + ``expires_in`` fields. Raises: - CustomTokenExchangeError: If the exchange fails or the subject - token parameters are invalid (see CustomTokenExchangeErrorCode). + :exc:`CustomTokenExchangeError`: If the exchange fails or the + subject token parameters are invalid (see + :class:`CustomTokenExchangeErrorCode`). """ return await self.client.custom_token_exchange(options, store_options=store_options) @@ -256,26 +265,28 @@ async def login_with_custom_token_exchange( store_options: dict = None, ) -> LoginWithCustomTokenExchangeResult: """ - Performs an RFC 8693 token exchange for the given subject token and - establishes a session for the resulting user. + Performs an `RFC 8693 `_ token + exchange for the given subject token and establishes a session for the + resulting user. Args: options: Subject token details and exchange parameters (subject_token, subject_token_type, audience, scope, organization, authorization_params). store_options: Options passed to the Transaction and State Store. - Must include {"request": request, "response": response} so the - session cookie can be written on response. + Must include ``{"request": request, "response": response}`` so + the session cookie can be written on response. Returns: - The LoginWithCustomTokenExchangeResult containing the session state - (including the resulting user). + :class:`LoginWithCustomTokenExchangeResult` containing the session + state (including the resulting user). Raises: - CustomTokenExchangeError: If the exchange fails or the subject - token parameters are invalid (see CustomTokenExchangeErrorCode). - ValueError: If store_options is missing the response needed to - write the session cookie. + :exc:`CustomTokenExchangeError`: If the exchange fails or the + subject token parameters are invalid (see + :class:`CustomTokenExchangeErrorCode`). + :exc:`ValueError`: If ``store_options`` is missing the response + needed to write the session cookie. """ return await self.client.login_with_custom_token_exchange(options, store_options=store_options) @@ -310,12 +321,13 @@ async def request_session_transfer_token( incoming request. Also used to read the agent session for the actor. Returns: - The SessionTransferTokenResult containing the STT and its metadata. + :class:`SessionTransferTokenResult` containing the STT and its + metadata. Raises: - CustomTokenExchangeError: If no actor can be resolved or the exchange - fails (see CustomTokenExchangeErrorCode). - InvalidArgumentError: If organization is provided but blank. + :exc:`CustomTokenExchangeError`: If no actor can be resolved or + the exchange fails (see :class:`CustomTokenExchangeErrorCode`). + :exc:`InvalidArgumentError`: If organization is provided but blank. """ return await self.client.request_session_transfer_token( subject_token=subject_token, @@ -347,13 +359,14 @@ def build_session_transfer_redirect( organization: Organization identifier to forward (optional). Returns: - A URL string with session_transfer_token (and organization) as query - parameters. + A URL string with ``session_transfer_token`` (and ``organization``) + as query parameters. Raises: - MissingRequiredArgumentError: If target_login_url is missing or blank. - InvalidArgumentError: If target_login_url is not an absolute https URL, - or organization is blank. + :exc:`MissingRequiredArgumentError`: If ``target_login_url`` is + missing or blank. + :exc:`InvalidArgumentError`: If ``target_login_url`` is not an + absolute https URL, or ``organization`` is blank. """ return self.client.build_session_transfer_redirect( target_login_url, result, organization=organization