diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index eeaf0fd..9004156 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,7 +33,6 @@ jobs: steps: - uses: actions/checkout@v1 with: - fetch-depth: 1 submodules: false - name: Use Python ${{ matrix.python-version }} diff --git a/README.md b/README.md index 31ae9ef..8eee2c4 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,6 @@ This repository contains the source code of the documentation that gets published to [https://www.neoteroi.dev/](https://www.neoteroi.dev/). ---- - -Work in progress. 🚧 -The code has been modified to unify different projects. - ---- - ## How to contribute The documentation uses MkDocs and Material for MkDocs. For information on how diff --git a/blacksheep/docs/anti-request-forgery.md b/blacksheep/docs/anti-request-forgery.md index 01a9291..6600f4b 100644 --- a/blacksheep/docs/anti-request-forgery.md +++ b/blacksheep/docs/anti-request-forgery.md @@ -7,21 +7,25 @@ requests. Examples of such situations are: - Cookies are automatically included in web requests, so if an application uses - cookie-based authentication, credentials are sent automatically + cookie-based authentication, credentials are sent automatically. - After a user signs in with Basic or Digest authentication, the browser - automatically sends the credentials until the session ends + automatically sends the credentials until the session ends. If a web application uses cookie-based authentication or other features that cause credentials to be automatically included in web requests, it requires anti-forgery measures. BlackSheep implements built-in support for anti-request-forgery validation, this -page describes how to use the built-in solution. +page describes how to use it. -!!! tip - Applications that store access tokens (for example JWTs) in the HTML5 - storage and include them in `Authorization: Bearer {...}` headers, are not - vulnerable to CSRF and do not require anti-forgery measures. +/// admonition | Options using HTML5 Storage. + type: tip + +Applications that store access tokens (for example JWTs) in the HTML5 +storage and include them in `Authorization: Bearer {...}` headers, are not +vulnerable to CSRF and do not require anti-forgery measures. + +/// ## How to use the built-in anti-forgery validation @@ -37,10 +41,10 @@ app = Application(show_error_details=True) use_anti_forgery(app) ``` -The call to `use_anti_forgery(app)` configures a middleware that can issue and -validate anti-forgery tokens, and extensions for Jinja2 templates to render -anti-forgery tokens in HTML templates. It is important to configure templating -before anti-forgery because the latter configures the extensions on the Jinja2 +The `use_anti_forgery(app)` function configures middleware to issue and +validate anti-forgery tokens, as well as extensions for Jinja2 templates to +render these tokens in HTML templates. It is important to configure templating +before enabling anti-forgery, as the latter sets up extensions in the Jinja2 environment. Consider an example having this folder structure: @@ -61,7 +65,7 @@ from blacksheep import Application, FromForm, get, post, view from blacksheep.server.csrf import use_anti_forgery -app = Application(show_error_details=True) +app = Application() use_anti_forgery(app) @@ -101,19 +105,23 @@ And `index.jinja` contains the following template: ``` -The `{% af_input %}` custom tag is used to render an HTML input element containing an -anti-forgery token. The built-in solution uses the Double-Token strategy: when -an anti-forgery token is required to render HTML for a response, a corresponding -HTTP-only cookie is configured for the response. The value of the cookie and the -control parameter are matched in following requests for validation. Contextually, -response headers are also set to protect the HTML view against click-jacking and to -forbid iframes. +The `{% af_input %}` tag renders an HTML input element containing an +anti-forgery token. The built-in solution uses the Double-Token strategy. When +an anti-forgery token is required to render HTML, a corresponding HTTP-only +cookie is included in the response. The cookie's value and the control +parameter are matched in subsequent requests for validation. Contextually, +response headers are also set to protect the HTML view against click-jacking +and to forbid iframes. + +/// admonition | Alternative tags. + type: tip -!!! tip "Alternative tags" - In alternative to `{% af_input %}`, it is possible to use the tag - `{% csrf_input %}` (like Django). However, `af_input` is recommended since - the objective of the tag is to obtain an input element containing an - anti-forgery token, not to achieve Cross-Site Request Forgery! +In alternative to `{% af_input %}`, it is possible to use the tag +`{% csrf_input %}` (like Django). However, `af_input` is recommended since +the objective of the tag is to obtain an input element containing an +anti-forgery token, not to achieve Cross-Site Request Forgery! + +/// An example of a rendered view looks like the following: @@ -138,14 +146,18 @@ Validation is applied by default to all `DELETE PATCH POST PUT` web requests. Requests using other methods are not validated as they are not supposed to change the state and should execute read-only operations. -!!! danger "Important note about token generation" - Tokens are signed using symmetric encryption. For your production - environments, configure application secrets using environment variables - as described in [data protection](../dataprotection/). +/// admonition | Important note about token generation. + type: danger + +Tokens are signed using symmetric encryption. For your production environments, +configure application secrets using environment variables as described in [data +protection](dataprotection.md). + +/// ## How to send the anti-forgery token -The anti-forgery token can be sent to the server in one of these ways: +The anti-forgery token can be sent to the server in one of the following ways: | Location | Parameter Name | | -------------- | ---------------------------- | @@ -226,8 +238,8 @@ async def create_example(): ## Custom AntiForgeryHandler classes -The following example shows how to override methods of the `AntiForgeryHandler` -class: +The following example demonstrates how to override methods of the +`AntiForgeryHandler`: ```python from blacksheep.server.csrf import AntiForgeryHandler, use_anti_forgery diff --git a/blacksheep/docs/application.md b/blacksheep/docs/application.md index fabd5ef..e5a8f3f 100644 --- a/blacksheep/docs/application.md +++ b/blacksheep/docs/application.md @@ -41,25 +41,22 @@ trace, serving a page like the following: ![Internal server error page](./img/internal-server-error-page.png) -Consider using environmental variables to handle these kinds of settings that -can vary across environments. For example: +/// admonition | Use the `APP_SHOW_ERROR_DETAILS`. + type: tip -```python -import os -from blacksheep import Application - -app = Application(show_error_details=bool(os.environ.get("SHOW_ERROR_DETAILS", None))) - - -@get("/") -def crash_test(): - raise Exception("Crash test") +Rather than using the `show_error_details` parameter, it is recommended to use +the environment variable `APP_SHOW_ERROR_DETAILS` to control whether the +application displays detailed error information. Setting +`APP_SHOW_ERROR_DETAILS=1` or `APP_SHOW_ERROR_DETAILS=True` enables this +feature. +/// -``` +/// admonition | Settings strategy -!!! info "Settings strategy" - BlackSheep project templates include a strategy to handle application - settings and configuration roots. +BlackSheep project templates include a strategy to handle application +settings and configuration roots. Refer to [_Getting started with the MVC project template_](./mvc-project-template.md) +for more information. +/// ### Configuring exceptions handlers @@ -69,14 +66,13 @@ handling a web request and reaches the application, the application checks if there is a matching handler for that kind of exception. An exception handler is defined as a function with the following signature: -```python +```python {hl_lines="3"} from blacksheep import Request, Response, text async def exception_handler(self, request: Request, exc: Exception) -> Response: pass ``` -In the exception below ```python class CustomException(Exception): @@ -114,7 +110,7 @@ specific handler for one of the descendant classes. It is also possible to register exception handlers using decorators, instead of interacting with `app.exceptions_handlers` dictionary: -```python +```python {hl_lines="5"} class CustomException(Exception): pass @@ -131,8 +127,7 @@ To override how unhandled exceptions are handled, define a custom `Application` class overriding its `handle_internal_server_error` method, like in the following example: -```python - +```python {hl_lines="5-6"} from blacksheep import Application, json from blacksheep.messages import Request @@ -165,7 +160,7 @@ create an HTTP `ClientSession` that will be disposed of when the application stops. Note how the instance of `ClientSession` is also bound to application services, so that it can be injected into request handlers that need it. -```python +```python {linenums="1" hl_lines="9-10 16"} import asyncio from blacksheep import Application from blacksheep.client.pool import ClientConnectionPools @@ -198,14 +193,22 @@ if __name__ == "__main__": uvicorn.run(app, host="127.0.0.1", port=44777, log_level="debug", lifespan="on") ``` -!!! info - The method leverages `contextlib.asynccontextmanager`. What is defined - before the `yield` statement executes when the application starts, and what - is defined after the `yield` statement executes when the application stops. +- The code before the `yield` statement (lines _11-16_) is executed when the + application starts. +- The code after the `yield` statement (lines _17-18_) is executed when the + application stops. + +/// admonition | @app.lifespan + +This method leverages `contextlib.asynccontextmanager`. What is defined +before the `yield` statement executes when the application starts, and what +is defined after the `yield` statement executes when the application stops. + +/// The following example illustrates how a `redis-py` [connection can be disposed of](https://redis.readthedocs.io/en/stable/examples/asyncio_examples.html) -using the same method: +using `@app.lifespan`: ```python import redis.asyncio as redis @@ -231,31 +234,39 @@ async def configure_redis(): await connection.close() ``` -!!! info "Example using Redis" - The `BlackSheep-Examples` repository includes an example where `Redis` is - used to store access tokens and refresh tokens obtained using - `OpenID Connect`: [example](https://github.com/Neoteroi/BlackSheep-Examples/blob/main/oidc/scopes_redis_aad.py). For more information on `redis-py` and its async - interface, refer to its [official documentation](https://redis.readthedocs.io/en/stable/examples/asyncio_examples.html). - ### on_start -This event should be used to configure things such as new request handlers, -and services registered in `app.services`, such as database connection pools, +This event should be used to configure components such as new request handlers +and services registered in `app.services`, including database connection pools and HTTP client sessions. ### after_start -This event should be used to configure things that must happen after request -handlers are normalized. At this point, the application router contains information -about actual routes handled by the web application, and routes can be inspected. -For example, the built-in generation of OpenAPI Documentation generates the -API specification file at this point. +This event should be used to configure tasks that must occur after request +handlers are normalized. At this stage, the application router contains +information about the actual routes handled by the web application, allowing +routes to be inspected. For example, the built-in OpenAPI documentation +generation creates the API specification file at this point. + +/// admonition | Example: inspecting routes. + type: tip + +An `after_start` callback that prints all routes registered in the application +router: + +```python +@app.after_start +async def after_start_print_routes(application: Application) -> None: + print(application.router.routes) +``` +/// ### on_stop -This event should be used to fire callbacks that need to happen when the application -is stopped. For example, disposing of services that require disposal, such as -database connection pools, and HTTP client sessions using connection pools. +This event should be used to trigger callbacks that need to run when the +application stops. For example, it can be used to dispose of services that +require cleanup, such as database connection pools and HTTP client sessions +using connection pools. ### Application life cycle @@ -329,16 +340,6 @@ are fired, and the state of the application when they are executed. app.on_stop += on_stop ``` -!!! info - For example, to define an `after_start` callback that logs all routes registered - in the application router: - - ```python - @app.after_start - async def after_start_print_routes(application: Application) -> None: - print(application.router.routes) - ``` - ## Next -Read about the details of [routing in BlackSheep](../routing). +Read about the details of [routing in BlackSheep](routing.md). diff --git a/blacksheep/docs/asgi.md b/blacksheep/docs/asgi.md index 70fcfe3..56688e6 100644 --- a/blacksheep/docs/asgi.md +++ b/blacksheep/docs/asgi.md @@ -1,11 +1,12 @@ # ASGI Servers -BlackSheep belongs to the category of -[ASGI](https://asgi.readthedocs.io/en/latest/) web frameworks, so it requires -an ASGI HTTP server to run, such as [uvicorn](http://www.uvicorn.org/), or -[hypercorn](https://pgjones.gitlab.io/hypercorn/). All examples in this -documentation use `Uvicorn`, but the framework has been tested also with -`Hypercorn` and should work with any server that implements ASGI. +BlackSheep is an [ASGI](https://asgi.readthedocs.io/en/latest/) web framework, +which requires an ASGI HTTP server to run, such as +[Uvicorn](http://www.uvicorn.org/), or +[Hypercorn](https://pgjones.gitlab.io/hypercorn/). All examples in this +documentation use `Uvicorn`, but the framework has also been tested with +Hypercorn and should work with any server that implements the `ASGI` +specification. ### Uvicorn @@ -20,8 +21,10 @@ documentation use `Uvicorn`, but the framework has been tested also with

- Hypercorn + Hypercorn

+--- + Many details, such as how to run the server in production, depend on the chosen ASGI server. diff --git a/blacksheep/docs/authentication.md b/blacksheep/docs/authentication.md index 50a8e78..956e77c 100644 --- a/blacksheep/docs/authentication.md +++ b/blacksheep/docs/authentication.md @@ -1,39 +1,46 @@ # Authentication in BlackSheep -The words "authentication strategy" in the context of a web application refer -to the ability to identify the user who is using the application. BlackSheep -implements a built-in authentication strategy for request handlers. This page -describes: + +The term 'authentication strategy' in the context of a web application refers +to the process of identifying the user accessing the application. BlackSheep +provides a built-in authentication strategy for request handlers. This page +covers: - [X] How to use the built-in authentication strategy. - [X] How to configure a custom authentication handler. - [X] How to use the built-in support for JWT Bearer authentication. - [X] How to read the user's context in request handlers. -!!! warning - Using JWT Bearer and OpenID integrations requires more dependencies: use - `pip install blacksheep[full]` to use these features +/// admonition | Additional dependencies. + type: warning + +Using JWT Bearer and OpenID integrations requires additional dependencies. +Install them by running: `pip install blacksheep[full]`. + +/// ## Underlying library -The authentication and authorization logic implemented for BlackSheep was -packed and published into a dedicated library: + +The authentication and authorization logic for BlackSheep is packaged and +published in a dedicated library: [`guardpost`](https://github.com/neoteroi/guardpost) ([in pypi](https://pypi.org/project/guardpost/)). ## How to use built-in authentication -Examples of common strategies to identify users in web applications include: +Common strategies for identifying users in web applications include: + +- Reading an `Authorization: Bearer xxx` request header containing a [JWT](https://jwt.io/introduction/). + with claims that identify the user. +- Reading a signed token from a cookie. + +The following sections first explain how to use the built-in support for JWT +Bearer tokens and then describe how to write a custom authentication handler. -- reading an `Authorization: Bearer xxx` request header containing a [JWT](https://jwt.io/introduction/) - with claims that identify the user -- reading a signed token from a cookie +/// admonition | Terms: user, service, principal. -The next paragraphs explain first how to use the built-in support for JWT -Bearer tokens, and how to write a custom authentication handler. +The term 'user' typically refers to human users, while 'service' describes non-human clients. In Java and .NET, the term 'principal' is commonly used to describe a generic identity. -!!! info - The word "user" is usually used only to refer to human users, while - the word "service" is used to describe non-human clients. In Java and .NET, a - common word to describe a generic identity is "principal". +/// ## OIDC @@ -41,19 +48,23 @@ BlackSheep implements built-in support for OpenID Connect authentication, meaning that it can be easily integrated with identity provider services such as: -- [Auth0](https://auth0.com) -- [Entra ID](https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id) -- [Azure Active Directory B2C](https://docs.microsoft.com/en-us/azure/active-directory-b2c/overview) -- [Okta](https://www.okta.com) +- [Auth0](https://auth0.com). +- [Entra ID](https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id). +- [Azure Active Directory B2C](https://docs.microsoft.com/en-us/azure/active-directory-b2c/overview). +- [Okta](https://www.okta.com). -!!! tip "Examples in GitHub" - The [Neoteroi/BlackSheep-Examples/](https://github.com/Neoteroi/BlackSheep-Examples/) - repository in GitHub contains examples of JWT Bearer authentication and - OpenID Connect integrations. +/// admonition | Examples in GitHub. + type: tip -A basic example integration with any of the identity providers above, having -implicit flow enabled for `id_token` (meaning that the code doesn't need to -handle any secret), looks like the following: +The [Neoteroi/BlackSheep-Examples/](https://github.com/Neoteroi/BlackSheep-Examples/) +repository in GitHub contains examples of JWT Bearer authentication and OpenID +Connect integrations. + +/// + +A basic example of integration with any of the identity providers listed above, +using implicit flow for `id_token` (which removes the need to handle secrets), +is shown below: ```python from blacksheep import Application, get, html, pretty_json @@ -93,20 +104,20 @@ Where: | CALLBACK_PATH | The path that is enabled for `reply_uri` in your app settings, for example if you enabled for localhost: `http://localhost:5000/authorization-callback`, the value should be `/authorization-callback` | For more information and examples, refer to the dedicated page about -[OpenID Connect authentication](../openid-connect). +[OpenID Connect authentication](openid-connect.md). ## JWT Bearer BlackSheep implements built-in support for JWT Bearer authentication, and validation of JWTs: -* issued by identity providers implementing OpenID Connect (OIDC) discovery - (such as Auth0, Azure Active Directory) -* and more in general, JWTs signed using asymmetric encryption and verified - using public RSA keys +* Issued by identity providers implementing OpenID Connect (OIDC) discovery + (such as Auth0, Microsoft Entra ID). +* And more in general, JWTs signed using asymmetric encryption and verified + using public RSA keys. The following example shows how to configure JWT Bearer authentication for an -application registered in `Azure Active Directory`, and also how to configure +application registered in `Microsoft Entra ID`, and also how to configure authorization to restrict access to certain methods, only for users who are successfully authenticated: @@ -154,15 +165,18 @@ async def open(user: User | None): ``` -The built-in handler for JWT Bearer authentication does not support JWTs signed -with symmetric keys. Support for symmetric keys might be added in the future, -inside **[guardpost](https://github.com/Neoteroi/guardpost)** library. +The built-in handler for JWT Bearer authentication does not currently support +JWTs signed with symmetric keys. Support for symmetric keys might be added in +the future. + +/// admonition | 💡 -!!! info - 💡 It is possible to configure several JWTBearerAuthentication handlers, - for applications that need to support more than one identity provider. For - example, for applications that need to support sign-in through Auth0, Azure - Active Directory, Azure Active Directory B2C. +It is possible to configure several JWTBearerAuthentication handlers, +for applications that need to support more than one identity provider. For +example, for applications that need to support sign-in through Auth0, Azure +Active Directory, Azure Active Directory B2C. + +/// ## Writing a custom authentication handler @@ -224,21 +238,21 @@ async def open(user: User | None): ``` It is possible to configure several authentication handlers to implement -different ways to identify users. To differentiate the way the user has been -authenticated, use the second parameter of `Identity`'s constructor: +different ways to identify users. To distinguish how the user was +authenticated, use the second parameter of the Identity constructor: ```python identity = Identity({"name": "Jan Kowalski"}, "AUTHENTICATION_MODE") ``` -The authentication context is the instance of `Request` created to handle the -incoming web request. Authentication handlers must set the `identity` property -on the request, to enable automatic injection of `user` by dependency injection. +The authentication context is the `Request` instance created to handle the +incoming web request. Authentication handlers must set the `identity` property on +the request to enable the automatic injection of `user` via dependency injection. ### Testing the example To test the example above, start a web server as explained in the [getting -started guide](../getting-started), then navigate to its root. A web request to +started guide](getting-started.md), then navigate to its root. A web request to the root of the application without an `Authorization` header will produce a response with the following body: @@ -296,7 +310,8 @@ The example below shows how a user's identity can be read from the web request: ``` ## Next -While authentication deals with identifying users, authorization deals with -determining whether the user is authorized to do the action of the web request. -The next page describes the built-in [authorization strategy](../authorization) -in BlackSheep. + +While authentication focuses on *identifying* users, authorization determines +whether a user *is permitted* to perform the requested action. The next page +describes the built-in [authorization strategy](authorization.md) in +BlackSheep. diff --git a/blacksheep/docs/authorization.md b/blacksheep/docs/authorization.md index 5af1314..0d2927c 100644 --- a/blacksheep/docs/authorization.md +++ b/blacksheep/docs/authorization.md @@ -1,30 +1,31 @@ # Authorization in BlackSheep -The words "authorization strategy" in the context of a web application refer to -the ability to determine whether the user is allowed to do certain operations. -BlackSheep implements a built-in authorization strategy for request handlers. -This page describes: + +The term 'authorization strategy' in the context of a web application refers to +the process of determining whether a user is permitted to perform certain operations. +BlackSheep provides a built-in authorization strategy for request handlers. +This page covers: - [X] How to use the built-in authorization strategy. - [X] How to apply authorization rules to request handlers. -It is recommended to read about [authentication](../authentication) before -reading this page. +It is recommended to review the [authentication documentation](authentication.md) +before proceeding with this page. ## How to use built-in authorization -Examples of common strategies to authorize users in web applications include: +Common strategies for authorizing users in web applications include: -* verifying that the user's context obtained from a [JWT includes certain - claims](https://jwt.io/introduction/) (e.g. `scope`, `role`) -* verifying that a web request includes a certain key, like an instrumentation - key or a key signed by a private RSA key (owned by the user) that can be - verified by a public RSA key (used by the server to validate) +* Verifying that the user's context, obtained from a [JWT](https://jwt.io/introduction/), + includes certain claims (e.g., `scope`, `role`) +* Verifying that a web request contains a specific key, such as an + instrumentation key or a key signed by a private RSA key (owned by the user) + and validated by a public RSA key (used by the server). -The example below shows how to configure an authorization handler that -requires an authenticated user. It is modified from the example in the -[authentication](../authentication) page: +The following example demonstrates how to configure an authorization handler +that requires an authenticated user. It is adapted from the example on the +[authentication's documentation](authentication.md) page: -```python +```python {hl_lines="17-24 27 32 43"} from typing import Optional from blacksheep import Application, Request, json, ok, get @@ -74,28 +75,24 @@ async def only_for_authenticated_users(): ``` -Note: - -* authorization is enabled using `app.use_authorization()` -* this method returns an instance of `AuthorizationStrategy`, which handles - the authorization rules -* the method `.add(Policy(Authenticated, AuthenticatedRequirement()))` +* Authorization is enabled by calling `app.use_authorization()`. This method + returns an instance of `AuthorizationStrategy`, which manages the + authorization rules. +* The method `.add(Policy(Authenticated, AuthenticatedRequirement()))` configures an authorization policy with a single requirement, to have an - authenticated user -* the authorization policy is applied to request handlers using the `@auth` + authenticated user. +* The authorization policy is applied to request handlers using the `@auth` decorator from `blacksheep.server.authorization` with an argument that - specifies the policy to be used + specifies the policy to be used. It is possible to define several authorization policies, each specifying one or more requirements to be satisfied in order for authorization to succeed. -The next example explains how to configure an authorization policy that checks -for user's roles from claims. -## Defining an authorization policy that checks a user's claims +## Defining an authorization policy that checks claims -The example below shows how to configure an authorization handler that -validates a user's claims (looking for a "role" claim that might be coming from -a JWT). +The following example demonstrates how to configure an authorization handler +that validates a user's claims, such as checking for a `role` claim that may +originate from a JWT. ```python from blacksheep.server.authorization import Policy @@ -199,10 +196,10 @@ async def only_for_administrators(): ## Using the default policy -The method `app.use_authorization()`, when used without arguments, returns an -instance of `AuthorizationStrategy` from the `guardpost` library. This object -can be configured to use a default policy, for example to require an -authenticated user by default for all request handlers. +The `app.use_authorization()` method returns an instance of +`AuthorizationStrategy` from the `guardpost` library. This object can be +configured to use a default policy, such as requiring an authenticated user by +default for all request handlers. ```python authorization = app.use_authorization() @@ -233,18 +230,17 @@ async def for_anybody(user: Optional[User]): In some scenarios it is necessary to specify multiple authentication schemes for web applications: for example, the same application might handle -authentication obtained through the `GitHub` OAuth app and `Azure Active -Directory (AAD)`. In such scenarios, it might be necessary to restrict access -to some endpoints by authentication method, too. +authentication obtained through the `GitHub` OAuth app and `Microsoft Entra ID`. +In such scenarios, it might be necessary to restrict access to some endpoints +by authentication method, too. To do so: -1. specify different authentication handlers, configuring schemes overriding +1. Specify different authentication handlers, configuring schemes overriding the `scheme` property as in the example below. -2. use the `authentication_schemes` parameter in the `@auth` decorator - -```python +2. Use the `authentication_schemes` parameter in the `@auth` decorator. +```python {hl_lines="1 3-5 11"} class GitHubAuthHandler(AuthenticationHandler): @property @@ -268,7 +264,9 @@ async def only_for_user_authenticated_with_github(): When a request fails because of authorization reasons, the web framework returns: -- status [`401 Unauthorized`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) if authentication failed, and no valid credentials were provided -- status [`403 Forbidden`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403) if +- Status [`401 + Unauthorized`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) + if authentication failed and no valid credentials were provided. +- Status [`403 Forbidden`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403) if authentication succeeded as valid credentials were provided, but the user is - not authorized to perform an action + not authorized to perform an action. diff --git a/blacksheep/docs/behind-proxies.md b/blacksheep/docs/behind-proxies.md index cfef0a8..1657cc5 100644 --- a/blacksheep/docs/behind-proxies.md +++ b/blacksheep/docs/behind-proxies.md @@ -122,7 +122,7 @@ implementing interactive sign-in. --- -`BlackSheep` offers two ways to deal with this scenario: +BlackSheep offers two ways to deal with this scenario: - One approach, defined by the `ASGI` specification, involves specifying a `root_path` in the `ASGI` server. This information is passed in the scope of @@ -137,7 +137,7 @@ implementing interactive sign-in. the web server is desirable to align it with the path handled by the HTTP proxy server, and it is ideal when applying URL rewrite is not easy. -For both options, `BlackSheep` handles the information provided by `root_path` +For both options, BlackSheep handles the information provided by `root_path` and the application router prefix in some specific ways. For example, the `get_absolute_url_to_path` defined in `blacksheep.messages` will handle the information and return an absolute URL to the server diff --git a/blacksheep/docs/binders.md b/blacksheep/docs/binders.md index b256675..579e7bf 100644 --- a/blacksheep/docs/binders.md +++ b/blacksheep/docs/binders.md @@ -6,7 +6,7 @@ This feature improves code quality and the developer experience since it provides a strategy to read values from request objects in a consistent way and removes the need to write parts that read values from the request object inside request handlers. It also enables a more accurate generation of [OpenAPI -Documentation](../openapi), since the framework is aware of what kind of +Documentation](openapi.md), since the framework is aware of what kind of parameters are used by the request handlers (e.g. _headers, cookies, query_). This page describes: @@ -17,15 +17,15 @@ This page describes: It is recommended to read the following pages before this one: -* [Getting started: Basics](../getting-started/) -* [Getting started: MVC](../mvc-project-template/) -* [Requests](../requests/) +* [Getting started: Basics](getting-started.md) +* [Getting started: MVC](mvc-project-template.md) +* [Requests](requests.md) ## Introduction Automatic binding of request query strings and route parameters has been described in several places in the previous pages, and explicit and implicit -binding is introduced in the section about [requests](../requests/). +binding is introduced in the section about [requests](requests.md). Binding is implicit when the source of a parameter is inferred by conventions, or explicit when the programmer specifies exact binders from @@ -99,7 +99,7 @@ In the example above, `create_cat_handler` is obtained from `application.services`, an exception is thrown if the the service cannot be resolved. This happens if the service is not registered in application services, or any of the services on which it depends is not registered -(see [_Service resolution_](../dependency-injection/#service-resolution) for +(see [_Service resolution_](dependency-injection.md#service-resolution) for more information on services that depend on other services). `input` is obtained by reading the request payload, parsing it as JSON, and diff --git a/blacksheep/docs/cache-control.md b/blacksheep/docs/cache-control.md index 82eadab..5d12829 100644 --- a/blacksheep/docs/cache-control.md +++ b/blacksheep/docs/cache-control.md @@ -1,10 +1,10 @@ -BlackSheep offers features to configure `Cache-Control` response headers. -This page explains: +This page describes features to configure `Cache-Control` response headers. +It covers: -- [X] How to use the `cache_control` decorator to configure a header for specific - request handlers -- [X] How to use the `CacheControlMiddleware` to configure a common header for all - request handlers globally +- [X] Using the `cache_control` decorator to configure a header for specific + request handlers. +- [X] Using the `CacheControlMiddleware` to configure a common header for all + request handlers globally. ## About Cache-Control @@ -38,9 +38,13 @@ async def get_cats(): ``` -!!! warning "Decorators order" - The order of decorators matters: the router decorator must be the outermost - decorator in this case. +/// admonition | Decorators order. + type: warning + +The order of decorators matters: the router decorator must be the outermost +decorator in this case. + +/// For controllers: @@ -95,8 +99,8 @@ class Home(Controller): return "Example" ``` -The provided `CacheControlMiddleware` can be subclassed to control what requests -should be affected: +The provided `CacheControlMiddleware` can be subclassed to control when +requests should be affected: ```python from blacksheep import Request, Response @@ -109,7 +113,8 @@ class MyCacheControlMiddleware(CacheControlMiddleware): ... ``` -For example, a middleware that disables cache-control by default can be defined in the following way: +For instance, a middleware that disables cache-control by default can be +defined in the following way: ```python class NoCacheControlMiddleware(CacheControlMiddleware): diff --git a/blacksheep/docs/client.md b/blacksheep/docs/client.md index 20b61b0..638d2f4 100644 --- a/blacksheep/docs/client.md +++ b/blacksheep/docs/client.md @@ -41,7 +41,7 @@ different request-response cycles, when possible. By default, connections are not disposed of as long as they are kept open. Implementation: -[/blacksheep/client/pool.py](https://github.com/RobertoPrevato/BlackSheep/blob/master/blacksheep/client/pool.py). +[/blacksheep/client/pool.py](https://github.com/Neoteroi/BlackSheep/blob/master/blacksheep/client/pool.py). Connections are created using `asyncio` function `loop.create_connection`. @@ -76,11 +76,14 @@ The `ClientSession` owns by default a connections pool, if none is specified for it. The connections pool is automatically disposed of when the client is exited, if it was created for the client. -!!! danger "Connection pooling is important" - Avoid instantiating a new `ClientSession` at each web request, unless the - same `ConnectionsPool` is reused among the instances. Instantiating a new - `ClientSession` without reusing the same TCP connections pool has - negative effects on the performance of the application. +/// admonition | On the importance of connection pooling. + +Avoid instantiating a new `ClientSession` at each web request, unless the +same `ConnectionsPool` is reused among the instances. Instantiating a new +`ClientSession` without reusing the same TCP connections pool has +negative effects on the performance of the application. + +/// It is recommended to instantiate a single instance of HTTP client and register it as a service of the application, using the `@app.lifespan` method: diff --git a/blacksheep/docs/compression.md b/blacksheep/docs/compression.md index 4a62baf..97b93d1 100644 --- a/blacksheep/docs/compression.md +++ b/blacksheep/docs/compression.md @@ -1,8 +1,7 @@ -BlackSheep implements built-in features to handle automatic response -compression. This page describes: - -- [X] How to use the `GzipMiddleware` to enable gzip compression +This page describes built-in features to handle automatic response compression. +It covers: +- [X] Using the `GzipMiddleware` to enable gzip compression. ## GzipMiddleware @@ -33,11 +32,14 @@ app = Application() use_gzip_compression(app) ``` -!!! warning "Not for streamed content" - The `GzipMiddleware` does not compress bytes streamed using the - `StreamedContent` class (used by default when serving files), it only - compresses whole bodies like, for example, those that are generated when - returning `JSON` content to the client. +/// admonition | Not for streamed content. + +The `GzipMiddleware` does not compress bytes streamed using the +`StreamedContent` class (used by default when serving files), it only +compresses whole bodies like, for example, those that are generated when +returning `JSON` content to the client. + +/// ### Options diff --git a/blacksheep/docs/contributing.md b/blacksheep/docs/contributing.md index 7a4305a..82d8ea9 100644 --- a/blacksheep/docs/contributing.md +++ b/blacksheep/docs/contributing.md @@ -6,7 +6,7 @@ presented here applies also to other projects from [rodi](https://github.com/Neoteroi/rodi), [guardpost](https://github.com/Neoteroi/guardpost), [essentials-openapi](https://github.com/Neoteroi/essentials-openapi)), although -among these projects, only `BlackSheep` is using `Cython`. +among these projects, only BlackSheep is using `Cython`. ## System requirements @@ -43,7 +43,7 @@ Watch the following video for instructions: ## Formatters and style enforcement -`BlackSheep` uses the following tools for code formatting: +BlackSheep uses the following tools for code formatting: * [`flake8`](https://flake8.pycqa.org/en/latest/) * [`black`](https://github.com/psf/black) @@ -83,7 +83,7 @@ enough!), and to be fully test-covered. ## Code coverage -`BlackSheep` features 100% code coverage, except for some rare cases where +BlackSheep features 100% code coverage, except for some rare cases where `#pragma: no cover` is used. New contributions should not decrease code coverage, unless there is a good reason to skip lines. Integration with [`Codecov`](https://app.codecov.io/gh/Neoteroi/BlackSheep) checks code coverage diff --git a/blacksheep/docs/controllers.md b/blacksheep/docs/controllers.md index 611c162..5e96559 100644 --- a/blacksheep/docs/controllers.md +++ b/blacksheep/docs/controllers.md @@ -1,28 +1,30 @@ # Controllers -BlackSheep has built-in features to support MVC (Model, View, Controller) -architecture. A `Controller` is a class having at least one method registered -as a request handler (i.e. associated to a route). A Controller is instantiated -at each web request, when a web request is matched by a route defined in that -type of controller. +BlackSheep includes built-in features to support the MVC (Model, View, +Controller) architecture. A `Controller` is a class with at least one method +registered as a request handler (i.e., associated with a route). A Controller +is instantiated for each web request when the request matches a route defined +in that controller. This page describes: - [X] Controller methods. - [X] API Controllers. -It is recommended to follow the [MVC tutorial](../mvc-project-template/) before +It is recommended to follow the [MVC tutorial](mvc-project-template.md) before reading this page. -!!! tip "For Flask users..." - If you come from Flask, controllers in BlackSheep can be considered - equivalent to Flask's Blueprints, as they allow to group request handlers - in dedicated modules and classes. +/// admonition | For Flask users + type: tip +If you come from Flask, controllers in BlackSheep can be considered +equivalent to Flask's Blueprints, as they allow to group request handlers +in dedicated modules and classes. +/// ## The Controller class Controllers implement several methods to simplify returning responses. These -are the same described in [Responses](../responses/), but they can be overridden +are the same described in [Responses](responses.md), but they can be overridden in subclasses of `Controller` and they remove the need to import functions. | Method | Description | @@ -35,7 +37,7 @@ in subclasses of `Controller` and they remove the need to import functions. | **json** | Returns a response with application/json content, and the given status (default HTTP 200 OK). | | **pretty_json** | Returns a response with indented application/json content, and the given status (default HTTP 200 OK). | | **text** | Returns a response with text/plain content, and the given status (default HTTP 200 OK). | -| **html** | Returns a response with text/html content, and the given status (default HTTP 200 OK). | +| **html** | Returns a response with text/html content, and the given status (default HTTP 200 OK). | | **moved_permanently** | Returns an HTTP 301 Moved Permanently response, to the given location. | | **redirect** | Returns an HTTP 302 Found response (commonly called redirect), to the given location. | | **see_other** | Returns an HTTP 303 See Other response, to the given location. | @@ -79,9 +81,9 @@ Using controllers involves a performance fee compared to using functions because a controller must be instantiated at each web request, but has the following benefits: -* Controllers support [dependency injection](../dependency-injection/) to +* Controllers support [dependency injection](dependency-injection.md) to receive services for their constructors, in addition to [dependency - injection](../dependency-injection) for every single request handler + injection](dependency-injection.md) for every single request handler * Controllers support defining an `on_request(request: Request)` method, that gets called at every web request, `on_response(response: Response)` method, and a base `route` (defined as class method) for all handlers defined in the @@ -90,7 +92,7 @@ benefits: base classes to personalize the behavior of the application without monkey-patching functions -Therefore they can help avoid code repetition in some circumstances. +Therefore they can help avoid code repetition. The following example shows how dependency injection can be used in controller constructors, and an implementation of the `on_request` method: @@ -130,6 +132,15 @@ class Home(Controller): app.services.add_instance(Settings(value)) ``` +The dependency can also be described as class property: + +```python {hl_lines="2"} +class Home(Controller): + settings: Settings + + ... +``` + If route methods (e.g. `head`, `get`, `post`, `put`, `patch`) from `blacksheep.server.controllers` are used, then the default singleton `Router` instance for controllers is used. It is also possible to use a specific router, @@ -145,12 +156,13 @@ get = app.controllers_router.get ``` ## The APIController class + The `APIController` class is a kind of `Controller` dedicated to API -definitions. An APIController offers some properties to enable versioning of +definitions. An APIController offers some properties to simplify versioning of routes and adding a common path prefix to all routes, for example, prepending "/v1/" fragment to all routes and the name of the controller class. -```python +```python {hl_lines="5"} from blacksheep import Response, FromJSON, FromQuery from blacksheep.server.controllers import APIController, delete, get, patch, post @@ -187,14 +199,14 @@ the following paths: | HTTP Method | Path | Request handler | Description | | ----------- | ------------------ | --------------- | --------------------------------- | -| HTTP GET | /api/cats | `get_cats` | Returns a list of paginated cats. | -| HTTP GET | /api/cats/{cat_id} | `get_cat` | Gets a cat by id. | -| HTTP POST | /api/cats/{cat_id} | `create_cat` | Creates a new cat. | -| HTTP PATCH | /api/cats/{cat_id} | `update_cat` | Updates a cat with given id. | -| HTTP DELETE | /api/cats/{cat_id} | `delete_cat` | Deletes a cat by id. | - -To include a version number in the API, implement a `version` `@classmethod` like in the -following example: +| GET | /api/cats | `get_cats` | Returns a list of paginated cats. | +| GET | /api/cats/{cat_id} | `get_cat` | Gets a cat by id. | +| POST | /api/cats/{cat_id} | `create_cat` | Creates a new cat. | +| PATCH | /api/cats/{cat_id} | `update_cat` | Updates a cat with given id. | +| DELETE | /api/cats/{cat_id} | `delete_cat` | Deletes a cat by id. | + +To include a version number in the API, implement a `version` `@classmethod` +like in the following example: ```python class Cats(APIController): diff --git a/blacksheep/docs/cors.md b/blacksheep/docs/cors.md index 6cf10dc..c71ba39 100644 --- a/blacksheep/docs/cors.md +++ b/blacksheep/docs/cors.md @@ -1,14 +1,15 @@ # Cross-Origin Resource Sharing -BlackSheep implements a strategy to handle [Cross-Origin Resource Sharing +BlackSheep provides a strategy to handle [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). This page -describes: +covers: -- [X] How to enable CORS globally. -- [X] How to enable CORS for specific endpoints. +- [X] Enabling CORS globally. +- [X] Enabling CORS for specific endpoints. ## Enabling CORS globally -The example below shows how to enable CORS globally on a BlackSheep application: + +The example below demonstrates how to enable CORS globally: ```python app.use_cors( @@ -19,8 +20,8 @@ app.use_cors( ) ``` -When enabled this way, the framework handles `CORS` requests, including -preflight `OPTIONS` requests. +When enabled this way, the framework handles `CORS` requests and preflight +`OPTIONS` requests. It is possible to use `*` to enable any origin or any method: @@ -42,10 +43,12 @@ app.use_cors( | expose_headers | Controls the value of [Access-Control-Expose-Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers). 🗡️ | | max_age | Controls the value of [Access-Control-Max-Age](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age), defaults to 5 seconds. | -🗡️ The value can be a string of values separated by space, comma, or semi-colon, or a list. +🗡️ The value can be a string of values separated by space, comma, or semi-colon, + or a list. ## Enabling CORS for specific endpoints -The example below shows how to enable CORS only for certain endpoints: + +The example below demonstrates how to enable CORS only for specific endpoints: ```python @@ -71,18 +74,18 @@ async def enabled(): Explanation: -1. the function call `app.use_cors()` activates built-in handling of CORS - requests and registers a global CORS rule that doesn't allow anything by - default -2. the call `app.add_cors_policy(...)` registers a new set of rules for CORS, - associated to the key "example" -3. the set of rules for CORS called "example" is associated to specific - request handlers using the `@cors` decorator +1. The function call `app.use_cors()` activates the built-in handling of CORS + requests and registers a global CORS rule that denies all requests by + default. +2. The call to `app.add_cors_policy(...)` registers a new set of CORS rules + associated with the key 'example'. +3. The CORS rules associated with the key 'example' are applied to specific + request handlers using the `@cors` decorator. It is possible to register many sets of rules for CORS, each with its own key, and apply different rules to request handlers. It is also possible to define a global rule when calling `app.use_cors(...)` -that enables certain operations for all request handlers, while defining +that enables certain operations for all request handlers, while still defining specific rules. ```python diff --git a/blacksheep/docs/css/extra.css b/blacksheep/docs/css/extra.css index cf0cec7..c2c769c 100644 --- a/blacksheep/docs/css/extra.css +++ b/blacksheep/docs/css/extra.css @@ -104,7 +104,7 @@ span.task-list-indicator { .md-typeset .tabbed-labels>label, .md-typeset .admonition, .md-typeset details { font-size: .75rem !important; } - +/* .md-typeset code { font-size: .9em !important; } @@ -112,3 +112,4 @@ span.task-list-indicator { table td code { white-space: nowrap; } +*/ diff --git a/blacksheep/docs/dataprotection.md b/blacksheep/docs/dataprotection.md index 7a76a9b..3ee5932 100644 --- a/blacksheep/docs/dataprotection.md +++ b/blacksheep/docs/dataprotection.md @@ -1,15 +1,17 @@ # Data protection Web applications often need to protect data, so that it can be stored in -cookies or other types of storage. BlackSheep uses [`itsdangerous`](https://pypi.org/project/itsdangerous/) to sign and encrypt -information, for example when storing `claims` obtained from `id_token`s when -using an integration with an identity provider using [OpenID -Connect](../authentication/#oidc), or when handling [session cookies](../sessions/). +cookies or other types of client storage. BlackSheep uses +[`itsdangerous`](https://pypi.org/project/itsdangerous/) to sign and encrypt +information. For example, it is used to store `claims` obtained from +`id_token`s in integrations with identity providers using [OpenID +Connect](authentication.md#oidc), or when handling [session +cookies](sessions.md). -This page documents: +This page covers: -- [X] How to handle secrets -- [X] Example use of data protection +- [X] Handling secrets. +- [X] Using data protection features. ## How to handle secrets @@ -20,7 +22,7 @@ generated automatically in memory when the application starts, for the best user experience. !!! danger - This means that keys are not persisted when applications + This means that keys are **not persisted** when applications restart, and are not consistent when multiple instances of the same application are deployed across regions, or within the same server. This is acceptable during local development, but should not be the case in @@ -29,15 +31,13 @@ user experience. To use consistent keys, configure one or more environment variables like the following: -* APP_SECRET_1="***" -* APP_SECRET_2="***" -* APP_SECRET_3="***" +- APP_SECRET_1="***" +- APP_SECRET_2="***" +- APP_SECRET_3="***" Keys can be configured in a host environment, or fetched from a dedicated service such as `AWS Secrets Manager` or `Azure Key Vault` at application start-up, and configured as environment settings for the application. -DO NOT store secrets that are meant to be used in production -under source control. ## Example diff --git a/blacksheep/docs/dependency-injection.md b/blacksheep/docs/dependency-injection.md index bfadd09..b58ecf2 100644 --- a/blacksheep/docs/dependency-injection.md +++ b/blacksheep/docs/dependency-injection.md @@ -1,50 +1,49 @@ # Dependency injection in BlackSheep -The getting started tutorials show how route and query string parameters can be -injected directly in request handlers, by function signature. BlackSheep -also supports dependency injection of services configured for the application. -This page describes: -- [X] An introduction to dependency injection in BlackSheep, with a focus on `rodi`. +The getting started tutorials demonstrate how route and query string parameters +can be directly injected into request handlers through function signatures. +Additionally, BlackSheep supports the dependency injection of services +configured for the application. This page covers: + +- [X] An introduction to dependency injection in BlackSheep, with a focus on Rodi. - [X] Service resolution. - [X] Service lifetime. - [X] Options to create services. - [X] Examples of dependency injection. -- [X] How to use alternatives to `rodi`. +- [X] How to use alternatives to Rodi. -!!! info "Rodi's documentation" - Detailed documentation for Rodi can be found at: [_Rodi_](/rodi/). +/// admonition | Rodi's documentation +Detailed documentation for Rodi can be found at: [_Rodi_](/rodi/). +/// ## Introduction -The `Application` object exposes a `services` property that can be used to -configure services. When the function signature of a request handler references -a type that is registered as a service, an instance of that type is -automatically injected when the request handler is called. +The `Application` object exposes a `services` property that can be used to configure services. When the function signature of a request handler references a type that is registered as a service, an instance of that type is automatically injected when the request handler is called. Consider this example: -* some context is necessary to handle certain web requests (for example, a - database connection pool) -* a class that contains this context can be configured in application services - before the application starts -* request handlers have this context automatically injected +* Some context is necessary to handle certain web requests (for example, a + database connection pool). +* A class that contains this context can be configured in application services + before the application starts. +* Request handlers have this context automatically injected. ### Demo Starting from a minimal environment as described in the [getting started -tutorial](../getting-started/), create a `foo.py` file with the following +tutorial](getting-started.md), create a `foo.py` file with the following contents, inside a `domain` folder: ``` . ├── domain -│ ├── foo.py -│ └── __init__.py +│ ├── __init__.py +│ └── foo.py └── server.py ``` -**domain/foo.py**: ```python +# domain/foo.py class Foo: def __init__(self) -> None: @@ -54,8 +53,8 @@ class Foo: Import the new class in `server.py`, and register the type in `app.services` as in this example: -**server.py**: -```python +```python {hl_lines="9 13"} +# server.py from blacksheep import Application, get from domain.foo import Foo @@ -69,41 +68,37 @@ app.services.add_scoped(Foo) # <-- register Foo type as a service @get("/") def home(foo: Foo): # <-- foo is referenced in type annotation return f"Hello, {foo.foo}!" - ``` An instance of `Foo` is injected automatically for every web request to "/". -Dependency injection is implemented in a dedicated library from the same author: -[`rodi`](https://github.com/RobertoPrevato/rodi). `rodi` implements dependency -injection in an unobtrusive way: it works by inspecting `__init__` methods and -doesn't require altering the source code of classes registered as services. -`rodi` can also resolve dependencies by inspecting class annotations, if an -`__init__` method is not specified for the class to activate. +Dependency injection is implemented in a dedicated library: +[Rodi](https://github.com/neoteroi/rodi). Rodi implements dependency injection +in an unobtrusive way: it works by inspecting code and doesn't require altering +the source code of the types it resolves. ## Service resolution -`rodi` automatically resolves graphs of services, when a type that is resolved -requires other types. In the following example, instances of `A` are created -automatically when resolving `Foo` because the `__init__` method in `Foo` -requires an instance of `A`: +Rodi automatically resolves dependency graphs when a resolved type depends on +other types. In the following example, instances of `A` are automatically +created when resolving `Foo` because the `__init__` method in `Foo` requires an +instance of `A`: -**foo.py**: -```python +```python {hl_lines="2 7"} +# domain/foo.py class A: - def __init__(self) -> None: - pass + pass class Foo: - def __init__(self, a: A) -> None: - self.a = a + def __init__(self, dependency: A) -> None: + self.dependency = dependency ``` -Note that both types need to be registered in `app.services`: +Both types must be registered in `app.services`: -**server.py**: -```python +```python {hl_lines="9-10"} +# server.py from blacksheep import Application, get, text from domain.foo import A, Foo @@ -119,10 +114,9 @@ app.services.add_scoped(Foo) def home(foo: Foo): return text( f""" - A: {id(foo.a)} + A: {id(foo.dependency)} """ ) - ``` Produces a response like the following at "/": @@ -133,44 +127,40 @@ Produces a response like the following at "/": ## Using class annotations -An alternative to defining `__init__` methods is to use class annotations, like -in the example below: +It is possible to use class properties, like in the example below: -```python +```python {hl_lines="6"} class A: pass class Foo: - a: A + dependency: A ``` ## Understanding service lifetimes -`rodi` supports services having one of these lifetimes: +`rodi` supports types having one of these lifetimes: -* __singleton__ - instantiated only once per service provider -* __transient__ - services are instantiated every time they are required -* __scoped__ - instantiated once per web request +* __singleton__ - instantiated only once. +* __transient__ - services are instantiated every time they are required. +* __scoped__ - instantiated once per web request. Consider the following example, where a type `A` is registered as transient, `B` as scoped, `C` as singleton: -**foo.py**: ```python +# domain/foo.py class A: - def __init__(self) -> None: - pass + ... class B: - def __init__(self) -> None: - pass + ... class C: - def __init__(self) -> None: - pass + ... class Foo: @@ -184,8 +174,8 @@ class Foo: ``` -**server.py**: ```python +# server.py from blacksheep import Application, get, text from domain.foo import A, B, C, Foo @@ -222,51 +212,52 @@ def home(foo: Foo): Produces responses like the following at "/": -**Request 1**: -``` - A1: 139976289977296 +=== "Request 1" + ``` + A1: 139976289977296 - A2: 139976289977680 + A2: 139976289977680 - B1: 139976289977584 + B1: 139976289977584 - B2: 139976289977584 + B2: 139976289977584 - C1: 139976289978736 + C1: 139976289978736 - C2: 139976289978736 -``` + C2: 139976289978736 + ``` -**Request 2**: -``` - A1: 139976289979888 - - A2: 139976289979936 +=== "Request 2" + ``` + A1: 139976289979888 - B1: 139976289979988 + A2: 139976289979936 - B2: 139976289979988 + B1: 139976289979988 - C1: 139976289978736 + B2: 139976289979988 - C2: 139976289978736 -``` + C1: 139976289978736 -Note how: + C2: 139976289978736 + ``` -- transient services are always instantiated whenever they are activated (A) -- scoped services are instantiated once per web request (B) -- a singleton service is activated only once (C) +- Transient services are created every time they are needed (A). +- Scoped services are created once per web request (B). +- Singleton services are instantiated only once and reused across the application (C). ## Options to create services -`rodi` provides several ways to define and instantiate services. +Rodi provides several ways to define and instantiate services. 1. registering an exact instance as a singleton 2. registering a concrete class by its type 3. registering an abstract class and one of its concrete implementations 4. registering a service using a factory function +For detailed information on this subject, refer to the Rodi documentation: +[_Registering types_](https://www.neoteroi.dev/rodi/registering-types/). + #### Singleton example ```python @@ -280,7 +271,9 @@ class ServiceSettings: self.oauth_application_id = oauth_application_id self.oauth_application_secret = oauth_application_secret -app.services.add_instance(ServiceSettings("00000000001", "APP_SECRET_EXAMPLE")) +app.services.add_instance( + ServiceSettings("00000000001", os.environ["OAUTH_APP_SECRET"]) +) ``` @@ -290,15 +283,11 @@ app.services.add_instance(ServiceSettings("00000000001", "APP_SECRET_EXAMPLE")) class HelloHandler: - def __init__(self): - pass - def greetings() -> str: return "Hello" app.services.add_transient(HelloHandler) - ``` #### Registering an abstract class @@ -352,7 +341,6 @@ async def get_cat(cat_id: str, repo: CatsRepository): return not_found() return json(cat) - ``` #### Using a factory function @@ -371,6 +359,7 @@ app.services.add_transient_by_factory(something_factory) ``` #### Example: implement a request context + A good example of a scoped service is one used to assign each web request with a trace id that can be used to identify requests for logging purposes. @@ -398,20 +387,75 @@ app.services.add_scoped(OperationContext) @get("/") def home(context: OperationContext): - return text( - f""" - Request ID: {context.trace_id} - """ - ) - + return text(f"Request ID: {context.trace_id}") ``` ## Services that require asynchronous initialization -Services that require asynchronous initialization can be configured inside -`on_start` callbacks, like in the following example: +Services that require asynchronous initialization can be configured using +application events. The recommended way is using the `lifespan` context +manager, like described in the example below. -```python +```python {linenums="1" hl_lines="9 15-16 18 22"} +import asyncio +from blacksheep import Application +from blacksheep.client.pool import ClientConnectionPools +from blacksheep.client.session import ClientSession + +app = Application() + + +@app.lifespan +async def register_http_client(): + async with ClientSession( + pools=ClientConnectionPools(asyncio.get_running_loop()) + ) as client: + print("HTTP client created and registered as singleton") + app.services.register(ClientSession, instance=client) + yield + + print("HTTP client disposed of") + + +@router.get("/") +async def home(http_client: ClientSession): + print(http_client) + return {"ok": True, "client_instance_id": id(http_client)} + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="127.0.0.1", port=44777, log_level="debug", lifespan="on") +``` + +Here are the key points describing the logic of using the `lifespan` decorator: + +- **Purpose**: The `@app.lifespan` decorator is used to define asynchronous + setup and teardown logic for an application, such as initializing and + disposing of resources. +- **Setup Phase**: Code before the `yield` statement is executed when the + application starts. This is typically used to initialize resources (e.g., + creating an HTTP client, database connections, or other services). +- **Resource Registration**: During the setup phase, resources can be + registered as services in the application's dependency injection container, + making them available for injection into request handlers. +- **Teardown Phase**: Code after the `yield` statement is executed when the + application stops. This is used to clean up or dispose of resources (e.g., + closing connections or releasing memory). +- **Singleton Resource Management**: The `@lifespan` decorator is particularly + useful for managing singleton resources that need to persist for the + application's lifetime. +- **Example Use Case**: In the provided example, an `HTTP client` is created + and registered as a singleton during the setup phase, and it is disposed of + during the teardown phase. + +--- + +Otherwise, it is possible to use the `on_start` callback, like in the following +example, to register a service that requires asynchronous initialization: + +```python {hl_lines="13-19"} import asyncio from blacksheep import Application, get, text @@ -439,16 +483,10 @@ async def home(service: Example): ``` -Services configured this way are automatically injected in request handlers -when a parameter name or type annotation matches a key inside `app.services`. - -Services that require disposing of should be disposed of in `on_stop` callback: +Services that require disposal can be disposed of in the `on_stop` callback: -```python +```python {hl_lines="3 6"} async def dispose_example(app: Application): - # Note: after the application is started, services are read from - # app.service_provider: - service = app.service_provider[Example] await service.dispose() @@ -462,9 +500,10 @@ Since version 2, BlackSheep supports alternatives to `rodi` for dependency injection. The `services` property of the `Application` class needs to conform to the following container protocol: -- `register` method to register types -- `resolve` method to resolve instances of types -- `__contains__` method to describe whether a type is defined inside the container +- The `register` method to register types. +- The `resolve` method to resolve instances of types. +- The `__contains__` method to describe whether a type is defined inside the + container. ```python class ContainerProtocol: @@ -485,12 +524,11 @@ class ContainerProtocol: """ ``` -The following example shows how to use -[`punq`](https://github.com/bobthemighty/punq) for dependency injection instead -of `rodi`, and how a transient service can be resolved at "/" and a singleton -service resolved at "/home": +The following example demonstrates how to use +[`punq`](https://github.com/bobthemighty/punq) for dependency injection as an +alternative to `rodi`. -```python +```python {hl_lines="17 36-37 39"} from typing import Type, TypeVar, Union, cast import punq @@ -581,8 +619,12 @@ def default_container_factory(): di_settings.use(default_container_factory) ``` -!!! danger "Dependency injection libraries vary" - Some features might not be supported when using a different kind of container, - because not all libraries for dependency injection implement the notion of - `singleton`, `scoped`, and `transient` (most only implement `singleton` and - `transient`). +/// admonition | Dependency injection libraries vary. + type: danger + +Some features might not be supported when using a different kind of container, +because not all libraries for dependency injection implement the notion of +`singleton`, `scoped`, and `transient` (most only implement `singleton` and +`transient`). + +/// diff --git a/blacksheep/docs/getting-started.md b/blacksheep/docs/getting-started.md index d18ee2e..7265622 100644 --- a/blacksheep/docs/getting-started.md +++ b/blacksheep/docs/getting-started.md @@ -1,7 +1,7 @@ # Getting started with BlackSheep This tutorial explains how to create and start a minimal BlackSheep web -application.
It provides a general view, covering the following topics: +application. It provides an overview of the following topics: - [X] Creating a web application from scratch. - [X] Running the web application. @@ -12,22 +12,21 @@ application.
It provides a general view, covering the following topics: ### Requirements * [Python](https://www.python.org) version >= **3.10** (3.8 and 3.9 are - supported but not recommended to follow this tutorial) -* path to the python executable configured in the environment `$PATH` variable + supported but not recommended for this tutorial) +* Ensure the Python executable is included in the `$PATH` environment variable. (tip: if you install Python on Windows using the official installer, enable the checkbox to update your `$PATH` variable during the installation) ### Preparing a development environment -Create a folder in the desired location on your file system, then open a -command line terminal and navigate to the new folder. -Create a virtual environment using the following command: +1. Create a folder in your desired location, open a terminal, and navigate to it. +2. Create a virtual environment using the following command: ``` python -m venv venv ``` -and activate it: +3. Activate the virtual environment: === "On Linux or Mac" @@ -41,12 +40,10 @@ and activate it: venv\Scripts\activate ``` -BlackSheep belongs to the category of -[ASGI](https://asgi.readthedocs.io/en/latest/) web frameworks, therefore it -requires an ASGI HTTP server to run, such as -[uvicorn](http://www.uvicorn.org/), or -[hypercorn](https://pgjones.gitlab.io/hypercorn/). For this tutorial, install -`uvicorn` together with `blacksheep`: +BlackSheep is an [ASGI](https://asgi.readthedocs.io/en/latest/) web framework, +so it requires an ASGI HTTP server like [uvicorn](http://www.uvicorn.org/), or +[hypercorn](https://pgjones.gitlab.io/hypercorn/). Install `uvicorn` and +`blacksheep` for this tutorial: ```bash pip install blacksheep uvicorn @@ -87,11 +84,10 @@ text answer from the web application: ### Configuring routes -The current code configures a request handler for [HTTP GET](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET) -method at the root path of the application: `"/"`. Note how a function decorator -is used to register the `home` function as a request handler: +The current code configures a request handler for the [HTTP GET](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET) +method at the application's root path: `"/"`. Notice how a function decorator registers the home function as a request handler: -```python +```python {hl_lines="1"} @get("/") def home(): ... @@ -103,10 +99,10 @@ handle the request and produce a response. Register more request handlers to handle more routes and [HTTP methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods). -Update your `server.py` file to contain the following example, which includes +Update your `server.py` file to contain the following code, which includes two request handlers: one for `HTTP GET /`, and one for `HTTP POST /`. -```python +```python {hl_lines="7 12"} from blacksheep import Application, get, post @@ -123,17 +119,20 @@ def post_example(request): return "POST Example" ``` -!!! info - Thanks to `uvicorn`'s auto reload feature (used with `--reload` argument), - when the `server.py` file is updated, the application is automatically reloaded. - This is extremely useful during development. +/// admonition | Auto reload. + type: info + +Thanks to `uvicorn`'s auto reload feature (used with `--reload` argument), +when the `server.py` file is updated, the application is automatically reloaded. +This is extremely useful during development. +/// Navigate again to `http://127.0.0.1:44777`, it should display the text: `"GET Example"`. To verify that the `post_example` request handler is handling `POST` requests, -use a tool to generate a POST HTTP request at the server's address. -For example, using [`curl`](https://curl.haxx.se): +make a POST HTTP request at the server's address. +For example, using [`curl`](https://curl.haxx.se) or [PowerShell](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell): === "curl" @@ -147,11 +146,14 @@ For example, using [`curl`](https://curl.haxx.se): Invoke-WebRequest -Uri http://localhost:44777 -Method POST ``` -!!! info - The application automatically handles requests for any path that - is not handled by the router, returning an `HTTP 404 Not Found` response; - and produces `HTTP 500 Internal Server Error` responses in case of - unhandled exceptions happening during code execution. +/// admonition + type: info + +The application automatically handles requests for any path that is not handled +by the router, returning an `HTTP 404 Not Found` response and producing `HTTP +500 Internal Server Error` responses in case of unhandled exceptions happening +during code execution. +/// ### Handling route parameters @@ -166,7 +168,7 @@ def greetings(name): ``` Route parameters and function parameters are bound by matching names. -Add the fragment of code above to `server.py` and try navigating to +Add the fragment of code above to `server.py` and navigate to `http://127.0.0.1:44777/World`. A route can contain several named parameters, separated by slashes, and @@ -190,7 +192,14 @@ For example, to define a route that handles integer route parameters and returns `HTTP 400 Bad Request` for invalid values, it is sufficient to decorate the function argument this way: -```python +By default, route parameters are treated as strings. However, BlackSheep +supports automatic value parsing when function arguments are annotated with +[type annotations](https://docs.python.org/3/library/typing.html). For instance, +to define a route that handles integer parameters and returns an `HTTP 400 Bad Request` +for invalid values, it is sufficient to decorate the function argument as +follows: + +```python {hl_lines="2"} @get("/lucky-number/{number}") def only_numbers_here(number: int): return f"Lucky number: {number}\n" @@ -222,10 +231,8 @@ Several built-in types are handled automatically, like `str`, `bool`, `int`, ### Handling query string parameters -In the same way, route parameters are injected automatically into request -handlers by route parameters with matching names, `blacksheep` can handle -query string parameters automatically. Add this new fragment to your -application: +BlackSheep can handle query string parameters automatically. Add this new +fragment to your application: ```python @get("/query") @@ -237,10 +244,10 @@ Then navigate to [http://localhost:44777/query?name=World](http://localhost:4477 --- -A request handler can use different query strings, and query string parameters -support lists. +Request handlers can work with various query strings, and query string +parameters also support lists. -```python +```python {hl_lines="2 6"} @get("/query-list") def greetings_many(name: list[str]): return f"Hello, {', '.join(name)}!" @@ -253,7 +260,7 @@ def greetings_many(name: list[str]): Every handler can have many input parameters from different sources: request headers, cookies, query, route, request body, and configured application services. These are treated in more detail in the dedicated page about -[Binders](./binders). +[Binders](binders.md). ### Accessing the request object @@ -270,24 +277,25 @@ def request_object(request: Request): ... ``` -!!! info - You can name the request parameter any way you like (e.g. `request`, `req`, `foo`, etc.), - as long as you keep the correct type annotation (`blacksheep.Request`). +/// admonition + type: info -This subject will be treated in more detail in a different section. +You can name the request parameter any way you like (e.g. `request`, `req`, `foo`, etc.), +as long as you keep the correct type annotation (`blacksheep.Request`). +/// ### Handling responses -Generally speaking, request handlers in BlackSheep must return an instance of -`blacksheep.messages.Response` class. The framework provides several functions -to produce responses for various use cases, defined in the +Generally, request handlers in BlackSheep must return an instance of +`blacksheep.messages.Response` class. The framework offers several functions +for generating responses for various use cases, which are defined in the `blacksheep.server.responses` namespace. -The following example shows how to serve a JSON response, using a class defined -with [`dataclass`](https://docs.python.org/3/library/dataclasses.html). Delete -all contents from the current `server.py` file and paste the following code: +The following example demonstrates how to serve a JSON response using a class +defined with [`dataclass`](https://docs.python.org/3/library/dataclasses.html). +Replace the contents of `server.py` with the following code: -```python +```python {hl_lines="4 19"} from dataclasses import dataclass from uuid import UUID, uuid4 @@ -316,8 +324,8 @@ def get_cats(): ) ``` -Then navigate to [http://127.0.0.1:44777/api/cats](http://127.0.0.1:44777/api/cats) -to see the result, it will look like this: +Navigate to [http://127.0.0.1:44777/api/cats](http://127.0.0.1:44777/api/cats) +to view the result, which will look like this: ```js [{"id":"9dea0080-0e92-46e0-b090-55454c23d37f","name":"Lampo","active":true}, @@ -326,10 +334,10 @@ to see the result, it will look like this: {"id":"b697358e-0f74-4449-840a-32c8db839244","name":"Pilou","active":true}] ``` -Note how the `json` function is used to create an instance of `Response` whose -content is a payload serialized into a JSON string. +Notice how the `json` function creates an instance of `Response` with content +serialized into a JSON string. -```python +```python {hl_lines="3 8"} from blacksheep import json response = json({"example": 1}) @@ -343,16 +351,19 @@ response.content.length 13 ``` -!!! tip - Try also the `pretty_json` function in `blacksheep.server.responses`, which - returns indented JSON. +/// admonition + type: tip + +Try also the `pretty_json` function in `blacksheep.server.responses`, which +returns indented JSON. +/// For more granular control, it is possible to use the `blacksheep.messages.Response` class directly (read `blacksheep.server.responses` module for examples), and -it is possible to modify the response before returning it to the client: -for example to set a response header. +it is possible to modify the response before returning it to the client. +For instance, to set a response header: -```python +```python {hl_lines="12"} @get("/api/cats") def get_cats(): response = json( @@ -369,9 +380,8 @@ def get_cats(): return response ``` -User-defined request handlers can also return arbitrary objects, which will -be automatically converted to JSON responses. The example above could also be -written this way: +User-defined request handlers can return arbitrary objects, which will +be automatically converted to JSON responses. ```python @@ -385,16 +395,17 @@ def get_cats() -> list[Cat]: ] ``` -The rationale for this design choice is that JSON is the most commonly used -format to serialize objects today, and this feature is useful to reduce code -verbosity while making the return type explicit. Additionally, it enables -better generation of OpenAI Documentation. +The rationale behind this design choice is that JSON is the most widely used +format for serializing objects today. This feature helps reduce code verbosity +while making the return type explicit. Furthermore, it enables the automatic +generation of OpenAPI documentation to describe the response body. ### Asynchronous request handlers -The examples so far showed synchronous request handlers. To define asynchronous -request handlers, define `async` functions: -```python +The examples so far have demonstrated synchronous request handlers. To create +asynchronous request handlers, use `async` functions: + +```python {hl_lines="3 4"} @get("/api/movies") async def get_movies(): @@ -407,14 +418,14 @@ Asynchronous code is described more in other sections of the documentation. ### Summary -This tutorial covered the ABCs of creating a BlackSheep application. The -general concepts presented here apply to any kind of web framework: +This tutorial covered the basics of creating a BlackSheep application. The +general concepts introduced here are applicable to any web framework: -- server side routing +- server-side routing - handling query strings and route parameters - handling requests and responses -The next page will describe a more articulated scenario, including handling -HTML views on the server side, serving static files, and more. +The next page will explore a more advanced scenario, including server-side +rendering of HTML views, serving static files, and more features. -- [Getting started with the MVC project template](../mvc-project-template/) +- [Getting started with the MVC project template](mvc-project-template.md) diff --git a/blacksheep/docs/hsts.md b/blacksheep/docs/hsts.md index 5946e02..dcc5b72 100644 --- a/blacksheep/docs/hsts.md +++ b/blacksheep/docs/hsts.md @@ -1,11 +1,10 @@ -The HTTP Strict-Transport-Security response header (often abbreviated as HSTS) -is a standard feature used to instruct clients that a site should only be -accessed using HTTPS, and any attempt to access it using HTTP should be -converted automatically to HTTPS. +The HTTP Strict-Transport-Security (HSTS) response header is a standard feature +that instructs clients to access a site exclusively using HTTPS. Any attempt to +access the site via HTTP is automatically redirected to HTTPS. -BlackSheep offers a middleware to configure the HTTP Strict-Transport-Security -response header globally. This page explains how to use the built-in middleware -to enforce HSTS on a web application. +BlackSheep provides middleware to globally configure the HTTP +Strict-Transport-Security (HSTS) response header. This page explains how to use +the built-in middleware to enforce HSTS in a web application. ## Enabling HSTS @@ -21,13 +20,16 @@ if not is_development(): app.middlewares.append(HSTSMiddleware()) ``` -!!! tip "Considerations for local development" - It is generally undesirable to enable `HSTS` during local development, - since browsers get instructed to require `HTTPS` for all traffic on - `localhost`. This is why the example above configures the middleware only - if the application is not running in development mode. - See [_Defining application environment_](/blacksheep/settings/#defining-application-environment) - for more information. +/// admonition | Considerations for local development. + type: tip + +Enabling `HSTS` during local development is generally not recommended, as it +instructs browsers to require `HTTPS` for all traffic on `localhost`. For this +reason, the example above configures the middleware only when the application +is not running in development mode. Refer to [_Defining application environment_](settings.md#defining-application-environment) +for more information. + +/// ## Options @@ -37,5 +39,7 @@ if not is_development(): | include_subdomains | `bool` | Control the `include-subdomains` directive of the HSTS header (default false) | ## For more information -For more information on HTTP Strict Transport Security, it is recommended to -refer to the [developer.mozilla.org documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security). + +For more information on HTTP Strict Transport Security, refer to the +[developer.mozilla.org +documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security). diff --git a/blacksheep/docs/index.md b/blacksheep/docs/index.md index 8710a6a..f3844d1 100644 --- a/blacksheep/docs/index.md +++ b/blacksheep/docs/index.md @@ -28,8 +28,8 @@ pip install blacksheep To get started with BlackSheep, read these tutorials: -- [Basics](./getting-started/) -- [The MVC template](./mvc-project-template/) +- [Basics](getting-started.md) +- [The MVC template](mvc-project-template.md) ## Versions diff --git a/blacksheep/docs/middlewares.md b/blacksheep/docs/middlewares.md index dd9a272..04dfad8 100644 --- a/blacksheep/docs/middlewares.md +++ b/blacksheep/docs/middlewares.md @@ -1,7 +1,7 @@ # Middlewares -A BlackSheep application supports middlewares, which provide a flexible way to -define a chain of functions that handle every web request. +Middlewares enable modifying the chain of functions that handle each web +request. This page covers: @@ -14,33 +14,32 @@ Middlewares enable the definition of callbacks that are executed for each web request in a specific order. !!! info - When a function should be called only for certain routes, use - instead a [decorator function](../middlewares/#wrapping-request-handlers). + If a function should only be called for specific routes, use + a [decorator function](middlewares.md#wrapping-request-handlers) instead. -Middlewares are called in order: each receives the `Request` object as the -first parameter, and the next handler to be called as the second parameter. Any -middleware can decide to not call the next handler, and return a `Response` -object instead. For example, a middleware can be used to return an `HTTP 401 -Unauthorized` response in certain scenarios. +Middlewares are executed in order: each receives the `Request` object as the +first parameter and the next handler to be called as the second parameter. Any +middleware can choose not to call the next handler and instead return a +`Response` object. For instance, a middleware can be used to return an `HTTP +401 Unauthorized` response in certain scenarios. ```python -from blacksheep import Application, text +from blacksheep import Application, get -app = Application(show_error_details=True) -get = app.router.get +app = Application() async def middleware_one(request, handler): - print("middleware one: A") + print("middleware 1: A") response = await handler(request) - print("middleware one: B") + print("middleware 1: B") return response async def middleware_two(request, handler): - print("middleware two: C") + print("middleware 2: C") response = await handler(request) - print("middleware two: D") + print("middleware 2: D") return response @@ -51,15 +50,15 @@ app.middlewares.append(middleware_two) @get("/") def home(): return "OK" - ``` In this example, the following data would be printed to the console: + ``` -middleware one: A -middleware two: C -middleware two: D -middleware one: B +middleware 1: A +middleware 2: C +middleware 2: D +middleware 1: B ``` ### Middlewares defined as classes @@ -69,6 +68,7 @@ example below: ```python class ExampleMiddleware: + async def __call__(self, request, handler): # do something before passing the request to the next handler @@ -99,6 +99,7 @@ class ExampleMiddleware: ``` ### Resolution chains + When middlewares are defined for an application, resolution chains are built at its start. Every handler configured in the application router is replaced by a chain, executing middlewares in order, down to the registered handler. @@ -143,17 +144,21 @@ async def home(): return "OK" ``` -**The order of decorators matters**: user-defined decorators must be applied -before the route decorator (before `@get` in the example above). +/// admonition | The order of decorators matters. + type: warning + +User-defined decorators must be applied before the route decorator (in the example above, before `@get`). + +/// ### Define a wrapper compatible with synchronous and asynchronous functions To define a wrapper that is compatible with both synchronous and asynchronous functions, it is possible to use `inspect.iscoroutinefunction` function. For -example, to alter the decorator above to be compatible with request handlers -defined as synchronous functions (recommended): +example, to alter the decorator above to be *also* compatible with request +handlers defined as synchronous functions: -```python +```python {hl_lines="1 11"} import inspect from functools import wraps from typing import Tuple @@ -193,4 +198,4 @@ def headers(additional_headers: Tuple[Tuple[str, str], ...]): !!! warning The `ensure_response` function is necessary to support scenarios when the request handlers defined by the user doesn't return an instance of - Response class (see _[request handlers normalization](../request-handlers/)_). + Response class (see _[request handlers normalization](request-handlers.md)_). diff --git a/blacksheep/docs/mounting.md b/blacksheep/docs/mounting.md index fe4d511..a75eeb0 100644 --- a/blacksheep/docs/mounting.md +++ b/blacksheep/docs/mounting.md @@ -1,11 +1,11 @@ # Mounting applications -The word "mounting" refers to the ability to plug ASGI applications into -others, under specific routes. This enables reusing whole applications, or -components, across web applications. This page describes: +The term 'mounting' refers to the ability to integrate ASGI applications into others +under specific routes. This enables reusing whole applications across web applications. +This page covers: -- [X] How to use the mount feature in BlackSheep. -- [X] Details about mounting, and handling of application events. +- [X] Using the mount feature. +- [X] Details about mounting and handling of application events. - [X] Mounting and OpenAPI Documentation. - [X] An example using [Piccolo Admin](https://github.com/piccolo-orm/piccolo_admin). @@ -50,18 +50,18 @@ In the example above, both `parent` and `child` are complete applications that can be started independently. If `child` is started alone, it replies to GET web requests at route "/" with the text "Hello, from the child app". -Since `parent` mounts `child` under the path "/sub", when `parent` is started, it -delegates requests starting with `/sub/*` to the mounted application, therefore when the -`parent` is started, a GET request to the route "/sub" produces the greetings message -from `child`. A GET request to the route "/" instead is replied with the text "Hello, -from the parent app". +Since `parent` mounts `child` under the path '/sub', it delegates requests starting with +`/sub/*` to the mounted application. As a result, when parent is started, a GET request +to the route '/sub' produces the greeting message from `child`. A GET request to the +route "/" instead is replied with the text "Hello, from the parent app". -!!! info +///!!! info Try to create a file `server.py` like in the example above, and run the applications using `uvicorn`, to verify how they work in practice. ## Side effects of mounting -Even though mounting can enable interesting scenarios, it comes at a price. + +Although mounting enables interesting scenarios, it comes at a cost. Applications that are supposed to be mounted by other applications need to be designed to be "mount-friendly", for example when they handle redirects or @@ -115,9 +115,9 @@ app_b.mount("/a", app_a) ``` -This won't produce the expected result in real-life scenarios! `app_a` in this +This won't produce the expected result in real-life scenarios. `app_a` in this case redirects to the absolute path "/", therefore a path that is handled by -`app_b`! In general, mounted apps will be defined in dedicated packages with +`app_b`. In general, mounted apps will be defined in dedicated packages with no knowledge of the applications that mount them. To fix this scenario, it is necessary to use a relative path for redirection, like: @@ -128,6 +128,7 @@ def redirect_to_home(): ``` ### Handling of application events + Applications often need to define actions that must happen when the application starts, and actions that must happen when the application stops. @@ -173,11 +174,8 @@ events. web framework used to implement it. The example above is correct when `child` is an instance of BlackSheep Application. -!!! danger "Note" - `APP_MOUNT_AUTO_EVENTS` is not the default to not introduce breaking changes. - This will be the default behavior in BlackSheep 2.x - ## Mounting and OpenAPI Documentation + Since version `1.2.5`, BlackSheep supports generating OpenAPI Documentation for mounted BlackSheep applications, meaning that parent applications can expose OpenAPI Documentation about all endpoints, including those of mounted apps and @@ -307,6 +305,7 @@ Produces OpenAPI Documentation for all endpoints. ![Mount OAD](./img/mount-oad.png) ## Examples + To see a working example where `mount` is used, see [the Piccolo Admin example at _BlackSheep-Examples_](https://github.com/Neoteroi/BlackSheep-Examples/tree/main/piccolo-admin). diff --git a/blacksheep/docs/mvc-project-template.md b/blacksheep/docs/mvc-project-template.md index cf7736f..7c44b1c 100644 --- a/blacksheep/docs/mvc-project-template.md +++ b/blacksheep/docs/mvc-project-template.md @@ -10,24 +10,23 @@ project template, covering the following topics: - [X] Handling parameters in controllers. - [X] Serving static files -It is recommended to follow the [previous tutorial](../getting-started) before +It is recommended to follow the [previous tutorial](getting-started.md) before reading this one. ### Requirements * [Python](https://www.python.org) version >= **3.10** (3.8 and 3.9 are - supported but not recommended to follow this tutorial) -* path to the python executable configured in the environment `$PATH` variable + supported but not recommended for this tutorial) +* Ensure the Python executable is included in the `$PATH` environment variable. (tip: if you install Python on Windows using the official installer, enable - the checkbox to update your `$PATH` variable automatically) + the checkbox to update your `$PATH` variable during the installation) * a text editor: any is fine; this tutorial uses [Visual Studio Code](https://code.visualstudio.com/Download) ## Introduction to the BlackSheep CLI -The previous tutorial described the basics of creating an application from -scratch. While that knowledge is important, it is usually not desirable to -start every project from scratch. BlackSheep offers a command-line interface -(CLI) that can be used to start new projects. The CLI can be installed from the +The previous tutorial covered the basics of creating an application from +scratch. While that knowledge is important, starting every project from scratch is often unnecessary. BlackSheep provides a command-line interface +(CLI) to simplify the process of starting new projects. The CLI can be installed from the [Python Package Index](https://pypi.org/project/blacksheep-cli/) using the `blacksheep-cli` package: @@ -35,7 +34,7 @@ start every project from scratch. BlackSheep offers a command-line interface pip install blacksheep-cli ``` -Once installed, the `create` command can be used to start new projects: +The BlackSheep-CLI provides the `create` command to start new projects: ```bash blacksheep create @@ -62,16 +61,20 @@ tutorial, answer: INI ``` -!!! tip "blacksheep create" - It is possible to use the `create` command specifying the project name - and template directly, like in: +/// admonition | BlackSheep create. + type: tip - - `blacksheep create some_name` - - `blacksheep create some_name --template api` +It is possible to use the `create` command specifying the project name +and template directly, like in: + +- `blacksheep create some_name` +- `blacksheep create some_name --template api` + +/// ![MVC template](./img/mvc-template-v2.png) -After a project is created, the CLI displays a message with instructions. +After a project is created, the CLI displays a message with instructions: ``` ────────────────────────────────────────────────────────────────────── @@ -101,25 +104,26 @@ python dev.py uvicorn app.main:app --port 44777 --lifespan on --reload ``` -And navigate to the local page, opening a browser at [`http://localhost:44777`](http://localhost:44777) -(use the same port of the previous command). +Navigate to the local page, opening a browser at [`http://localhost:44777`](http://localhost:44777) +(use the same port used in the previous command). The browser should display this page: ![MVC Project home](./img/mvc-template-home.png) -Several things are happening because the web application is configured: +The web application is configured to handle several tasks: -- to build and serve dynamic HTML pages -- to serve static files (e.g. pictures, JavaScript, CSS files) -- to expose an API and offer OpenAPI Documentation about the API -- to handle application settings and application start/stop events +- Build and serve dynamic HTML pages. +- Serve static files (e.g., images, JavaScript, CSS files). +- Expose an API and provide OpenAPI documentation for it. +- Handle application settings and manage application start/stop events. -Let's see these elements in order, but first let's get acquainted with the -project's structure. +Let's explore these elements in order, but first, let's review the project's +structure. ## Project structure -The project is organized with the following folder structure: + +The project follows the folder structure outlined below: ``` ├── app @@ -146,22 +150,19 @@ The project is organized with the following folder structure: └── settings.yaml (base settings file) ``` -- the `app` folder contains files that are specific to the web application, - settings, a folder for `controllers` that define routes, folders for `static` - files and one for `views` (HTML templates) -- other packages at the root of the project, like `domain`, should be - abstracted from the web server and should be reusable in other kinds of - applications (for example, a CLI) -- the root folder contains the `dev.py` file to start the application in +- The `app` folder contains files that are specific to the web application, + settings, a folder for `controllers` that define request handlers, folders + for `static` files and one for `views` (HTML templates). +- Other packages at the root of the project, like `domain`, should be + abstracted from the web server and potentially reusable in other kinds of + applications. +- The root folder contains the `dev.py` file to start the application in development mode, and settings files with `.yaml` extension that are read when the application starts (since the YAML format was selected when using - the `blacksheep create` command) - -The project uses `onion architecture`. For example, a valid scenario would be -to add an additional package for the data access layer, and implement the -business logic in modules inside the `domain` folder. + the `blacksheep create` command). ## Open the project with a text editor + Open the project's folder using your favorite text editor. ![Visual Studio Code](./img/vs-code-mvc.png) @@ -176,8 +177,8 @@ async def home(): ... ``` -`blacksheep` offers an alternative way to define request handlers: using class -methods. Both approaches have pros and cons, which will be described later in +BlackSheep offers an alternative way to define request handlers: using classes. +Both approaches have pros and cons, which will be described later in more detail. To see this in practice, create a new file `app/controllers/greetings.py` and copy the following code into it: @@ -190,41 +191,51 @@ class Greetings(Controller): @get("/hello-world") def index(self): return self.text("Hello, World!") - ``` Stop and restart the application, then navigate to [`http://localhost:44777/hello-world`](http://localhost:44777/hello-world): it will display the response from the `Greetings.index` method. -When the path of a web request matches a route defined in a controller type, a -new instance of that `Controller` is created. In other words, every instance of -controller is scoped to a specific web request. Just like function handlers, -controllers support the automatic injection of parameters into request -handlers, and also dependency injection into their constructors (`__init__` -methods). This is a feature that improves development speed and enables cleaner -code (compare this approach with a scenario where all dependencies need to be -imported and referenced inside function bodies by hand). +When the path of a web request matches a route defined in a controller, a new +instance of that `Controller` is created to handle the request. In other words, +each container instance is scoped to a specific web request. Just like function +handlers, controllers support automatic injection of parameters and dependency +injection to resolve parameters defined in constructors (`__init__` methods) +and class properties. This feature enhances development speed and promotes +cleaner code. -The `Controller` class implements methods to return values and offers -`on_request` and `on_response` extensibility points. +/// admonition | Rodi documentation. + type: info -!!! tip "Controllers and routes automatic import" - Python modules defined inside `controllers` and `routes` packages are - automatically imported by a BlackSheep application. The automatic import - happens relative to the namespace where the application is instantiated. +Refer to [Rodi's documentation](https://www.neoteroi.dev/rodi/) for a detailed +introduction to dependency injection. +/// + +The `Controller` class provides methods to return various kinds of responses +and offers `on_request` and `on_response` extensibility points. These functions +can be overridden in subclasses of `Controller` to apply logic at the start and +end of each web request. + +/// admonition | Automatic import of controllers and routes. + type: tip + +Python modules defined inside `controllers` and `routes` packages are +automatically imported by a BlackSheep application. The automatic import +happens relatively to the namespace where the application is instantiated. +/// ## Server side templating (views and models) -Server side templating refers to the ability of a web application to generate -HTML pages from templates and dynamic variables. By default, BlackSheep does -this using the [`Jinja2` library](https://palletsprojects.com/p/jinja/) -by the [Pallets](https://palletsprojects.com) team. +Server-side templating refers to a web application's ability to generate HTML +pages using templates and dynamic variables. By default, BlackSheep achieves +this with the [`Jinja2` library](https://palletsprojects.com/p/jinja/) +developed by the [Pallets](https://palletsprojects.com) team. -To see how this works in practice when using `Controllers`, edit the `Greetings` -controller created previously to look like this: +To see how this works in practice when using controllers, edit the `Greetings` +controller created previously as follows: -```python +```python {hl_lines="8"} from blacksheep.server.controllers import Controller, get @@ -235,9 +246,11 @@ class Greetings(Controller): return self.view() ``` -Then, create a new folder inside `views` directory, called "greetings", and +Then, create a new folder inside `views` directory, named "greetings", and add an HTML file named "hello.jinja". + + ![New view](./img/new-view.png) Copy the following contents into `hello.jinja`: @@ -251,14 +264,14 @@ Copy the following contents into `hello.jinja`: Now navigate to [http://localhost:44777/hello-view](http://localhost:44777/hello-view), to see the response from the new HTML view. -Note how convention over configuration is used in this case, to determine that +Notice how convention over configuration is used in this case, to determine that `./views/greetings/hello.jinja` file must be used, because of the convention:
`./views/{CONTROLLER_NAME}/{METHOD_NAME}.jinja`. The view currently is an HTML fragment, not a full document. To make it a full page, modify `hello.jinja` to use the application layout: -```html +```html {hl_lines="1 11-15"} {%- extends "layout.jinja" -%} {%- block title -%} Hello Page! @@ -282,20 +295,21 @@ full page, modify `hello.jinja` to use the application layout: Refresh the page at [http://localhost:44777/hello-view](http://localhost:44777/hello-view) to see the result. In this case, a page layout is applied using: `{%- extends "layout.jinja" -%}`, -with several blocks going in various areas of `layout.jinja`. For more -information on layouts and features of the templating library, refer to the -[Jinja2 documentation](https://jinja2docs.readthedocs.io/en/stable/). +with several blocks defined in `layout.jinja`. For more information on layouts +and features of the templating library, refer to the [Jinja2 +documentation](https://jinja2docs.readthedocs.io/en/stable/). --- -So far the tutorials only showed the _Controller_ and the _View_ part of the _MVC_ architecture. A _Model_ is a context for an HTML view. -To include dynamic content into an HTML template, use mustaches _`{{name}}`_ -placeholders and pass a model having properties whose names match their key -to the `view` function. +Until now, the tutorial have only demonstrated the _Controller_ and _View_ +components of the _MVC_ architecture. A _Model_ serves as the context for an +HTML view. To include dynamic content in an HTML template, use mustache-style +_`{{name}}`_ placeholders and pass a model with properties whose names match +the placeholders to the `view` function. For example, modify `hello.jinja` to use dynamic content from a model: -```html +```html {hl_lines="5-7"}

Hello, {{name}}!

@@ -309,7 +323,7 @@ For example, modify `hello.jinja` to use dynamic content from a model: and `greetings.py` to contain the following code: -```python +```python {hl_lines="13 22-23"} from dataclasses import dataclass from typing import List from blacksheep.server.controllers import Controller, get @@ -337,7 +351,7 @@ class Greetings(Controller): sentences=[ Sentence( "Check this out!", - "https://github.com/RobertoPrevato/BlackSheep", + "https://github.com/Neoteroi/BlackSheep", ) ], ) @@ -353,10 +367,11 @@ Models can be defined as [dictionaries](https://docs.python.org/3.9/library/stdt implementing a constructor. ## Handling parameters in controllers -The previous tutorial showed how request handlers support the automatic -injection of parameters read from the HTTP request. Controllers support the -same, therefore it is possible to have parameters read automatically and -injected into controller methods: + +The _Getting started guide_ demonstrated how request handlers support the +automatic injection of parameters from HTTP requests. Controllers offer the +same functionality, allowing parameters to be automatically read and passed +into controller methods: ```python class Example(Controller): @@ -371,45 +386,46 @@ class Example(Controller): ``` Controllers also support dependency injection for their constructor -(`__init__` method), this will be explained in the next page. +(`__init__` method) and class properties, this will be explained in the next +page. ## Serving static files -This tutorial previously showed how the homepage of the MVC project template looks -like, at the root of the website: + +The homepage of the MVC project template looks like in the following picture: ![MVC Project home](./img/mvc-template-home.png) -The project template includes a folder for `static` files, including pictures, -CSS, and JavaScript files. Static files are served using a catch-all route, reading -files whose path, relative to the static folder, matches the URL path of the request. +The project template includes a folder for static files, such as images, CSS, +and JavaScript files. Static files are served using a catch-all route that +reads files whose paths, relative to the static folder, match the URL path of +the request. -For example, if the `static` folder contains the file `scripts/example.js`, -web requests at `http://localhost:44777/scripts/example.js` will be resolved -with this file and related information. When handling static files, BlackSheep -automatically takes care of several details: +For example, if the `static` folder contains the file `scripts/example.js`, an +HTTP GET web request to `http://localhost:44777/scripts/example.js` will +resolve to this file and its related information. When serving static files, +BlackSheep automatically handles several tasks: -- it handles the ETag response header, If-None-Match request header and HTTP 304 Not - Modified responses if files don't change on the file system -- it handles HTTP GET requests returning file information -- it handles Range requests, to support pause and restore downloads out of the box - and enable optimal support for videos (videos can be downloaded from a certain - point in time) +- It manages the `ETag` response header, the `If-None-Match` request header, and + `HTTP 304 Not Modified` responses when files remain unchanged on the file + system. +- It processes `HTTP GET` and `HTTP HEAD` requests to return file information. +- It supports Range requests, enabling pause-and-resume downloads and optimal + handling of videos (e.g., downloading videos from a specific point in time). -Try to add a file to the static folder, and download it writing the path in your +Add a file to the static folder and access it by entering its path in your browser. -Relative paths are supported, but only files inside the root static folder are -served, it is not possible to download files outside of the static folder (it would be -a security issue if it worked otherwise!). -Additionally, BlackSheep only handles certain file extensions: by default -only the most common file extensions used in web applications. -Paths starting with "/" are always considered absolute paths starting from the -root of the website. +Relative paths are supported, but only files within the root static folder are +served. It is not possible to download files outside of the static folder, as +this would pose a security risk. Additionally, BlackSheep only handles certain +file extensions by default, specifically the most common ones used in web +applications. Paths starting with '/' are always treated as absolute paths +starting from the root of the website. ## Strategy for application settings The `API` and the `MVC` project templates include a strategy to read and -validate application settings, from various sources, and support multiple +validate application settings from various sources and support multiple system environments (like `dev`, `test`, and `prod` environments). - [`Pydantic`](https://docs.pydantic.dev/latest/) is always used to describe and validate application settings. @@ -434,13 +450,22 @@ general concepts presented here apply to many kinds of web frameworks: - use of MVC architecture The next pages describe the built-in support for -[dependency injection](../dependency-injection), and automatic generation of -[OpenAPI Documentation](../openapi). +[dependency injection](dependency-injection.md), and automatic generation of +[OpenAPI Documentation](openapi.md). + + +/// admonition | For more information... + +For more information about Server Side Rendering, read [_Templating_](/blacksheep/templating/). + +For more information about the BlackSheep CLI, read [_More about the CLI_](/blacksheep/cli/). + +/// + +/// admonition | Don't miss the api project template. + type: tip -!!! info "For more information..." - For more information about Server Side Rendering, read [_Templating_](/blacksheep/templating/).
- For more information about the BlackSheep CLI, read [_More about the CLI_](/blacksheep/cli/). +Try also the `api` project template, to start new Web API projects that +don't handle HTML views. -!!! tip "Don't miss the api project template" - Try also the `api` project template, to start new Web API projects that - don't handle HTML views. +/// diff --git a/blacksheep/docs/openapi.md b/blacksheep/docs/openapi.md index 556c941..1282b15 100644 --- a/blacksheep/docs/openapi.md +++ b/blacksheep/docs/openapi.md @@ -1,4 +1,5 @@ # OpenAPI Documentation + BlackSheep implements automatic generation of OpenAPI Documentation for most common scenarios, and provides methods to enrich the documentation with details. This page describes the following: @@ -12,6 +13,7 @@ details. This page describes the following: - [X] How to implement a custom `UIProvider`. ## Introduction to OpenAPI Documentation + Citing from the [Swagger website](https://swagger.io/specification/), at the time of this writing: @@ -23,15 +25,15 @@ time of this writing: > display the API, code generation tools to generate servers and clients in > various programming languages, testing tools, and many other use cases. -Since a web application knows by definition the paths it is handling, and since -a certain amount of metadata can be inferred from the source code, BlackSheep -implements automatic generation of OpenAPI Documentation, and offers an API -to enrich the documentation with information that cannot be inferred from the +Since a web application inherently knows the paths it handles, and a certain +amount of metadata can be inferred from the source code, BlackSheep provides +automatic generation of OpenAPI documentation. It also offers an API to enhance +the documentation with additional information that cannot be inferred from the source code. -If you followed the [Getting started: MVC](./mvc-project-template) tutorial, -its project template is configured to include an example of OpenAPI -Documentation and to expose a Swagger UI at `/docs` path. +If you followed the [Getting started: MVC](mvc-project-template.md) tutorial, +the project template is preconfigured to include an example of OpenAPI +documentation and to expose a Swagger UI at the `/docs` path. ![OpenAPI Docs](./img/openapi-docs.png) @@ -40,7 +42,7 @@ Documentation and to expose a Swagger UI at `/docs` path. The following piece of code describes a minimal set-up to enable generation of OpenAPI Documentation and exposing a Swagger UI in BlackSheep: -```python +```python {hl_lines="9-10"} from dataclasses import dataclass from blacksheep import Application, get @@ -49,15 +51,16 @@ from openapidocs.v3 import Info app = Application() -docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1")) docs.bind_app(app) + @dataclass class Foo: foo: str + @get("/foo") async def get_foo() -> Foo: return Foo("Hello!") @@ -76,13 +79,14 @@ at `/openapi.json` path: ```json { "openapi": "3.0.3", - "info": { "title": "Example API", + "version": "0.0.1" }, "paths": { "/foo": { "get": { + "responses": { "200": { "description": "Success response", @@ -119,21 +123,22 @@ at `/openapi.json` path: } ``` -Note how the `Foo` component schema is automatically documented. `BlackSheep` +Notice how the `Foo` component schema is automatically documented. BlackSheep supports both `@dataclass` and `Pydantic` models for the automatic generation -of documentation. +of documentation, however support for `Pydantic` is limited. And also YAML format at `/openapi.yaml` path: ```yaml openapi: 3.0.3 -info: title: Example API - version: 0.0.1 + +version: 0.0.1 paths: /foo: get: responses: + '200': description: Success response content: @@ -169,13 +174,14 @@ After this change, the specification file includes the new information: ```yaml openapi: 3.0.3 -info: title: Example API - version: 0.0.1 + +version: 0.0.1 paths: /: get: responses: + '200': description: Returns a text saying OpenAPI Example operationId: home @@ -183,6 +189,7 @@ components: {} ``` ### Adding description and summary + An endpoint description can be specified either using a `docstring`: ```python @@ -213,14 +220,15 @@ and the whole docstring as the description. ![OpenAPI description and summary](./img/openapi-description-summary.png) -!!! info Most of the BlackSheep code base is typed using the `typing` module, + thus IDEs and text editors like Visual Studio Code and PyCharm can provide user's friendly hints for code completion (see the screenshot below). ![Type hints](./img/openapi-docs-type-hints.png) ### Ignoring endpoints + To exclude certain endpoints from the API documentation, use `@docs.ignore()`: ```python @@ -236,7 +244,7 @@ To document only certain routes, use an include function like in the example bel For example, to include only those routes that start with "/api": ```python -docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1")) + # include only endpoints whose path starts with "/api/" docs.include = lambda path, _: path.startswith("/api/") @@ -244,6 +252,7 @@ docs.include = lambda path, _: path.startswith("/api/") ### Documenting response examples + The following example shows how to describe examples for responses: ```python @@ -258,13 +267,14 @@ from openapidocs.v3 import Info app = Application() -docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1")) docs.bind_app(app) + @dataclass class Cat: id: UUID + name: str creation_time: datetime @@ -334,7 +344,7 @@ def get_cat_by_id(cat_id: UUID): ``` To see a complete example, refer to the source code of the [MVC project -template](https://github.com/RobertoPrevato/BlackSheepMVC), and see how +template](https://github.com/Neoteroi/BlackSheep-MVC), and see how documentation is organized and configured (in `app.docs`, `app.controllers.docs`). ### Deprecating an API @@ -369,13 +379,14 @@ class MyOpenAPIHandler(OpenAPIHandler): ] -docs = MyOpenAPIHandler(info=Info(title="Example API", version="0.0.1")) docs.bind_app(app) + ``` ### Handling common responses APIs often implement a common way to handle failures, to provide clients with + details for web requests that cannot be completed successfully. For example, an API might return a response body like the following, in case of a bad request for a certain endpoint: @@ -396,7 +407,7 @@ class ErrorInfo: code: int ``` -`blacksheep` offers the following way to document common responses: +Common responses can be documented this way: ```python from openapidocs.v3 import MediaType, Response as ResponseDoc, Schema @@ -432,22 +443,19 @@ docs.common_responses = { } ``` -Common responses are configured for all endpoints. - - ### Support for generics -The generation of OpenAPI Documentation supports the handling of [generic +The generation of OpenAPI Documentation supports [generic types](https://docs.python.org/3/library/typing.html#typing.Generic). Consider the following example: -1. a common task is to implement an API that returns a paginated subset of - elements, usually given some filters (e.g. textual search) -2. clients need to know the count of items that match the filters, to display - the total number of items and the number of pages that are necessary to - display all results (depending on page size) -3. for such a scenario, using a `Generic` type is a good solution, because many - kinds of objects can be paginated +1. A common use case is implementing an API that returns a paginated subset of + elements, often based on filters (e.g., textual search). +1. Clients need to know the total count of items matching the filters to + display the total number of items and calculate the number of pages required + to show all results (depending on the page size). +1. In such scenarios, using a `Generic` type is an effective solution, as many + kinds of objects can be paginated. _Example of generic class definition_ @@ -465,9 +473,9 @@ class PaginatedSet(Generic[T]): total: int ``` -_Full example illustrating OpenAPI Documentation for generics_ +_Full example illustrating OpenAPI Documentation for generics:_ -```python +```python {linenums="1" hl_lines="11 14-17 36 40"} from dataclasses import dataclass from datetime import datetime from typing import Generic, List, TypeVar @@ -498,10 +506,10 @@ app = Application() # enable OpenAPI Documentation -docs = OpenAPIHandler(info=Info(title="Example", version="0.0.1")) docs.bind_app(app) + @router.get("/api/orders") async def get_orders( page: FromQuery[int] = FromQuery(1), @@ -517,11 +525,11 @@ async def get_orders( In the example below, the generic type is handled properly and produces the following OpenAPI Documentation: -```yaml +```yaml {linenums="1" hl_lines="9 11-14 40-41 61 71"} openapi: 3.0.3 -info: title: Example - version: 0.0.1 + +version: 0.0.1 paths: /api/orders: get: @@ -595,17 +603,20 @@ components: nullable: false ``` -!!! info - Generic types, expressed in Python using `GenericType[T]`, are - represented with `GenericTypeOfT` to respect OpenAPI specification, saying - that `$ref values must be RFC3986-compliant percent-encoded URIs`. - A generic type with more arguments, like `Foo[T, U, X]` gets represented with - `FooOfTAndUAndX`. +/// admonition | Generic types names. + type: info + +Generic types, expressed in Python using `GenericType[T]`, are +represented with `GenericTypeOfT` to respect OpenAPI specification, saying +that `$ref values must be RFC3986-compliant percent-encoded URIs`. +A generic type with more arguments, like `Foo[T, U, X]` gets represented with +`FooOfTAndUAndX`. + +/// ### Describing parameters -It is possible to describe parameters explicitly, using docstrings, or -leveraging `Pydantic`. +It is possible to describe parameters explicitly using docstrings. #### Documenting parameters explicitly @@ -620,13 +631,14 @@ app = Application() # enable OpenAPI Documentation -docs = OpenAPIHandler(info=Info(title="Example", version="0.0.1")) docs.bind_app(app) + @router.get("/api/orders") @docs( parameters={ + "page": ParameterInfo(description="Page number"), "page_size": ParameterInfo( description="The number of items to display per page" @@ -728,13 +740,14 @@ from blacksheep.server.openapi.v3 import OpenAPIHandler from openapidocs.v3 import Info docs = OpenAPIHandler( - info=Info(title="Example API", version="0.0.1"), anonymous_access=True ) + # include only endpoints whose path starts with "/api/" docs.include = lambda path, _: path.startswith("/api/") ``` + ### Support for ReDoc UI BlackSheep supports [ReDoc UI](https://github.com/Redocly/redoc), although @@ -748,13 +761,14 @@ from blacksheep.server.openapi.ui import ReDocUIProvider from openapidocs.v3 import Info docs = OpenAPIHandler( - info=Info(title="Example API", version="0.0.1"), ) + docs.ui_providers.append(ReDocUIProvider()) # include only endpoints whose path starts with "/api/" docs.include = lambda path, _: path.startswith("/api/") + ``` ### How to implement a custom UIProvider @@ -792,13 +806,14 @@ class CustomUIProvider(SwaggerUIProvider): return _template.replace("{options.spec_url}", options.spec_url) -docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1")) # Set the UI provider as desired: + docs.ui_providers = [CustomUIProvider()] docs.bind_app(app) @dataclass + class Foo: foo: str @@ -855,13 +870,14 @@ app = Application() + return _template.replace("{options.spec_url}", options.spec_url) -docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1")) # Set the UI provider as desired: + +docs.ui_providers = [CustomUIProvider()] docs.bind_app(app) ``` ### Changing operations ids + When OpenAPI Documentation is generated, operation ids are obtained from the name of the Python function definitions. diff --git a/blacksheep/docs/openid-connect.md b/blacksheep/docs/openid-connect.md index c874ea9..35a7531 100644 --- a/blacksheep/docs/openid-connect.md +++ b/blacksheep/docs/openid-connect.md @@ -1,6 +1,6 @@ # OpenID Connect -BlackSheep implements built-in support for OpenID Connect authentication, +BlackSheep provides built-in support for OpenID Connect authentication, meaning that it can be easily integrated with identity provider services such as: @@ -9,18 +9,22 @@ as: * [Azure Active Directory B2C](https://docs.microsoft.com/en-us/azure/active-directory-b2c/overview) * [Okta](https://www.okta.com) -This page documents: +This page covers: -- [X] How to use OpenID Connect integration to provide sign-in and sign-up features, +- [X] Using OpenID Connect integration to provide sign-in and sign-up features. and to identify users who use the application -- [X] How to use OpenID Connect integration to obtain `access_token`s to use APIs - (in addition, or instead of `id_token`s) +- [X] Using OpenID Connect integration to obtain `access_token`s to use APIs + (in addition, or instead of `id_token`s). - [X] How tokens are protected and how to configure applications to support - multiple instances and regions + multiple instances and regions. -!!! warning - Using JWT Bearer and OpenID integrations requires more dependencies: use - `pip install blacksheep[full]` to use these features +/// admonition | Additional dependencies. + type: warning + +Using JWT Bearer and OpenID integrations requires additional dependencies. +Install them by running: `pip install blacksheep[full]`. + +/// ## Basic example @@ -74,13 +78,13 @@ async def home(user: Identity): ### use_openid_connect -| Parameter | Type, default | Description | -| ------------------ | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| app | Application | Instance of BlackSheep application. | -| settings | OpenIDSettings | Instance of OpenIDSettings. | +| Parameter | Type, default | Description | +| ------------------ | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| app | Application | Instance of BlackSheep application. | +| settings | OpenIDSettings | Instance of OpenIDSettings. | | auth_handler | Optional[OpenIDTokensHandler] = None (CookiesOpenIDTokensHandler) | Instance of OpenIDTokensHandler that can handle tokens for requests and responses for the OpenID Connect flow. This class is responsible for communicating tokens to clients, and restoring tokens context for following requests. | -| parameters_builder | Optional[ParametersBuilder] = None | Optional instance of `ParametersBuilder`, used to handle parameters configured in redirects and requests to the authorization server. | -| is_default | bool = True | If default, clients are automatically redirected to the `sign-in` page when a non-authenticated user tries to access in `GET` a web page that requires authentication. | +| parameters_builder | Optional[ParametersBuilder] = None | Optional instance of `ParametersBuilder`, used to handle parameters configured in redirects and requests to the authorization server. | +| is_default | bool = True | If default, clients are automatically redirected to the `sign-in` page when a non-authenticated user tries to access in `GET` a web page that requires authentication. | ### OpenIDSettings @@ -100,8 +104,8 @@ The `OpenIDSettings` class has the following properties: | refresh_token_path | str = "/refresh-token" | The local path used to handle refresh tokens to obtain new tokens. | | scope | str = "openid profile email" | The scope of the request, by default an `id_token` is obtained with email and profile. | | response_type | str = "code" | Type of OAuth response. | -| redirect_uri | Optional[str] = None | If specified, the redirect URL that must match the one configured for the application. If not provided, a redirect_url is obtained automatically (see note 🗡️). | -| scheme_name | str = "OpenIDConnect" | The name of the authentication scheme, affecting the name of authentication cookies (see note 🍒). | +| redirect_uri | Optional[str] = None | If specified, the redirect URL that must match the one configured for the application. If not provided, a redirect_url is obtained automatically (see note 🗡️). | +| scheme_name | str = "OpenIDConnect" | The name of the authentication scheme, affecting the name of authentication cookies (see note 🍒). | | error_redirect_path | Optional[str] = None | If specified, the local path to which a user is redirected in case of error. | | end_session_endpoint | Optional[str] = None | If specified, the local path to which the user can log out. | @@ -109,13 +113,13 @@ Notes: * 🗡️ obtaining a redirect_url automatically can require handling of forward headers, when an application is deployed behind a proxy. See - [remotes for more information](../remotes). + [remotes for more information](remotes.md). * 🍒 this should be changed when configuring more than one OIDC identity provider. !!! info `access_token`s issued for APIs can be validated using - [JWT Bearer authentication](../authentication/#jwt-bearer) + [JWT Bearer authentication](authentication.md#jwt-bearer) ## Examples using custom scopes @@ -125,11 +129,11 @@ looks like the following: ```python """ -This example shows how to configure an OpenID Connect integration with Auth0, obtaining -an id_token, an access_token, and a refresh_token. The id_token is exchanged with the -client using a response cookie (also used to authenticate users -for following requests), while the access token and the refresh token are not stored -and can only be accessed using optional events. +This example shows how to configure an OpenID Connect integration with Auth0, +obtaining an id_token, an access_token, and a refresh_token. The id_token is +exchanged with the client using a response cookie (also used to authenticate +users for following requests), while the access token and the refresh token are +not stored and can only be accessed using optional events. """ import uvicorn from blacksheep.server.application import Application @@ -141,7 +145,7 @@ from common.secrets import Secrets load_dotenv() secrets = Secrets.from_env() -app = Application(show_error_details=True) +app = Application() # Auth0 with a custom scope @@ -303,4 +307,4 @@ start-up, and configured as environment settings for the application. DO NOT store secrets that are meant to be used in production under source control. -For more information, refer to [data protection](../dataprotection/). +For more information, refer to [data protection](dataprotection.md). diff --git a/blacksheep/docs/remotes.md b/blacksheep/docs/remotes.md index e79ee0d..cd08931 100644 --- a/blacksheep/docs/remotes.md +++ b/blacksheep/docs/remotes.md @@ -1,34 +1,33 @@ -# Remotes - The `blacksheep.server.remotes` namespace provides classes and functions to handle information related to remote proxies and clients. -Web applications in production environments are commonly hosted behind other -kinds of servers, such as Apache, IIS, or NGINX. Proxy servers usually obscure -some of the information of the original web request before it reaches the web -applications. +Web applications in production environments are often hosted behind servers +such as Apache, IIS, or NGINX. Proxy servers typically obscure some information +from the original web request before it reaches the web application. For example: -* when HTTPS requests are proxied over HTTP, the original scheme (HTTPS) is +- When HTTPS requests are proxied over HTTP, the original scheme (HTTPS) is lost and must be forwarded in a header. -* when an app receives a request from a proxy and not its true source, the - original client IP address must also be forwarded in a header. -* the *path* of web requests can be changed while being proxied (e.g. NGINX - configured to proxy requests to `/example` to the root `/` of a web +- When an application receives a request from a proxy instead of its true + source, the original client IP address must also be forwarded in a header. +- The path of web requests can be altered during proxying (e.g., NGINX + configured to proxy requests from `/example` to the root `/` of a web application). -This information may be important in request processing, for example in -redirects, authentication, link generation when absolute URLs are needed, and -client geolocation. +This information is often critical for request processing, such as in +redirects, authentication, link generation (when absolute URLs are required), +and client geolocation. This page documents how to configure BlackSheep to work +with proxy servers and load balancers, using the provided classes to handle: -This page documents how to configure BlackSheep to work with proxy servers and -load balancers, using the provided classes to handle: +- [X] X-Forwarded headers. +- [X] Forwarded header. +- [X] Trusted hosts. +- [X] How to read information about the original clients in web requests. -- [X] X-Forwarded headers -- [X] Forwarded header -- [X] Trusted hosts -- [X] How to read information about the original clients in web requests +For information on how to handle the prefix of routes when exposing a web +application behind a proxy, refer to the dedicated page +[_Behind Proxies_](./behind-proxies.md). ## Handling X-Forwarded headers @@ -44,9 +43,9 @@ information about original web requests to web applications. BlackSheep provides an `XForwardedHeadersMiddleware` class to handle these headers, providing: -* optional validation of trusted hosts -* optional validation of proxies count and IP addresses by known IPs or known - networks +- Optional validation of trusted hosts. +- Optional validation of proxies count and IP addresses by known IPs or known + networks. To configure a BlackSheep web application to handle `X-Forwarded` headers and configure incoming web requests to expose the correct information about source @@ -81,6 +80,7 @@ When `known_proxies` is not provided, it is set by default to handle `localhost` `[ip_address("127.0.0.1")]`. ## Handling Forwarded header + The `Forwarded` header is a standard header to propagate information about original web requests to web applications. @@ -116,6 +116,7 @@ When `known_proxies` is not provided, it is set by default to handle `localhost` `[ip_address("127.0.0.1")]`. ## Handling trusted hosts + When forwarded headers middlewares are not used, but it is necessary to validate hosts, it is possible to use the `TrustedHostsMiddleware`: @@ -136,6 +137,7 @@ def configure_forwarded_headers(app): ``` ## Reading information about the original clients in web requests + Web requests expose information about the original clients in the following properties, that are updated by forwarded header middlewares: @@ -170,12 +172,8 @@ absolute_url = get_request_absolute_url(request) absolute_url_to_path = get_absolute_url_to_path(request, "/example") ``` -!!! warning - When configuring [OpenID Connect](../authentication/#oidc) authentication, - it can be necessary to handle forwarded headers, so that the application can - generate correct `redirect_uri` for authorization servers. - ## ASGI root_path When the `ASGI` scope includes the `root_path` information, it is automatically -used for the request `base_path` property. +used for the request `base_path` property. For more information on this +subject, refer to the dedicated page [_Behind Proxies_](./behind-proxies.md). diff --git a/blacksheep/docs/request-handlers.md b/blacksheep/docs/request-handlers.md index cc85518..16f48b1 100644 --- a/blacksheep/docs/request-handlers.md +++ b/blacksheep/docs/request-handlers.md @@ -1,15 +1,17 @@ # Request handlers -The previous pages describe that a request handler in BlackSheep is a function -associated with a route, having the responsibility of handling web requests. -This page describes `request handlers` in detail, covering the following: +The previous pages explain that a request handler in BlackSheep is a function +associated with a route, responsible for handling web requests. This page +provides a detailed explanation of request handlers, covering the following +topics: - [X] Request handler normalization. - [X] Using asynchronous and synchronous code. ## Request handler normalization. -A normal request handler in BlackSheep is defined as an asynchronous function -having the following signature: + +A standard request handler in BlackSheep is defined as an asynchronous function +with the following signature: ```python from blacksheep import Request, Response @@ -19,7 +21,7 @@ async def normal_handler(request: Request) -> Response: ``` -To be a request handler, a function must be associated with a route: +For a function to act as a request handler, it must be associated with a route: ```python from blacksheep import Application, Request, Response, get, text @@ -37,11 +39,10 @@ A request handler defined this way is called directly to generate a response when a web request matches the route associated with the function (in this case, HTTP GET at the root of the website "/"). -However, to improve the developer's experience and development speed, -BlackSheep implements automatic normalization of request handlers. For example, -it is possible to define a request handler as a synchronous function, the -framework automatically wraps the synchronous function into an asynchronous -wrapper: +To enhance the developer experience and improve development speed, BlackSheep +implements automatic normalization of request handlers. For instance, a request +handler can be defined as a synchronous function, and the framework will +automatically wrap it in an asynchronous wrapper: ```python @@ -51,13 +52,15 @@ def sync_handler(request: Request) -> Response: ``` -!!! danger "Avoid blocking code in synchronous methods!" - When a request handler is defined as a synchronous method, BlackSheep - assumes that the author of the code knows what they are doing and about - asynchronous programming, and that the response should be returned - immediately without I/O or CPU-intensive operations that would block the - event loop. BlackSheep does nothing to prevent blocking the event loop, if - you add blocking operations in your code. +/// admonition | Avoid blocking code in synchronous methods! + type: danger + +When a request handler is defined as a synchronous function, BlackSheep assumes +that the developer understands asynchronous programming and intends for the +response to be returned immediately without performing I/O or CPU-intensive +operations that could block the event loop. + +/// Similarly, request handlers are normalized when their function signature is different than the normal one. For example, a request handler can be defined @@ -112,15 +115,15 @@ def get_cats(page: int = 1, page_size: int = 30, search: str = "") -> Response: ``` -In the `get_cats` example above, function parameters are read automatically -from the query string and parsed, if present, otherwise default values are +In the `get_cats` example above, function parameters are automatically read +from the query string and parsed if present; otherwise, default values are used. ### Explicit and implicit binding All examples so far showed how to use implicit binding of request parameters. In the `get_cats` example above, all parameters are _implicitly_ bound from the -request query string. To enable more scenarios, `BlackSheep` also provides +request query string. To enable more scenarios, BlackSheep also provides explicit bindings that allow specifying the source of the parameter (e.g. request headers, cookies, route, query, body, application services). In the example below, `cat_input` is read automatically from the request payload as @@ -147,12 +150,12 @@ async def create_cat( ... ``` -More details about bindings are described in _[Binders](../binders/)_. +More details about bindings are described in _[Binders](binders.md)_. ### Normalization and OpenAPI Documentation Request handler normalization also enables a more accurate generation of -[OpenAPI Documentation](../openapi/), since the web framework knows that request +[OpenAPI Documentation](openapi.md), since the web framework knows that request handlers need input from query strings, routes, headers, cookies, etc.; and produce responses of a certain type. @@ -181,21 +184,26 @@ def redirect_example() -> Response: ``` -Request handlers that do I/O bound operations or CPU-intensive operations -should instead be `async`, to not hinder the performance of the web server. For +Request handlers that perform I/O-bound or CPU-intensive operations should be +defined as `async` to avoid hindering the performance of the web server. For example, if information is fetched from a database or a remote API when -handling a web request handler, it is correct to use asynchronous code -to reduce RAM consumption and not block the event loop of the web application. - -!!! warning - If an operation is CPU-intensive (e.g. involving file operations, - resizing a picture), the request handlers that initiate such operation should - be async, and use a [thread or process - pool](https://docs.python.org/3/library/asyncio-eventloop.html#executing-code-in-thread-or-process-pools) - to not block the web app's event loop. - Similarly, request handlers that initiate I/O bound operations (e.g. web - requests to external APIs, connecting to a database) should also be `async`. +handling a web request handler, it is correct to use asynchronous code to +reduce RAM consumption and not block the event loop of the web application. + +/// admonition | CPU and I/O intensive operations. + type: warning + +If an operation is CPU-intensive (e.g. file operations or image resizing), the +request handlers that initiate such operation should be async, and use a +[thread or process +pool](https://docs.python.org/3/library/asyncio-eventloop.html#executing-code-in-thread-or-process-pools) +to not block the web app's event loop. Similarly, request handlers that +initiate I/O bound operations (e.g. web requests to external APIs, connecting +to a database) should also be `async`. + +/// ## Next -The next pages describe [requests](../requests/) and [responses](../responses/) + +The next pages describe [requests](requests.md) and [responses](responses.md) in detail. diff --git a/blacksheep/docs/requests.md b/blacksheep/docs/requests.md index 1491866..ef55df3 100644 --- a/blacksheep/docs/requests.md +++ b/blacksheep/docs/requests.md @@ -1,4 +1,5 @@ # Requests + This page describes: - [X] Handling requests. @@ -7,6 +8,7 @@ This page describes: - [X] Reading request bodies. ## The Request class + BlackSheep handles requests as instances of the `blacksheep.Request` class. This class provides methods and properties to handle request headers, cookies, the URL, route parameters, the request body, the user's identity, and other @@ -14,6 +16,7 @@ information like the content type of the request. Each web request results in the creation of a new instance of `Request`. ### Reading parameters from the request object + It is possible to read query and route parameters from an instance of `request`. The example below shows how the query string, route parameters, and request headers can be read from the request: @@ -112,6 +115,7 @@ def home(accept: FromAcceptHeader, foo: FromFooCookie) -> Response: ``` ### Reading the request body + The request class offers several methods to read request bodies of different kinds. @@ -256,6 +260,7 @@ kinds. ``` #### Reading files + Files read from `multipart/form-data` payload. === "Using binders (recommended)" @@ -284,6 +289,7 @@ Files read from `multipart/form-data` payload. ``` #### Reading streams + Reading streams enables reading large-sized bodies using an asynchronous generator. The example below saves a file of arbitrary size without blocking the event loop: diff --git a/blacksheep/docs/responses.md b/blacksheep/docs/responses.md index 1717aa2..3c93ca6 100644 --- a/blacksheep/docs/responses.md +++ b/blacksheep/docs/responses.md @@ -1,4 +1,5 @@ # Responses + This page describes: - [X] How responses are handled. @@ -6,7 +7,8 @@ This page describes: - [X] Responses using asynchronous generators. ## The Response class -A normal request handler in BlackSheep is expected to return an instance of + +A standard request handler in BlackSheep is expected to return an instance of the `blacksheep.Response` class. Users of the framework can define request handlers that return different kinds of objects. In such cases they are normalized at application start-up to return instances of `Response`. @@ -26,16 +28,16 @@ def home() -> Response: ``` -`BlackSheep` uses these exact types to benefit from static typing and +BlackSheep uses these exact types to benefit from static typing and compilation of [`Cython` extensions](https://cython.org). However, handling responses this way is not comfortable for regular use. For this reason, a number of helper functions are provided to create `Response` objects with a -simpler code API. +more user-friendly code API. For example, the `json` function in `blacksheep.server.responses` produces a response object having a JSON body. -```python +```python {hl_lines="8"} from blacksheep import Application, get, json app = Application() @@ -67,16 +69,13 @@ def home(): return {"message": "Hello, World!"} ``` -Note that, when a request handler doesn't specify a `Response` return type with -type annotations, the framework checks the function's return type at each call -(causing a small performance fee!), and automatically prepares a `Response` -if necessary. +When a request handler doesn't specify a `Response` return type with type +annotations, the framework must check the function's return type at each call +(causing a small performance fee!), and automatically prepares a `Response` if +necessary. ## Functions in `blacksheep.server.responses` -!!! info - Note that you can import these functions from the `blacksheep` package itself. - The table below describes the built-in functions to produce responses: | Method | Description | @@ -103,6 +102,9 @@ The table below describes the built-in functions to produce responses: | **view_async** | Returns a view rendered asynchronously. | | **file** | Returns a binary file response with the given content type and optional file name, for download (attachment) (default HTTP 200 OK). This method supports being called with bytes, or a generator yielding chunks. | +!!! info + These functions can be imported directly from the `blacksheep` namespace. + For information on how to use these methods, refer to the type annotations provided in the code. @@ -119,7 +121,7 @@ To specify response headers use one of the following methods: ```python @get("/") -def home(): +def home() -> Response: response = json({"message": "Hello, World!"}) response.add_header(b"Example", b"Value") @@ -133,11 +135,7 @@ def home(): return response ``` -Note that `BlackSheep` enforces specifying header names and values as `bytes`, -not strings. - -!!! warning - This might change in a future version. +BlackSheep requires specifying header names and values as `bytes`, not strings. ## Setting cookies @@ -229,7 +227,7 @@ Cookie's options: ### Setting many cookies -Use the `Response.set_cookies` method to set several cookies at the same time. +Use the `Response.set_cookies` method to set several cookies at once. ```python @@ -269,8 +267,9 @@ response header containing an instruction to remove a cookie by name. ## Removing cookies Use the `Response.remove_cookie` method to remove a cookie from the response -object before it's sent to the client. Note that this method does not generate -a `Set-Cookie` header. +object before it's sent to the client. This method does **not** generate +a `Set-Cookie` header. Use `unset_cookie` if the intention is to instruct the +client to discard a previously sent cookie. ## Response streaming @@ -284,10 +283,11 @@ the generator will return the correct amount of bytes). ## Chunked encoding -The following example shows how response streaming can be used in responses, -using a `StreamedContent` object bound to a generator yielding bytes. +The following example illustrates how response streaming can be used in +responses, using a `StreamedContent` object bound to a generator yielding +bytes. -```python +```python {hl_lines="9 18"} import asyncio from blacksheep import Application, Response, StreamedContent, get diff --git a/blacksheep/docs/routing.md b/blacksheep/docs/routing.md index fb0b068..6b2b1b4 100644 --- a/blacksheep/docs/routing.md +++ b/blacksheep/docs/routing.md @@ -2,7 +2,7 @@ Server side routing refers to the ability of a web application to handle web requests using different functions, depending on the URL path and HTTP method. -Each `BlackSheep` application is bound to a router, which provides several ways +Each BlackSheep application is bound to a router, which provides several ways to define routes. A function that is bound to a route is called a "request handler", since its responsibility is to handle web requests and produce responses. @@ -75,6 +75,7 @@ async def example(): ``` ### Without decorators + Request handlers can be registered without decorators: ```python @@ -87,11 +88,11 @@ app.router.add_options("/", hello_world) ``` ### Request handlers as class methods + Request handlers can also be configured as class methods, defining classes that -inherit the `blacksheep.server.controllers.Controller` class (name taken from -the MVC architecture): +inherit the `blacksheep.server.controllers.Controller` class: -```python +```python {hl_lines="4 17 22 27 32"} from dataclasses import dataclass from blacksheep import Application, text, json @@ -126,8 +127,10 @@ class Home(Controller): @post("/foo") async def create_foo(self, foo: CreateFooInput): # HTTP POST /foo - # with foo instance automatically injected parsing the request body as JSON - # if the value cannot be parsed as CreateFooInput, Bad Request is returned automatically + # with foo instance automatically injected parsing + # the request body as JSON + # if the value cannot be parsed as CreateFooInput, + # Bad Request is returned automatically return json({"status": True}) ``` @@ -139,33 +142,38 @@ BlackSheep supports three ways to define route parameters: * `"/{example}"` - using curly braces * `"/"` - using angle brackets (i.e. [Flask notation](https://flask.palletsprojects.com/en/1.1.x/quickstart/?highlight=routing#variable-rules)) -Route parameters can be read from `request.route_values`, or injected -automatically by the request handler's function signature: +Route parameters can be read from `request.route_values`, or bound +automatically by the request handler's signature: -```python +=== "Using the signature (recommended)" -@get("/{example}") -def handler(request): - # reading route values from the request object: - value = request.route_values["example"] + ```python {hl_lines="3-4"} + from blacksheep import get - return text(value) + @get("/api/cats/{cat_id}") + def get_cat(cat_id): + # cat_id is bound automatically + ... + ``` +=== "Using the Request object" -@get("/api/cats/{cat_id}") -def get_cat(cat_id): - # cat_id is injected automatically - ... -``` + ```python {hl_lines="3 6"} + from blacksheep import Request, get + + @get("/{example}") + def handler(request: Request): + # reading route values from the request object: + value = request.route_values["example"] + ... + ``` It is also possible to specify the expected type, using `typing` annotations: ```python - @get("/api/cats/{cat_id}") def get_cat(cat_id: int): ... - ``` ```python @@ -175,7 +183,6 @@ from uuid import UUID @get("/api/cats/{cat_id}") def get_cat(cat_id: UUID): ... - ``` In this case, BlackSheep will automatically produce an `HTTP 400 Bad Request` @@ -188,6 +195,7 @@ UUID. ``` ## Value patterns + By default, route parameters are matched by any string until the next slash "/" character. Having the following route: @@ -301,7 +309,7 @@ example, a web request for the root of the service "/" having a request header "X-Area" == "Test" gets the reply of the `test_home` request handler, and without such header the reply of the `home` request handler is returned. -```python +```python {hl_lines="4 6 12-13 17"} from blacksheep import Application, Router @@ -319,7 +327,6 @@ def test_home(): app = Application(router=router) - ``` A router can have filters based on headers, host name, query string parameters, @@ -334,6 +341,8 @@ filter_by_query = Router(params={"version": "1"}) filter_by_host = Router(host="neoteroi.xyz") ``` +### Custom filters + To define a custom filter, define a type of `RouteFilter` and set it using the `filters` parameter: @@ -378,8 +387,8 @@ class Home(Controller): ... ``` -In this case, routes are registered using default singleton routers, used if an -application is instantiated without specifying a router: +In this case, routes are registered using **default singleton routers**, used +if an application is instantiated without specifying a router: ```python from blacksheep import Application @@ -443,7 +452,7 @@ async def home(): Then specify the router when instantiating the application: -```python +```python {hl_lines="3 7"} from blacksheep import Application from app.router import router @@ -476,7 +485,7 @@ post = controllers_router.post Then when defining your controllers: -```python +```python {hl_lines="3 8"} from blacksheep.server.controllers import Controller from app.controllers import get, post @@ -489,7 +498,7 @@ class Home(Controller): ... ``` -```python +```python {hl_lines="3 8"} from blacksheep import Application from app.controllers import controllers_router @@ -500,8 +509,25 @@ app = Application() app.controllers_router = controllers_router ``` -!!! info "About Router and RoutesRegistry" +/// admonition | About Router and RoutesRegistry + +Controller routes use a "RoutesRegistry" to support the dynamic generation +of paths by controller class name. Controller routes are evaluated and +merged into `Application.router` when the application starts. +/// + +## Routing prefix + +In some scenarios, it may be necessary to centrally manage prefixes for all +request handlers. To set a prefix for all routes in a `Router`, use the `prefix` +parameter in its constructor. + +```python +router = Router(prefix="/foo") +``` + +To globally configure a prefix for all routes, use the environment variable +`APP_ROUTE_PREFIX` and specify the desired prefix as its value. - Controller routes use a "RoutesRegistry" to support the dynamic generation - of paths by controller class name. Controller routes are evaluated and - merged into `Application.router` when the application starts. +This feature is intended for applications deployed behind proxies. For more +information, refer to [_Behind proxies_](./behind-proxies.md). diff --git a/blacksheep/docs/server-sent-events.md b/blacksheep/docs/server-sent-events.md index 5b425a8..1c85494 100644 --- a/blacksheep/docs/server-sent-events.md +++ b/blacksheep/docs/server-sent-events.md @@ -45,7 +45,7 @@ async def events_handler() -> AsyncIterable[ServerSentEvent]: and must include at least one `yield` statement like above). In this case the return type annotation on the request handler is mandatory -because the request handler is automatically normalized by BlackSheep. +because the request handler is normalized automatically. BlackSheep supports request handlers defined as asynchronous generators since version `2.0.6` especially for this use case, to support less verbose code @@ -55,15 +55,15 @@ an asynchronous generator yielding bytes. Using `async generators` with custom classes require configuring the type of `Response` that is used to convert those classes into bytes, like described in [the responses page](/responses/#chunked-encoding). -The following example shows how to define a server-sent event route controlling -the `Response` object. +The following example demonstrates how to define a server-sent event route +controlling the `Response` object. ## Defining a server-sent events route controlling the Response object To define a server-sent events route and maintain control of the `Response` object, refer to the following example: -```python +```python {hl_lines="4 11-14 20"} import asyncio from collections.abc import AsyncIterable diff --git a/blacksheep/docs/sessions.md b/blacksheep/docs/sessions.md index 221dfea..02e8cb8 100644 --- a/blacksheep/docs/sessions.md +++ b/blacksheep/docs/sessions.md @@ -1,10 +1,20 @@ # Sessions -BlackSheep implements built-in support for sessions, which are handled through -digitally signed cookies. This page describes how to use sessions with the -built-in classes. +This page describes features to support sessions, handled with digitally signed +cookies. + +/// admonition | Sessions are stored in cookies. + type: danger + +The built-in sessions store data in cookies on the client side. Therefore, web +applications using these features must implement [Anti-Forgery +validation](anti-request-forgery.md) to prevent Cross-Site Request Forgery +(XSRF/CSRF). + +/// ## Enabling sessions + To enable sessions, use the `app.use_sessions` method as in the example below: ```python @@ -27,13 +37,13 @@ def home(request: Request): The `use_sessions` method accepts the following parameters: -| Name | Description | Defaults to | -| --------------- | ------------------------------------------------------------------------------------- | ------------------------------------- | -| secret_key | required secret key used for signing | N/A | -| session_cookie | optional session cookie name | "session" | -| serializer | optional `blacksheep.sessions.Serializer` to serialize and deserialize session values | `blacksheep.sessions.JSONSerializer` | -| signer | optional `itsdangerous.Serializer` to sign and encrypt the session cookie | `itsdangerous.URLSafeTimedSerializer` | -| session_max_age | Optional session max age, in **seconds** | `None` | +| Name | Description | Defaults to | +| --------------- | -------------------------------------------------------------------------------------- | ------------------------------------- | +| secret_key | Required secret key used for signing cookies. | N/A | +| session_cookie | Optional session cookie name. | "session" | +| serializer | Optional `blacksheep.sessions.Serializer` to serialize and deserialize session values. | `blacksheep.sessions.JSONSerializer` | +| signer | Optional `itsdangerous.Serializer` to sign and encrypt the session cookie. | `itsdangerous.URLSafeTimedSerializer` | +| session_max_age | Optional session max age, in **seconds**. | `None` | ```python def use_sessions( @@ -51,7 +61,7 @@ The `use_sessions` method accepts the following parameters: The built-in sessions middleware uses [`itsdangerous`](https://itsdangerous.palletsprojects.com/en/1.1.x/) to sign, encrypt, and verify session cookies. Refer to [data -protection](../dataprotection/) for more information on how tokens are signed +protection](dataprotection.md) for more information on how tokens are signed and encrypted. ## Using sessions diff --git a/blacksheep/docs/settings.md b/blacksheep/docs/settings.md index d50a338..ceef94b 100644 --- a/blacksheep/docs/settings.md +++ b/blacksheep/docs/settings.md @@ -96,7 +96,7 @@ json_settings.use( ### Example: using orjson To use [`orjson`](https://github.com/ijl/orjson) for JSON serialization and -deserialization with the built-in [`responses`](../responses/) and +deserialization with the built-in [`responses`](responses.md) and `JSONContent` class, it can be configured this way: ```python diff --git a/blacksheep/docs/static-files.md b/blacksheep/docs/static-files.md index 038b6b2..ce1a8eb 100644 --- a/blacksheep/docs/static-files.md +++ b/blacksheep/docs/static-files.md @@ -53,6 +53,7 @@ app.serve_files("app/videos", root_path="videos") ``` ## File extensions + Only files with a configured extension are served to the client. By default, only files with these extensions are served (case insensitive check): @@ -83,18 +84,21 @@ app.serve_files("static", extensions={'.foo', '.config'}) ``` ## Accept-Ranges and Range requests -Range requests are enabled and handled by default (since version `0.2.1`), -meaning that BlackSheep supports serving big files with the pause and resume -feature, and serving videos with the possibility to jump to specific points. + +Range requests are enabled and handled by default, meaning that BlackSheep +supports serving big files with the pause and resume feature, and serving +videos and audio files with the possibility to jump to specific points. ## ETag and If-None-Match -`ETag`, `If-None-Match` and HTTP Status 304 Not Modified are handled -automatically, as well as support for `HEAD` requests returning only headers -with information about the files. + +`ETag`, `If-None-Match` and `HTTP Status 304 Not Modified` responses are +handled automatically, as well as support for `HEAD` requests returning only +headers with information about the files. ## Configurable Cache-Control -To control `Cache-Control` `max-age` HTTP header, use `cache_time` parameter -(defaults to 10800 seconds). + +To control `Cache-Control` `max-age` HTTP header, use `cache_time` parameter, +defaulting to 10800 seconds (3 hours). ```python app.serve_files("static", cache_time=90000) @@ -102,17 +106,16 @@ app.serve_files("static", cache_time=90000) ## How to serve SPAs that use HTML5 History API -To serve an SPA that uses HTML5 History API, configure files serving with a -`fallback_document="index.html"` if the index file is called "index.html" (like -it happens in most scenarios). +To serve an SPA that uses the HTML5 History API, configure a +`fallback_document="index.html"` if the index file is called "index.html". -```python +```python {hl_lines="7"} from blacksheep import Application app = Application() app.serve_files( - "/path/to/folder/containing/spa", + "app/static", fallback_document="index.html", ) ``` @@ -121,13 +124,13 @@ If the SPA uses a file with a different name, specify both the index file name and the fallback document to be the same: -```python +```python {hl_lines="7-8"} from blacksheep import Application app = Application() app.serve_files( - "/path/to/folder/containing/spa", + "app/static", index_document="example.html", fallback_document="example.html", ) diff --git a/blacksheep/docs/templating.md b/blacksheep/docs/templating.md index 2aaa578..12d9e35 100644 --- a/blacksheep/docs/templating.md +++ b/blacksheep/docs/templating.md @@ -1,26 +1,27 @@ # Server Side Rendering (SSR) -Server side templating refers to the ability of a web application to generate -HTML pages from templates and dynamic variables. By default, BlackSheep does -this using [`Jinja2` library](https://palletsprojects.com/p/jinja/) by the -[Pallets](https://palletsprojects.com) team, but it supports custom renderers. +Server-side templating refers to a web application's ability to generate HTML +pages using templates and dynamic variables. By default, BlackSheep uses the +[`Jinja2` library](https://palletsprojects.com/p/jinja/) library developed by +the [Pallets](https://palletsprojects.com) team, but it also supports custom +renderers. -This page describes: +This page covers: -- [X] How to configure server side templating. +- [X] Configuring server-side templating. - [X] Returning views using response functions. -- [X] Returning views using the MVC features. +- [X] Returning views with MVC features. - [X] Using alternatives to `Jinja2`. !!! info The [BlackSheep MVC project - template](https://github.com/RobertoPrevato/BlackSheepMVC) includes a + template](https://github.com/Neoteroi/BlackSheep-MVC) includes a ready-to-use solution having an application with templates and layout configured. ## Configuration -This example shows how to use Jinja2 templating engine with BlackSheep: +This example illustrates how to use Jinja2 templating engine with BlackSheep: ```python from blacksheep import Application, get @@ -75,14 +76,13 @@ html_settings.use(JinjaRenderer(enable_async=True)) @get("/") async def home(): return await view_async("home", {"example": "Hello", "foo": "World"}) - ``` ## Loading templates -It is possible to load templates by name including '.jinja', or without file -extension; '.jinja' extension is added automatically. The extension must be -lowercase. +It is possible to load templates by name including `.jinja`, or without file +extension; `.jinja` extension is added automatically if no extension is +specified. The extension must be lowercase. ```python @get("/") @@ -146,7 +146,7 @@ def configure_templating( ## Using alternatives to Jinja2 -To use alternative classes for server side rendering: +To use alternative classes for server-side rendering: 1. Define an implementation of `blacksheep.server.rendering.abc.Renderer` 2. Configure it using `from blacksheep.settings.html import html_settings` diff --git a/blacksheep/docs/testing.md b/blacksheep/docs/testing.md index 7d0c1cb..e52ac50 100644 --- a/blacksheep/docs/testing.md +++ b/blacksheep/docs/testing.md @@ -90,7 +90,7 @@ it, step by step. ### Requirements * The requirements described for the [getting -started tutorial](../getting-started/) +started tutorial](getting-started.md) * Familiarity with test frameworks and common concepts like `fixtures`; if you are not familiar with this subject, read `pytest` documentation for an overview (e.g. [pytest home](https://docs.pytest.org/), [what are @@ -101,7 +101,7 @@ started tutorial](../getting-started/) ### Preparing the project structure Prepare a Python virtual environment, as described in the [getting started -tutorial](../getting-started). In addition to `blacksheep` and `uvicorn`, +tutorial](getting-started.md). In addition to `blacksheep` and `uvicorn`, install the following packages in the virtual environment: ```bash diff --git a/blacksheep/docs/versions/migrating-to-v2.md b/blacksheep/docs/versions/migrating-to-v2.md index fceeec0..e7fc5c0 100644 --- a/blacksheep/docs/versions/migrating-to-v2.md +++ b/blacksheep/docs/versions/migrating-to-v2.md @@ -95,17 +95,17 @@ from blacksheep.server.env import is_development, is_production ## Changes to dependency injection -In v2, `rodi` and `BlackSheep` have been modified to enable alternative +In v2, `rodi` and BlackSheep have been modified to enable alternative implementations of dependency injection. `rodi` now defines a `ContainerProtocol` with a basic API to register and resolve dependencies, and -`BlackSheep` relies on that protocol instead of its specific implementation in +BlackSheep relies on that protocol instead of its specific implementation in `rodi`. For more information, read the [_dedicated part in the Dependency Injection_](/blacksheep/dependency-injection/#the-container-protocol) page. ## Changes to server side rendering -`BlackSheep` v2 has been modified to not be strictly reliant on `Jinja2` for +BlackSheep v2 has been modified to not be strictly reliant on `Jinja2` for template rendering. To achieve this, two new namespaces have been added: - `blacksheep.server.rendering.abc`, defining an abstract `Renderer` class, diff --git a/blacksheep/docs/websocket.md b/blacksheep/docs/websocket.md index ef9d2cc..85a594d 100644 --- a/blacksheep/docs/websocket.md +++ b/blacksheep/docs/websocket.md @@ -1,22 +1,22 @@ # WebSocket -**WebSocket** is a technology that allows creating a persistent, bi-directional -connection between a client and a server. It's mostly used in real-time apps, -chat apps, etc. +**WebSocket** is a technology that enables the creation of a persistent, +bi-directional connection between a client and a server. It is commonly used in +real-time applications, such as chat apps and similar use cases. -BlackSheep is able to handle incoming WebSocket connections if you're using -an ASGI server that supports WebSocket protocol -(for example [Uvicorn](https://www.uvicorn.org/#quickstart) -or [Hypercorn](https://pgjones.gitlab.io/hypercorn/)). +BlackSheep can handle incoming WebSocket connections when used with an ASGI +server that supports the WebSocket protocol (e.g., +[Uvicorn](https://www.uvicorn.org/#quickstart) or +[Hypercorn](https://pgjones.gitlab.io/hypercorn/)). ## Creating a WebSocket route -If you want your request handler to act as a WebSocket handler, use the `ws` -decorator or a corresponding `add_ws` method provided by the app router. Note -that the `ws` decorator doesn't have a default path pattern, so you must pass -it. +To make your request handler function as a WebSocket handler, use the `ws` +decorator or the corresponding `add_ws` method provided by the app router. Note +that the `ws` decorator does not have a default path pattern, so you must +specify one. -You can use route parameters just like with the regular request handlers. +Route parameters can be used in the same way as with regular request handlers. === "Using `ws` decorator" @@ -47,27 +47,35 @@ You can use route parameters just like with the regular request handlers. app.router.add_ws("/ws/{client_id}", ws_handler) ``` -A `WebSocket` object will be bound to a parameter injected into your handler -function when the client tries to connect to the endpoint. +When a client attempts to connect to the endpoint, a `WebSocket` object is +bound to a parameter and injected into your handler function. -!!! warning "Be careful" - Make sure that your function either has a parameter named **websocket** or - a parameter with an arbitrary name, annotated with the `WebSocket` class. - Otherwise, the route will not function properly. +/// admonition | Required function signature. + type: danger + +Make sure that your function either has a parameter named **websocket** or +a parameter type annotated with the `WebSocket` class. +Otherwise, the route will not function properly. + +/// ## Accepting the connection The `WebSocket` class provides the `accept` method to accept a connection, -passing optional parameters to the client. These optional parameters are -**headers** which will be sent back to the client with the handshake response -and **subprotocol** that your application agrees to accept. +allowing you to pass optional parameters to the client. These parameters +include **headers**, which are sent back to the client with the handshake response, +and **subprotocol**, which specifies the protocol your application agrees to use. -!!! info - The [MDN article](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers) - on writing WebSocket servers has some additional information regarding - subprotocols and response headers. +/// admonition | For more information. -```py +The [MDN article](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers) +on writing WebSocket servers has some additional information regarding +subprotocols and response headers. + +/// + + +```python @ws("/ws") async def ws_handler(websocket: WebSocket): # Parameters are optional. @@ -77,16 +85,17 @@ async def ws_handler(websocket: WebSocket): ) ``` -As soon as the connection is accepted, you can start receiving and sending messages. +As soon as the connection is accepted, you can start receiving and sending +messages. ## Communicating with the client -There are 3 helper method pairs to communicate with the client: +There are three pairs of helper method for communicating with the client: `receive_text`/`send_text`, `receive_bytes`/`send_bytes` and `receive_json`/`send_json`. There is also the `receive` method that allows for receiving raw WebSocket -messages. Although most of the time you'll want to use one of the helper +messages. However, in most cases, you will want to use one of the helper methods. All send methods accept an argument of data to be sent. @@ -141,9 +150,7 @@ until either the client disconnects or the server shuts down. ## Handling client disconnect -In the event of a client disconnect, the ASGI server will close the connection -and send the corresponding message to your app. Upon receiving this message -`WebSocket` object will raise the `WebSocketDisconnectError` exception. +If a client disconnects, the `ASGI` server will close the connection and send a corresponding message to your application. When this message is received, the `WebSocket` object raises the `WebSocketDisconnectError` exception. You'll likely want to catch it and handle it somehow. diff --git a/blacksheep/mkdocs.yml b/blacksheep/mkdocs.yml index 7f728a8..936d2fa 100644 --- a/blacksheep/mkdocs.yml +++ b/blacksheep/mkdocs.yml @@ -35,12 +35,12 @@ nav: - HSTS: hsts.md - Remotes: remotes.md - Response features: - - Cache control: cache-control.md - - Compression: compression.md + - Cache control: cache-control.md + - Compression: compression.md - Examples: - - Using Marshmallow: examples/marshmallow.md + - Using Marshmallow: examples/marshmallow.md - Production deployments: - - Behind proxies: behind-proxies.md + - Behind proxies: behind-proxies.md - Settings: settings.md - Binders: binders.md - Background tasks: background-tasks.md @@ -71,15 +71,14 @@ theme: name: Switch to dark mode name: "material" custom_dir: overrides/ - highlightjs: true favicon: img/favicon.ico logo: img/logow.svg icon: repo: fontawesome/brands/github extra: - version: 2 - is_current_version: true + version: 2 + is_current_version: true extra_css: - css/neoteroi.css @@ -90,7 +89,16 @@ extra_javascript: plugins: - search - - neoteroi.contribs + - neoteroi.contribs: + enabled_by_env: "GIT_CONTRIBS_ON" + +validation: + links: + absolute_links: ignore + +watch: + - docs + - overrides markdown_extensions: - admonition @@ -102,6 +110,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.highlight: + use_pygments: true + guess_lang: false + anchor_linenums: true + - pymdownx.blocks.admonition + - pymdownx.blocks.details - pymdownx.tasklist: custom_checkbox: true - pymdownx.tabbed: diff --git a/mkdocs-plugins/docs/contribs.md b/mkdocs-plugins/docs/contribs.md index 10be1d8..8783ed9 100644 --- a/mkdocs-plugins/docs/contribs.md +++ b/mkdocs-plugins/docs/contribs.md @@ -39,17 +39,36 @@ commits count. ## Options -| Name | Description | Type | Default | -| --------------------- | ------------------------------------------------------------------------- | ------------------------------- | ------------------- | -| `contributors_label` | The label text for contributors list. | `str` | "Contributors" | -| `last_modified_label` | The label text for last modified time. | `str` | "Last modified on" | -| `last_modified_time` | Whether to display the last modified time for each page. | `bool` | `True` | -| `time_format` | Format to be used for dates. | `str` | "%Y-%m-%d %H:%M:%S" | -| `contributors` | Information about contributors, use to configure images for contributors. | `list` of `contributor` objects | `[]` | +| Name | Description | Type | Default | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------------------- | +| `contributors_label` | The label text for contributors list. | `str` | "Contributors" | +| `contributors` | Information about contributors, use to configure images for contributors. | `list` of `contributor` objects | `[]` | +| `enabled_by_env` | When specified, the Git contributors plugin is only enabled if such variable exists and its value is in `{"1", "true"}` (case insensitive). | `str` | `""` | +| `last_modified_label` | The label text for last modified time. | `str` | "Last modified on" | +| `last_modified_time` | Whether to display the last modified time for each page. | `bool` | `True` | +| `time_format` | Format to be used for dates. | `str` | "%Y-%m-%d %H:%M:%S" | The plugin by default displays dots with the first two initials of each committer's name, displaying pictures requires explicit configuration, described below. +/// admonition | Keep disabled during local development. + type: warning + +This plugin has a significant impact on the performance of `mkdocs serve` and +`mkdocs build`. It is recommended to keep it disabled during local development +and enable it only in the build job that generates the artifacts for publishing. + +```yaml {hl_lines="2"} + - neoteroi.contribs: + enabled_by_env: "GIT_CONTRIBS_ON" +``` + +```bash +GIT_CONTRIBS_ON=True mkdocs build +``` + +/// + ### Contributor object The following table describes the objects that can be used to provide more @@ -70,7 +89,6 @@ following example: contributors: - email: roberto.prevato@gmail.com image: https://avatars.githubusercontent.com/u/2576032?s=400&u=d8d880e8ed05bb170877dd3d561d8301c4beeeed&v=4 - ``` Contributors are matched by email address, and the image is used if configured. diff --git a/mkdocs-plugins/docs/web/oad.md b/mkdocs-plugins/docs/web/oad.md index 49c101a..003edac 100644 --- a/mkdocs-plugins/docs/web/oad.md +++ b/mkdocs-plugins/docs/web/oad.md @@ -68,10 +68,6 @@ Example result: ![Example result](../img/oad-example-1.png) -!!! danger "Soon changing..." - This feature is currently shipped as plugin for MkDocs, it will be modified - to be an extension for Python Markdown. - ## Supported sources for OpenAPI Documentation | Source | Example | diff --git a/mkdocs-plugins/mkdocs.yml b/mkdocs-plugins/mkdocs.yml index 7c4efdb..77bbd74 100644 --- a/mkdocs-plugins/mkdocs.yml +++ b/mkdocs-plugins/mkdocs.yml @@ -22,6 +22,10 @@ nav: - Neoteroi docs home: "/" theme: + features: + - navigation.footer + - content.code.copy + - content.action.view palette: - scheme: slate toggle: @@ -33,7 +37,6 @@ theme: name: Switch to dark mode name: "material" custom_dir: overrides/ - highlightjs: true favicon: img/neoteroi.ico logo: img/neoteroi-w.svg font: @@ -46,9 +49,18 @@ extra_css: - css/extra.css - css/neoteroi.css +validation: + links: + absolute_links: ignore + +watch: + - docs + - overrides + plugins: - search - - neoteroi.contribs + - neoteroi.contribs: + enabled_by_env: "GIT_CONTRIBS_ON" markdown_extensions: - admonition @@ -63,6 +75,8 @@ markdown_extensions: - pymdownx.arithmatex - pymdownx.betterem: smart_enable: all + - pymdownx.blocks.admonition + - pymdownx.blocks.details - pymdownx.caret - pymdownx.critic - pymdownx.details @@ -78,6 +92,10 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.highlight: + use_pygments: true + guess_lang: false + anchor_linenums: true - pymdownx.tasklist: custom_checkbox: true - pymdownx.tabbed: diff --git a/pack.sh b/pack.sh index c9b6512..248eced 100755 --- a/pack.sh +++ b/pack.sh @@ -15,7 +15,7 @@ for folder in "${folders[@]}" ; do cd $folder - mkdocs build + GIT_CONTRIBS_ON=True mkdocs build # check if there is a copy-archive.sh file, to support including docs # of older versions of the library diff --git a/requirements.txt b/requirements.txt index fa93dc7..a6a7f7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ mkdocs-material==9.6.9 -neoteroi-mkdocs==1.1.0 +neoteroi-mkdocs>=1.1.1 diff --git a/rodi/mkdocs.yml b/rodi/mkdocs.yml index 445bc93..2307c5a 100644 --- a/rodi/mkdocs.yml +++ b/rodi/mkdocs.yml @@ -39,6 +39,14 @@ theme: icon: repo: fontawesome/brands/github +validation: + links: + absolute_links: ignore + +watch: + - docs + - overrides + extra: header_bg_color: "teal" # "#51003c" @@ -51,7 +59,8 @@ extra_javascript: plugins: - search - - neoteroi.contribs + - neoteroi.contribs: + enabled_by_env: "GIT_CONTRIBS_ON" # Use the name you wish here markdown_extensions: - pymdownx.highlight: