mirror of
https://github.com/fastapi/fastapi.git
synced 2026-01-16 10:01:46 -05:00
Compare commits
1 Commits
master
...
translate-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15e88aac9b |
6
.github/workflows/issue-manager.yml
vendored
6
.github/workflows/issue-manager.yml
vendored
@@ -41,15 +41,11 @@ jobs:
|
||||
"message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR.",
|
||||
"reminder": {
|
||||
"before": "P3D",
|
||||
"message": "Heads-up: this will be closed in 3 days unless there's new activity."
|
||||
"message": "Heads-up: this will be closed in 3 days unless there’s new activity."
|
||||
}
|
||||
},
|
||||
"invalid": {
|
||||
"delay": 0,
|
||||
"message": "This was marked as invalid and will be closed now. If this is an error, please provide additional details."
|
||||
},
|
||||
"maybe-ai": {
|
||||
"delay": 0,
|
||||
"message": "This was marked as potentially AI generated and will be closed now. If this is an error, please provide additional details, make sure to read the docs about contributing and AI."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,81 +177,252 @@ as Uvicorn by default will use the port `8000`, the documentation on port `8008`
|
||||
|
||||
### Translations
|
||||
|
||||
/// warning | Attention
|
||||
|
||||
**Update on Translations**
|
||||
|
||||
We're updating the way we handle documentation translations.
|
||||
|
||||
Until now, we invited community members to translate pages via pull requests, which were then reviewed by at least two native speakers. While this has helped bring FastAPI to many more users, we’ve also run into several challenges - some languages have only a few translated pages, others are outdated and hard to maintain over time.
|
||||
To improve this, we’re working on automation tools 🤖 to manage translations more efficiently. Once ready, documentation will be machine-translated and still reviewed by at least two native speakers ✅ before publishing. This will allow us to keep translations up-to-date while reducing the review burden on maintainers.
|
||||
|
||||
What’s changing now:
|
||||
|
||||
* 🚫 We’re no longer accepting new community-submitted translation PRs.
|
||||
|
||||
* ⏳ Existing open PRs will be reviewed and can still be merged if completed within the next 3 weeks (since July 11 2025).
|
||||
|
||||
* 🌐 In the future, we will only support languages where at least three active native speakers are available to review and maintain translations.
|
||||
|
||||
This transition will help us keep translations more consistent and timely while better supporting our contributors 🙌. Thank you to everyone who has contributed so far — your help has been invaluable! 💖
|
||||
|
||||
///
|
||||
|
||||
|
||||
Help with translations is VERY MUCH appreciated! And it can't be done without the help from the community. 🌎 🚀
|
||||
|
||||
Here are the steps to help with translations.
|
||||
|
||||
#### Review Translation PRs
|
||||
|
||||
Translation pull requests are made by LLMs guided with prompts designed by the FastAPI team together with the community of native speakers for each supported language.
|
||||
|
||||
These translations are normally still reviewed by native speakers, and here's where you can help!
|
||||
#### Tips and guidelines
|
||||
|
||||
* Check the currently <a href="https://github.com/fastapi/fastapi/pulls" class="external-link" target="_blank">existing pull requests</a> for your language. You can filter the pull requests by the ones with the label for your language. For example, for Spanish, the label is <a href="https://github.com/fastapi/fastapi/pulls?q=is%3Aopen+sort%3Aupdated-desc+label%3Alang-es+label%3Aawaiting-review" class="external-link" target="_blank">`lang-es`</a>.
|
||||
|
||||
* When reviewing a pull request, it's better not to suggest changes in the same pull request, because it is LLM generated, and it won't be possible to make sure that small individual changes are replicated in other similar sections, or that they are preserved when translating the same content again.
|
||||
|
||||
* Instead of adding suggestions to the translation PR, make the suggestions to the LLM prompt file for that language, in a new PR. For example, for Spanish, the LLM prompt file is at: <a href="https://github.com/fastapi/fastapi/blob/master/docs/es/llm-prompt.md" class="external-link" target="_blank">`docs/es/llm-prompt.md`</a>.
|
||||
* Review those pull requests, requesting changes or approving them. For the languages I don't speak, I'll wait for several others to review the translation before merging.
|
||||
|
||||
/// tip
|
||||
|
||||
You can <a href="https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request" class="external-link" target="_blank">add comments with change suggestions</a> to existing pull requests.
|
||||
|
||||
Check the docs about <a href="https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-request-reviews" class="external-link" target="_blank">adding a pull request review</a> to approve it or request changes.
|
||||
|
||||
///
|
||||
|
||||
#### Subscribe to Notifications for Your Language
|
||||
|
||||
* Check if there's a <a href="https://github.com/fastapi/fastapi/discussions/categories/translations" class="external-link" target="_blank">GitHub Discussion</a> to coordinate translations for your language. You can subscribe to it, and when there's a new pull request to review, an automatic comment will be added to the discussion.
|
||||
|
||||
* If you translate pages, add a single pull request per page translated. That will make it much easier for others to review it.
|
||||
|
||||
* To check the 2-letter code for the language you want to translate, you can use the table <a href="https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes" class="external-link" target="_blank">List of ISO 639-1 codes</a>.
|
||||
|
||||
#### Existing language
|
||||
|
||||
Let's say you want to translate a page for a language that already has translations for some pages, like Spanish.
|
||||
|
||||
In the case of Spanish, the 2-letter code is `es`. So, the directory for Spanish translations is located at `docs/es/`.
|
||||
|
||||
/// tip
|
||||
|
||||
The main ("official") language is English, located at `docs/en/`.
|
||||
|
||||
///
|
||||
|
||||
Now run the live server for the docs in Spanish:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// Use the command "live" and pass the language code as a CLI argument
|
||||
$ python ./scripts/docs.py live es
|
||||
|
||||
<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008
|
||||
<span style="color: green;">[INFO]</span> Start watching changes
|
||||
<span style="color: green;">[INFO]</span> Start detecting changes
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
/// tip
|
||||
|
||||
Alternatively, you can perform the same steps that scripts does manually.
|
||||
|
||||
Go into the language directory, for the Spanish translations it's at `docs/es/`:
|
||||
|
||||
```console
|
||||
$ cd docs/es/
|
||||
```
|
||||
|
||||
Then run `mkdocs` in that directory:
|
||||
|
||||
```console
|
||||
$ mkdocs serve --dev-addr 127.0.0.1:8008
|
||||
```
|
||||
|
||||
///
|
||||
|
||||
Now you can go to <a href="http://127.0.0.1:8008" class="external-link" target="_blank">http://127.0.0.1:8008</a> and see your changes live.
|
||||
|
||||
You will see that every language has all the pages. But some pages are not translated and have an info box at the top, about the missing translation.
|
||||
|
||||
Now let's say that you want to add a translation for the section [Features](features.md){.internal-link target=_blank}.
|
||||
|
||||
* Copy the file at:
|
||||
|
||||
```
|
||||
docs/en/docs/features.md
|
||||
```
|
||||
|
||||
* Paste it in exactly the same location but for the language you want to translate, e.g.:
|
||||
|
||||
```
|
||||
docs/es/docs/features.md
|
||||
```
|
||||
|
||||
/// tip
|
||||
|
||||
Notice that the only change in the path and file name is the language code, from `en` to `es`.
|
||||
|
||||
///
|
||||
|
||||
If you go to your browser you will see that now the docs show your new section (the info box at the top is gone). 🎉
|
||||
|
||||
Now you can translate it all and see how it looks as you save the file.
|
||||
|
||||
#### Don't Translate these Pages
|
||||
|
||||
🚨 Don't translate:
|
||||
|
||||
* Files under `reference/`
|
||||
* `release-notes.md`
|
||||
* `fastapi-people.md`
|
||||
* `external-links.md`
|
||||
* `newsletter.md`
|
||||
* `management-tasks.md`
|
||||
* `management.md`
|
||||
* `contributing.md`
|
||||
|
||||
Some of these files are updated very frequently and a translation would always be behind, or they include the main content from English source files, etc.
|
||||
|
||||
#### Request a New Language
|
||||
|
||||
Let's say that you want to request translations for a language that is not yet translated, not even some pages. For example, Latin.
|
||||
|
||||
* The first step would be for you to find other 2 people that would be willing to be reviewing translation PRs for that language with you.
|
||||
* Once there are at least 3 people that would be willing to commit to help maintain that language, you can continue the next steps.
|
||||
If there is no discussion for that language, you can start by requesting the new language. For that, you can follow these steps:
|
||||
|
||||
* Create a new discussion following the template.
|
||||
* Tag the other 2 people that will help with the language, and ask them to confirm there they will help.
|
||||
* Get a few native speakers to comment on the discussion and commit to help review translations for that language.
|
||||
|
||||
Once there are several people in the discussion, the FastAPI team can evaluate it and can make it an official translation.
|
||||
|
||||
Then the docs will be automatically translated using LLMs, and the team of native speakers can review the translation, and help tweak the LLM prompts.
|
||||
Then the docs will be automatically translated using AI, and the team of native speakers can review the translation, and help tweak the AI prompts.
|
||||
|
||||
Once there's a new translation, for example if docs are updated or there's a new section, there will be a comment in the same discussion with the link to the new translation to review.
|
||||
|
||||
## Automated Code and AI
|
||||
#### New Language
|
||||
|
||||
You are encouraged to use all the tools you want to do your work and contribute as efficiently as possible, this includes AI (LLM) tools, etc. Nevertheless, contributions should have meaningful human intervention, judgement, context, etc.
|
||||
/// note
|
||||
|
||||
If the **human effort** put in a PR, e.g. writing LLM prompts, is **less** than the **effort we would need to put** to **review it**, please **don't** submit the PR.
|
||||
These steps will be performed by the FastAPI team.
|
||||
|
||||
Think of it this way: we can already write LLM prompts or run automated tools ourselves, and that would be faster than reviewing external PRs.
|
||||
///
|
||||
|
||||
### Closing Automated and AI PRs
|
||||
Checking the link from above (List of ISO 639-1 codes), you can see that the 2-letter code for Latin is `la`.
|
||||
|
||||
If we see PRs that seem AI generated or automated in similar ways, we'll flag them and close them.
|
||||
Now you can create a new directory for the new language, running the following script:
|
||||
|
||||
The same applies to comments and descriptions, please don't copy paste the content generated by an LLM.
|
||||
<div class="termy">
|
||||
|
||||
### Human Effort Denial of Service
|
||||
```console
|
||||
// Use the command new-lang, pass the language code as a CLI argument
|
||||
$ python ./scripts/docs.py new-lang la
|
||||
|
||||
Using automated tools and AI to submit PRs or comments that we have to carefully review and handle would be the equivalent of a <a href="https://en.wikipedia.org/wiki/Denial-of-service_attack" class="external-link" target="_blank">Denial-of-service attack</a> on our human effort.
|
||||
Successfully initialized: docs/la
|
||||
```
|
||||
|
||||
It would be very little effort from the person submitting the PR (an LLM prompt) that generates a large amount of effort on our side (carefully reviewing code).
|
||||
</div>
|
||||
|
||||
Please don't do that.
|
||||
Now you can check in your code editor the newly created directory `docs/la/`.
|
||||
|
||||
We'll need to block accounts that spam us with repeated automated PRs or comments.
|
||||
That command created a file `docs/la/mkdocs.yml` with a simple config that inherits everything from the `en` version:
|
||||
|
||||
### Use Tools Wisely
|
||||
```yaml
|
||||
INHERIT: ../en/mkdocs.yml
|
||||
```
|
||||
|
||||
As Uncle Ben said:
|
||||
/// tip
|
||||
|
||||
<blockquote>
|
||||
With great <strike>power</strike> <strong>tools</strong> comes great responsibility.
|
||||
</blockquote>
|
||||
You could also simply create that file with those contents manually.
|
||||
|
||||
Avoid inadvertently doing harm.
|
||||
///
|
||||
|
||||
You have amazing tools at hand, use them wisely to help effectively.
|
||||
That command also created a dummy file `docs/la/index.md` for the main page, you can start by translating that one.
|
||||
|
||||
You can continue with the previous instructions for an "Existing Language" for that process.
|
||||
|
||||
You can make the first pull request with those two files, `docs/la/mkdocs.yml` and `docs/la/index.md`. 🎉
|
||||
|
||||
#### Preview the result
|
||||
|
||||
As already mentioned above, you can use the `./scripts/docs.py` with the `live` command to preview the results (or `mkdocs serve`).
|
||||
|
||||
Once you are done, you can also test it all as it would look online, including all the other languages.
|
||||
|
||||
To do that, first build all the docs:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// Use the command "build-all", this will take a bit
|
||||
$ python ./scripts/docs.py build-all
|
||||
|
||||
Building docs for: en
|
||||
Building docs for: es
|
||||
Successfully built docs for: es
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
This builds all those independent MkDocs sites for each language, combines them, and generates the final output at `./site/`.
|
||||
|
||||
Then you can serve that with the command `serve`:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// Use the command "serve" after running "build-all"
|
||||
$ python ./scripts/docs.py serve
|
||||
|
||||
Warning: this is a very simple server. For development, use mkdocs serve instead.
|
||||
This is here only to preview a site with translations already built.
|
||||
Make sure you run the build-all command first.
|
||||
Serving at: http://127.0.0.1:8008
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
#### Translation specific tips and guidelines
|
||||
|
||||
* Translate only the Markdown documents (`.md`). Do not translate the code examples at `./docs_src`.
|
||||
|
||||
* In code blocks within the Markdown document, translate comments (`# a comment`), but leave the rest unchanged.
|
||||
|
||||
* Do not change anything enclosed in "``" (inline code).
|
||||
|
||||
* In lines starting with `///` translate only the text part after `|`. Leave the rest unchanged.
|
||||
|
||||
* You can translate info boxes like `/// warning` with for example `/// warning | Achtung`. But do not change the word immediately after the `///`, it determines the color of the info box.
|
||||
|
||||
* Do not change the paths in links to images, code files, Markdown documents.
|
||||
|
||||
* However, when a Markdown document is translated, the `#hash-parts` in links to its headings may change. Update these links if possible.
|
||||
* Search for such links in the translated document using the regex `#[^# ]`.
|
||||
* Search in all documents already translated into your language for `your-translated-document.md`. For example VS Code has an option "Edit" -> "Find in Files".
|
||||
* When translating a document, do not "pre-translate" `#hash-parts` that link to headings in untranslated documents.
|
||||
|
||||
@@ -196,11 +196,31 @@ They have contributed source code, documentation, etc. 📦
|
||||
|
||||
There are hundreds of other contributors, you can see them all in the <a href="https://github.com/fastapi/fastapi/graphs/contributors" class="external-link" target="_blank">FastAPI GitHub Contributors page</a>. 👷
|
||||
|
||||
## Top Translators
|
||||
|
||||
These are the **Top Translators**. 🌐
|
||||
|
||||
These users have created the most Pull Requests with [translations to other languages](contributing.md#translations){.internal-link target=_blank} that have been *merged*.
|
||||
|
||||
<div class="user-list user-list-center">
|
||||
|
||||
{% for user in (translators.values() | list)[:50] %}
|
||||
|
||||
{% if user.login not in skip_users %}
|
||||
|
||||
<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Translations: {{ user.count }}</div></div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
## Top Translation Reviewers
|
||||
|
||||
These users are the **Top Translation Reviewers**. 🕵️
|
||||
|
||||
Translation reviewers have the [**power to approve translations**](contributing.md#translations){.internal-link target=_blank} of the documentation. Without them, there wouldn't be documentation in several other languages.
|
||||
I only speak a few languages (and not very well 😅). So, the reviewers are the ones that have the [**power to approve translations**](contributing.md#translations){.internal-link target=_blank} of the documentation. Without them, there wouldn't be documentation in several other languages.
|
||||
|
||||
<div class="user-list user-list-center">
|
||||
{% for user in (translation_reviewers.values() | list)[:50] %}
|
||||
|
||||
@@ -81,14 +81,8 @@ function setupTermynal() {
|
||||
}
|
||||
}
|
||||
saveBuffer();
|
||||
const inputCommands = useLines
|
||||
.filter(line => line.type === "input")
|
||||
.map(line => line.value)
|
||||
.join("\n");
|
||||
node.textContent = inputCommands;
|
||||
const div = document.createElement("div");
|
||||
node.style.display = "none";
|
||||
node.after(div);
|
||||
node.replaceWith(div);
|
||||
const termynal = new Termynal(div, {
|
||||
lineData: useLines,
|
||||
noInit: true,
|
||||
|
||||
@@ -74,7 +74,7 @@ Make sure you use a supported label from the <a href="https://github.com/tiangol
|
||||
* `refactor`: Refactors
|
||||
* This is normally for changes to the internal code that don't change the behavior. Normally it improves maintainability, or enables future features, etc.
|
||||
* `upgrade`: Upgrades
|
||||
* This is for upgrades to direct dependencies from the project, or extra optional dependencies, normally in `pyproject.toml`. So, things that would affect final users, they would end up receiving the upgrade in their code base once they update. But this is not for upgrades to internal dependencies used for development, testing, docs, etc. Those internal dependencies or GitHub Action versions should be marked as `internal`, not `upgrade`.
|
||||
* This is for upgrades to direct dependencies from the project, or extra optional dependencies, normally in `pyproject.toml`. So, things that would affect final users, they would end up receiving the upgrade in their code base once they update. But this is not for upgrades to internal dependencies used for development, testing, docs, etc. Those internal dependencies, normally in `requirements.txt` files or GitHub Action versions should be marked as `internal`, not `upgrade`.
|
||||
* `docs`: Docs
|
||||
* Changes in docs. This includes updating the docs, fixing typos. But it doesn't include changes to translations.
|
||||
* You can normally quickly detect it by going to the "Files changed" tab in the PR and checking if the updated file(s) starts with `docs/en/docs`. The original version of the docs is always in English, so in `docs/en/docs`.
|
||||
@@ -106,25 +106,135 @@ This way, we can notice when there are new translations ready, because they have
|
||||
|
||||
## Merge Translation PRs
|
||||
|
||||
Translations are generated automatically with LLMs and scripts.
|
||||
For Spanish, as I'm a native speaker and it's a language close to me, I will give it a final review myself and in most cases tweak the PR a bit before merging it.
|
||||
|
||||
There's one GitHub Action that can be manually run to add or update translations for a language: <a href="https://github.com/fastapi/fastapi/actions/workflows/translate.yml" class="external-link" target="_blank">`translate.yml`</a>.
|
||||
For the other languages, confirm that:
|
||||
|
||||
For these language translation PRs, confirm that:
|
||||
|
||||
* The PR was automated (authored by @tiangolo), not made by another user.
|
||||
* The title is correct following the instructions above.
|
||||
* It has the labels `lang-all` and `lang-{lang code}`.
|
||||
* If the PR is approved by at least one native speaker, you can merge it.
|
||||
* The PR changes only one Markdown file adding a translation.
|
||||
* Or in some cases, at most two files, if they are small, for the same language, and people reviewed them.
|
||||
* If it's the first translation for that language, it will have additional `mkdocs.yml` files, for those cases follow the instructions below.
|
||||
* The PR doesn't add any additional or extraneous files.
|
||||
* The translation seems to have a similar structure as the original English file.
|
||||
* The translation doesn't seem to change the original content, for example with obvious additional documentation sections.
|
||||
* The translation doesn't use different Markdown structures, for example adding HTML tags when the original didn't have them.
|
||||
* The "admonition" sections, like `tip`, `info`, etc. are not changed or translated. For example:
|
||||
|
||||
```
|
||||
/// tip
|
||||
|
||||
This is a tip.
|
||||
|
||||
///
|
||||
|
||||
```
|
||||
|
||||
looks like this:
|
||||
|
||||
/// tip
|
||||
|
||||
This is a tip.
|
||||
|
||||
///
|
||||
|
||||
...it could be translated as:
|
||||
|
||||
```
|
||||
/// tip
|
||||
|
||||
Esto es un consejo.
|
||||
|
||||
///
|
||||
|
||||
```
|
||||
|
||||
...but needs to keep the exact `tip` keyword. If it was translated to `consejo`, like:
|
||||
|
||||
```
|
||||
/// consejo
|
||||
|
||||
Esto es un consejo.
|
||||
|
||||
///
|
||||
|
||||
```
|
||||
|
||||
it would change the style to the default one, it would look like:
|
||||
|
||||
/// consejo
|
||||
|
||||
Esto es un consejo.
|
||||
|
||||
///
|
||||
|
||||
Those don't have to be translated, but if they are, they need to be written as:
|
||||
|
||||
```
|
||||
/// tip | consejo
|
||||
|
||||
Esto es un consejo.
|
||||
|
||||
///
|
||||
|
||||
```
|
||||
|
||||
Which looks like:
|
||||
|
||||
/// tip | consejo
|
||||
|
||||
Esto es un consejo.
|
||||
|
||||
///
|
||||
|
||||
## First Translation PR
|
||||
|
||||
When there's a first translation for a language, it will have a `docs/{lang code}/docs/index.md` translated file and a `docs/{lang code}/mkdocs.yml`.
|
||||
|
||||
For example, for Bosnian, it would be:
|
||||
|
||||
* `docs/bs/docs/index.md`
|
||||
* `docs/bs/mkdocs.yml`
|
||||
|
||||
The `mkdocs.yml` file will have only the following content:
|
||||
|
||||
```YAML
|
||||
INHERIT: ../en/mkdocs.yml
|
||||
```
|
||||
|
||||
The language code would normally be in the <a href="https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes" class="external-link" target="_blank">ISO 639-1 list of language codes</a>.
|
||||
|
||||
In any case, the language code should be in the file <a href="https://github.com/fastapi/fastapi/blob/master/docs/language_names.yml" class="external-link" target="_blank">docs/language_names.yml</a>.
|
||||
|
||||
There won't be yet a label for the language code, for example, if it was Bosnian, there wouldn't be a `lang-bs`. Before creating the label and adding it to the PR, create the GitHub Discussion:
|
||||
|
||||
* Go to the <a href="https://github.com/fastapi/fastapi/discussions/categories/translations" class="external-link" target="_blank">Translations GitHub Discussions</a>
|
||||
* Create a new discussion with the title `Bosnian Translations` (or the language name in English)
|
||||
* A description of:
|
||||
|
||||
```Markdown
|
||||
## Bosnian translations
|
||||
|
||||
This is the issue to track translations of the docs to Bosnian. 🚀
|
||||
|
||||
Here are the [PRs to review with the label `lang-bs`](https://github.com/fastapi/fastapi/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc+label%3Alang-bs+label%3A%22awaiting-review%22). 🤓
|
||||
```
|
||||
|
||||
Update "Bosnian" with the new language.
|
||||
|
||||
And update the search link to point to the new language label that will be created, like `lang-bs`.
|
||||
|
||||
Create and add the label to that new Discussion just created, like `lang-bs`.
|
||||
|
||||
Then go back to the PR, and add the label, like `lang-bs`, and `lang-all` and `awaiting-review`.
|
||||
|
||||
Now the GitHub action will automatically detect the label `lang-bs` and will post in that Discussion that this PR is waiting to be reviewed.
|
||||
|
||||
## Review PRs
|
||||
|
||||
* If a PR doesn't explain what it does or why, if it seems like it could be useful, ask for more information. Otherwise, feel free to close it.
|
||||
If a PR doesn't explain what it does or why, ask for more information.
|
||||
|
||||
* If a PR seems to be spam, meaningless, only to change statistics (to appear as "contributor") or similar, you can simply mark it as `invalid`, and it will be automatically closed.
|
||||
|
||||
* If a PR seems to be AI generated, and seems like reviewing it would take more time from you than the time it took to write the prompt, mark it as `maybe-ai`, and it will be automatically closed.
|
||||
|
||||
* A PR should have a specific use case that it is solving.
|
||||
A PR should have a specific use case that it is solving.
|
||||
|
||||
* If the PR is for a feature, it should have docs.
|
||||
* Unless it's a feature we want to discourage, like support for a corner case that we don't want users to use.
|
||||
@@ -144,12 +254,27 @@ Every month, a GitHub Action updates the FastAPI People data. Those PRs look lik
|
||||
|
||||
If the tests are passing, you can merge it right away.
|
||||
|
||||
## External Links PRs
|
||||
|
||||
When people add external links they edit this file <a href="https://github.com/fastapi/fastapi/blob/master/docs/en/data/external_links.yml" class="external-link" target="_blank">external_links.yml</a>.
|
||||
|
||||
* Make sure the new link is in the correct category (e.g. "Podcasts") and language (e.g. "Japanese").
|
||||
* A new link should be at the top of its list.
|
||||
* The link URL should work (it should not return a 404).
|
||||
* The content of the link should be about FastAPI.
|
||||
* The new addition should have these fields:
|
||||
* `author`: The name of the author.
|
||||
* `link`: The URL with the content.
|
||||
* `title`: The title of the link (the title of the article, podcast, etc).
|
||||
|
||||
After checking all these things and ensuring the PR has the right labels, you can merge it.
|
||||
|
||||
## Dependabot PRs
|
||||
|
||||
Dependabot will create PRs to update dependencies for several things, and those PRs all look similar, but some are way more delicate than others.
|
||||
|
||||
* If the PR is for a direct dependency, so, Dependabot is modifying `pyproject.toml` in the main dependencies, **don't merge it**. 😱 Let me check it first. There's a good chance that some additional tweaks or updates are needed.
|
||||
* If the PR updates one of the internal dependencies, for example the group `dev` in `pyproject.toml`, or GitHub Action versions, if the tests are passing, the release notes (shown in a summary in the PR) don't show any obvious potential breaking change, you can merge it. 😎
|
||||
* If the PR is for a direct dependency, so, Dependabot is modifying `pyproject.toml`, **don't merge it**. 😱 Let me check it first. There's a good chance that some additional tweaks or updates are needed.
|
||||
* If the PR updates one of the internal dependencies, for example it's modifying `requirements.txt` files, or GitHub Action versions, if the tests are passing, the release notes (shown in a summary in the PR) don't show any obvious potential breaking change, you can merge it. 😎
|
||||
|
||||
## Mark GitHub Discussions Answers
|
||||
|
||||
|
||||
@@ -9,18 +9,10 @@ hide:
|
||||
|
||||
### Docs
|
||||
|
||||
* 🐛 Fix copy button in custom.js. PR [#14722](https://github.com/fastapi/fastapi/pull/14722) by [@fcharrier](https://github.com/fcharrier).
|
||||
* 📝 Add contribution instructions about LLM generated code and comments and automated tools for PRs. PR [#14706](https://github.com/fastapi/fastapi/pull/14706) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 📝 Update docs for management tasks. PR [#14705](https://github.com/fastapi/fastapi/pull/14705) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 📝 Update docs about managing translations. PR [#14704](https://github.com/fastapi/fastapi/pull/14704) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 📝 Update docs for contributing with translations. PR [#14701](https://github.com/fastapi/fastapi/pull/14701) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 📝 Specify language code for code block. PR [#14656](https://github.com/fastapi/fastapi/pull/14656) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||
|
||||
### Translations
|
||||
|
||||
* 🌐 Update portuguese llm-prompt.md. PR [#14702](https://github.com/fastapi/fastapi/pull/14702) by [@ceb10n](https://github.com/ceb10n).
|
||||
* 🌐 Update LLM prompt instructions file for French. PR [#14618](https://github.com/fastapi/fastapi/pull/14618) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🌐 Update translations for ko (add-missing). PR [#14699](https://github.com/fastapi/fastapi/pull/14699) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🌐 Update translations for ko (update-outdated). PR [#14589](https://github.com/fastapi/fastapi/pull/14589) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🌐 Update translations for uk (update-outdated). PR [#14587](https://github.com/fastapi/fastapi/pull/14587) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🌐 Update translations for es (update-outdated). PR [#14686](https://github.com/fastapi/fastapi/pull/14686) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
@@ -6,127 +6,123 @@ Language code: fr.
|
||||
|
||||
### Grammar to use when talking to the reader
|
||||
|
||||
Use the formal grammar (use `vous` instead of `tu`).
|
||||
|
||||
Additionally, in instructional sentences, prefer the present tense for obligations:
|
||||
|
||||
- Prefer `vous devez …` over `vous devrez …`, unless the English source explicitly refers to a future requirement.
|
||||
|
||||
- When translating “make sure (that) … is …”, prefer the indicative after `vous assurer que` (e.g. `Vous devez vous assurer qu'il est …`) instead of the subjunctive (e.g. `qu'il soit …`).
|
||||
Use the formal grammar (use «vous» instead of «tu»).
|
||||
|
||||
### Quotes
|
||||
|
||||
- Convert neutral double quotes (`"`) to French guillemets (`«` and `»`).
|
||||
1) Convert neutral double quotes («"») and English double typographic quotes («“» and «”») to French guillemets (««» and «»»).
|
||||
|
||||
- Do not convert quotes inside code blocks, inline code, paths, URLs, or anything wrapped in backticks.
|
||||
2) In the French docs, guillemets are written without extra spaces: use «texte», not « texte ».
|
||||
|
||||
3) Do not convert quotes inside code blocks, inline code, paths, URLs, or anything wrapped in backticks.
|
||||
|
||||
Examples:
|
||||
|
||||
Source (English):
|
||||
Source (English):
|
||||
|
||||
```
|
||||
"Hello world"
|
||||
“Hello Universe”
|
||||
"He said: 'Hello'"
|
||||
"The module is `__main__`"
|
||||
```
|
||||
«««
|
||||
"Hello world"
|
||||
“Hello Universe”
|
||||
"He said: 'Hello'"
|
||||
"The module is `__main__`"
|
||||
»»»
|
||||
|
||||
Result (French):
|
||||
Result (French):
|
||||
|
||||
```
|
||||
"Hello world"
|
||||
“Hello Universe”
|
||||
"He said: 'Hello'"
|
||||
"The module is `__main__`"
|
||||
```
|
||||
«««
|
||||
«Hello world»
|
||||
«Hello Universe»
|
||||
«He said: 'Hello'»
|
||||
«The module is `__main__`»
|
||||
»»»
|
||||
|
||||
### Ellipsis
|
||||
|
||||
- Make sure there is a space between an ellipsis and a word following or preceding the ellipsis.
|
||||
1) Make sure there is a space between an ellipsis and a word following or preceding the ellipsis.
|
||||
|
||||
Examples:
|
||||
|
||||
Source (English):
|
||||
Source (English):
|
||||
|
||||
```
|
||||
...as we intended.
|
||||
...this would work:
|
||||
...etc.
|
||||
others...
|
||||
More to come...
|
||||
```
|
||||
«««
|
||||
...as we intended.
|
||||
...this would work:
|
||||
...etc.
|
||||
others...
|
||||
More to come...
|
||||
»»»
|
||||
|
||||
Result (French):
|
||||
Result (French):
|
||||
|
||||
```
|
||||
... comme prévu.
|
||||
... cela fonctionnerait :
|
||||
... etc.
|
||||
D'autres ...
|
||||
La suite ...
|
||||
```
|
||||
«««
|
||||
... comme prévu.
|
||||
... cela fonctionnerait :
|
||||
... etc.
|
||||
D'autres ...
|
||||
La suite ...
|
||||
»»»
|
||||
|
||||
- This does not apply in URLs, code blocks, and code snippets. Do not remove or add spaces there.
|
||||
2) This does not apply in URLs, code blocks, and code snippets. Do not remove or add spaces there.
|
||||
|
||||
### Headings
|
||||
|
||||
- Prefer translating headings using the infinitive form (as is common in the existing French docs): `Créer…`, `Utiliser…`, `Ajouter…`.
|
||||
1) Prefer translating headings using the infinitive form (as is common in the existing French docs): «Créer…», «Utiliser…», «Ajouter…».
|
||||
|
||||
- For headings that are instructions written in imperative in English (e.g. `Go check …`), keep them in imperative in French, using the formal grammar (e.g. `Allez voir …`).
|
||||
2) For headings that are instructions written in imperative in English (e.g. “Go check …”), keep them in imperative in French, using the formal grammar (e.g. «Allez voir …»).
|
||||
|
||||
3) Keep heading punctuation as in the source. In particular, keep occurrences of literal « - » (space-hyphen-space) as « - » (the existing French docs use a hyphen here).
|
||||
|
||||
### French instructions about technical terms
|
||||
|
||||
Do not try to translate everything. In particular, keep common programming terms (e.g. `framework`, `endpoint`, `plug-in`, `payload`).
|
||||
Do not try to translate everything. In particular, keep common programming terms when that is the established usage in the French docs (e.g. «framework», «endpoint», «plug-in», «payload»). Use French where the existing docs already consistently use French (e.g. «requête», «réponse»).
|
||||
|
||||
Keep class names, function names, modules, file names, and CLI commands unchanged.
|
||||
|
||||
### List of English terms and their preferred French translations
|
||||
|
||||
Below is a list of English terms and their preferred French translations, separated by a colon (:). Use these translations, do not use your own. If an existing translation does not use these terms, update it to use them.
|
||||
Below is a list of English terms and their preferred French translations, separated by a colon («:»). Use these translations, do not use your own. If an existing translation does not use these terms, update it to use them.
|
||||
|
||||
- /// note | Technical Details»: /// note | Détails techniques
|
||||
- /// note: /// note | Remarque
|
||||
- /// tip: /// tip | Astuce
|
||||
- /// warning: /// warning | Alertes
|
||||
- /// check: /// check | Vérifications
|
||||
- /// info: /// info
|
||||
* «/// note | Technical Details»: «/// note | Détails techniques»
|
||||
* «/// note»: «/// note | Remarque»
|
||||
* «/// tip»: «/// tip | Astuce»
|
||||
* «/// warning»: «/// warning | Attention»
|
||||
* «/// check»: «/// check | vérifier»
|
||||
* «/// info»: «/// info»
|
||||
|
||||
- the docs: les documents
|
||||
- the documentation: la documentation
|
||||
* «the docs»: «les documents»
|
||||
* «the documentation»: «la documentation»
|
||||
|
||||
- Exclude from OpenAPI: Exclusion d'OpenAPI
|
||||
* «framework»: «framework» (do not translate to «cadre»)
|
||||
* «performance»: «performance»
|
||||
|
||||
- framework: framework (do not translate to cadre)
|
||||
- performance: performance
|
||||
* «type hints»: «annotations de type»
|
||||
* «type annotations»: «annotations de type»
|
||||
|
||||
- type hints: annotations de type
|
||||
- type annotations: annotations de type
|
||||
* «autocomplete»: «autocomplétion»
|
||||
* «autocompletion»: «autocomplétion»
|
||||
|
||||
- autocomplete: autocomplétion
|
||||
- autocompletion: autocomplétion
|
||||
* «the request» (what the client sends to the server): «la requête»
|
||||
* «the response» (what the server sends back to the client): «la réponse»
|
||||
|
||||
- the request (what the client sends to the server): la requête
|
||||
- the response (what the server sends back to the client): la réponse
|
||||
* «the request body»: «le corps de la requête»
|
||||
* «the response body»: «le corps de la réponse»
|
||||
|
||||
- the request body: le corps de la requête
|
||||
- the response body: le corps de la réponse
|
||||
* «path operation»: «opération de chemin»
|
||||
* «path operations» (plural): «opérations de chemin»
|
||||
* «path operation function»: «fonction de chemin»
|
||||
* «path operation decorator»: «décorateur d'opération de chemin»
|
||||
|
||||
- path operation: chemin d'accès
|
||||
- path operations (plural): chemins d'accès
|
||||
- path operation function: fonction de chemin d'accès
|
||||
- path operation decorator: décorateur de chemin d'accès
|
||||
* «path parameter»: «paramètre de chemin»
|
||||
* «query parameter»: «paramètre de requête»
|
||||
|
||||
- path parameter: paramètre de chemin
|
||||
- query parameter: paramètre de requête
|
||||
* «the `Request`»: «`Request`» (keep as code identifier)
|
||||
* «the `Response`»: «`Response`» (keep as code identifier)
|
||||
|
||||
- the `Request`: `Request` (keep as code identifier)
|
||||
- the `Response`: `Response` (keep as code identifier)
|
||||
* «deployment»: «déploiement»
|
||||
* «to upgrade»: «mettre à niveau»
|
||||
|
||||
- deployment: déploiement
|
||||
- to upgrade: mettre à niveau
|
||||
* «deprecated»: «déprécié»
|
||||
* «to deprecate»: «déprécier»
|
||||
|
||||
- deprecated: déprécié
|
||||
- to deprecate: déprécier
|
||||
|
||||
- cheat sheet: aide-mémoire
|
||||
- plug-in: plug-in
|
||||
* «cheat sheet»: «aide-mémoire»
|
||||
* «plug-in»: «plug-in»
|
||||
|
||||
@@ -1,503 +0,0 @@
|
||||
# LLM 테스트 파일 { #llm-test-file }
|
||||
|
||||
이 문서는 문서를 번역하는 <abbr title="Large Language Model - 대규모 언어 모델">LLM</abbr>이 `scripts/translate.py`의 `general_prompt`와 `docs/{language code}/llm-prompt.md`의 언어별 프롬프트를 이해하는지 테스트합니다. 언어별 프롬프트는 `general_prompt`에 추가됩니다.
|
||||
|
||||
여기에 추가된 테스트는 언어별 프롬프트를 설계하는 모든 사람이 보게 됩니다.
|
||||
|
||||
사용 방법은 다음과 같습니다:
|
||||
|
||||
* 언어별 프롬프트 `docs/{language code}/llm-prompt.md`를 준비합니다.
|
||||
* 이 문서를 원하는 대상 언어로 새로 번역합니다(예: `translate.py`의 `translate-page` 명령). 그러면 `docs/{language code}/docs/_llm-test.md` 아래에 번역이 생성됩니다.
|
||||
* 번역에서 문제가 없는지 확인합니다.
|
||||
* 필요하다면 언어별 프롬프트, 일반 프롬프트, 또는 영어 문서를 개선합니다.
|
||||
* 그런 다음 번역에서 남아 있는 문제를 수동으로 수정해 좋은 번역이 되게 합니다.
|
||||
* 좋은 번역을 둔 상태에서 다시 번역합니다. 이상적인 결과는 LLM이 더 이상 번역에 변경을 만들지 않는 것입니다. 이는 일반 프롬프트와 언어별 프롬프트가 가능한 한 최선이라는 뜻입니다(때때로 몇 가지 seemingly random 변경을 할 수 있는데, 그 이유는 <a href="https://doublespeak.chat/#/handbook#deterministic-output" class="external-link" target="_blank">LLM은 결정론적 알고리즘이 아니기 때문</a>입니다).
|
||||
|
||||
테스트:
|
||||
|
||||
## 코드 스니펫 { #code-snippets }
|
||||
|
||||
//// tab | 테스트
|
||||
|
||||
다음은 코드 스니펫입니다: `foo`. 그리고 이것은 또 다른 코드 스니펫입니다: `bar`. 그리고 또 하나: `baz quux`.
|
||||
|
||||
////
|
||||
|
||||
//// tab | 정보
|
||||
|
||||
코드 스니펫의 내용은 그대로 두어야 합니다.
|
||||
|
||||
`scripts/translate.py`의 일반 프롬프트에서 `### Content of code snippets` 섹션을 참고하세요.
|
||||
|
||||
////
|
||||
|
||||
## 따옴표 { #quotes }
|
||||
|
||||
//// tab | 테스트
|
||||
|
||||
어제 제 친구가 이렇게 썼습니다: "If you spell incorrectly correctly, you have spelled it incorrectly". 이에 저는 이렇게 답했습니다: "Correct, but 'incorrectly' is incorrectly not '"incorrectly"'"".
|
||||
|
||||
/// note | 참고
|
||||
|
||||
LLM은 아마 이것을 잘못 번역할 것입니다. 흥미로운 점은 재번역할 때 고정된 번역을 유지하는지 여부뿐입니다.
|
||||
|
||||
///
|
||||
|
||||
////
|
||||
|
||||
//// tab | 정보
|
||||
|
||||
프롬프트 설계자는 중립 따옴표를 타이포그래피 따옴표로 변환할지 선택할 수 있습니다. 그대로 두어도 괜찮습니다.
|
||||
|
||||
예를 들어 `docs/de/llm-prompt.md`의 `### Quotes` 섹션을 참고하세요.
|
||||
|
||||
////
|
||||
|
||||
## 코드 스니펫의 따옴표 { #quotes-in-code-snippets }
|
||||
|
||||
//// tab | 테스트
|
||||
|
||||
`pip install "foo[bar]"`
|
||||
|
||||
코드 스니펫에서 문자열 리터럴의 예: `"this"`, `'that'`.
|
||||
|
||||
코드 스니펫에서 문자열 리터럴의 어려운 예: `f"I like {'oranges' if orange else "apples"}"`
|
||||
|
||||
하드코어: `Yesterday, my friend wrote: "If you spell incorrectly correctly, you have spelled it incorrectly". To which I answered: "Correct, but 'incorrectly' is incorrectly not '"incorrectly"'"`
|
||||
|
||||
////
|
||||
|
||||
//// tab | 정보
|
||||
|
||||
... 하지만 코드 스니펫 안의 따옴표는 그대로 유지되어야 합니다.
|
||||
|
||||
////
|
||||
|
||||
## 코드 블록 { #code-blocks }
|
||||
|
||||
//// tab | 테스트
|
||||
|
||||
Bash 코드 예시...
|
||||
|
||||
```bash
|
||||
# 우주에 인사말 출력
|
||||
echo "Hello universe"
|
||||
```
|
||||
|
||||
...그리고 콘솔 코드 예시...
|
||||
|
||||
```console
|
||||
$ <font color="#4E9A06">fastapi</font> run <u style="text-decoration-style:solid">main.py</u>
|
||||
<span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting server
|
||||
Searching for package file structure
|
||||
```
|
||||
|
||||
...그리고 또 다른 콘솔 코드 예시...
|
||||
|
||||
```console
|
||||
// "Code" 디렉터리 생성
|
||||
$ mkdir code
|
||||
// 해당 디렉터리로 이동
|
||||
$ cd code
|
||||
```
|
||||
|
||||
...그리고 Python 코드 예시...
|
||||
|
||||
```Python
|
||||
wont_work() # 이건 동작하지 않습니다 😱
|
||||
works(foo="bar") # 이건 동작합니다 🎉
|
||||
```
|
||||
|
||||
...이상입니다.
|
||||
|
||||
////
|
||||
|
||||
//// tab | 정보
|
||||
|
||||
코드 블록의 코드는(주석을 제외하고) 수정하면 안 됩니다.
|
||||
|
||||
`scripts/translate.py`의 일반 프롬프트에서 `### Content of code blocks` 섹션을 참고하세요.
|
||||
|
||||
////
|
||||
|
||||
## 탭과 색상 박스 { #tabs-and-colored-boxes }
|
||||
|
||||
//// tab | 테스트
|
||||
|
||||
/// info | 정보
|
||||
일부 텍스트
|
||||
///
|
||||
|
||||
/// note | 참고
|
||||
일부 텍스트
|
||||
///
|
||||
|
||||
/// note Technical details | 기술 세부사항
|
||||
일부 텍스트
|
||||
///
|
||||
|
||||
/// check | 확인
|
||||
일부 텍스트
|
||||
///
|
||||
|
||||
/// tip | 팁
|
||||
일부 텍스트
|
||||
///
|
||||
|
||||
/// warning | 경고
|
||||
일부 텍스트
|
||||
///
|
||||
|
||||
/// danger | 위험
|
||||
일부 텍스트
|
||||
///
|
||||
|
||||
////
|
||||
|
||||
//// tab | 정보
|
||||
|
||||
탭과 `Info`/`Note`/`Warning`/등의 블록은 제목 번역을 수직 막대(`|`) 뒤에 추가해야 합니다.
|
||||
|
||||
`scripts/translate.py`의 일반 프롬프트에서 `### Special blocks`와 `### Tab blocks` 섹션을 참고하세요.
|
||||
|
||||
////
|
||||
|
||||
## 웹 및 내부 링크 { #web-and-internal-links }
|
||||
|
||||
//// tab | 테스트
|
||||
|
||||
링크 텍스트는 번역되어야 하고, 링크 주소는 변경되지 않아야 합니다:
|
||||
|
||||
* [위의 제목으로 가는 링크](#code-snippets)
|
||||
* [내부 링크](index.md#installation){.internal-link target=_blank}
|
||||
* <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">외부 링크</a>
|
||||
* <a href="https://fastapi.tiangolo.com/css/styles.css" class="external-link" target="_blank">스타일로 가는 링크</a>
|
||||
* <a href="https://fastapi.tiangolo.com/js/logic.js" class="external-link" target="_blank">스크립트로 가는 링크</a>
|
||||
* <a href="https://fastapi.tiangolo.com/img/foo.jpg" class="external-link" target="_blank">이미지로 가는 링크</a>
|
||||
|
||||
링크 텍스트는 번역되어야 하고, 링크 주소는 번역 페이지를 가리켜야 합니다:
|
||||
|
||||
* <a href="https://fastapi.tiangolo.com/ko/" class="external-link" target="_blank">FastAPI 링크</a>
|
||||
|
||||
////
|
||||
|
||||
//// tab | 정보
|
||||
|
||||
링크는 번역되어야 하지만, 주소는 변경되지 않아야 합니다. 예외는 FastAPI 문서 페이지로 향하는 절대 링크이며, 이 경우 번역 페이지로 연결되어야 합니다.
|
||||
|
||||
`scripts/translate.py`의 일반 프롬프트에서 `### Links` 섹션을 참고하세요.
|
||||
|
||||
////
|
||||
|
||||
## HTML "abbr" 요소 { #html-abbr-elements }
|
||||
|
||||
//// tab | 테스트
|
||||
|
||||
여기 HTML "abbr" 요소로 감싼 몇 가지가 있습니다(일부는 임의로 만든 것입니다):
|
||||
|
||||
### abbr가 전체 문구를 제공 { #the-abbr-gives-a-full-phrase }
|
||||
|
||||
* <abbr title="Getting Things Done - 일을 끝내는 방법론">GTD</abbr>
|
||||
* <abbr title="less than - 보다 작음"><code>lt</code></abbr>
|
||||
* <abbr title="XML Web Token - XML 웹 토큰">XWT</abbr>
|
||||
* <abbr title="Parallel Server Gateway Interface - 병렬 서버 게이트웨이 인터페이스">PSGI</abbr>
|
||||
|
||||
### abbr가 설명을 제공 { #the-abbr-gives-an-explanation }
|
||||
|
||||
* <abbr title="어떤 방식으로든 서로 연결되고 함께 작동하도록 구성된 머신들의 집합입니다.">cluster</abbr>
|
||||
* <abbr title="입력과 출력 계층 사이에 수많은 은닉 계층을 둔 인공 신경망을 사용하는 머신 러닝 방법으로, 이를 통해 포괄적인 내부 구조를 형성합니다">Deep Learning</abbr>
|
||||
|
||||
### abbr가 전체 문구와 설명을 제공 { #the-abbr-gives-a-full-phrase-and-an-explanation }
|
||||
|
||||
* <abbr title="Mozilla Developer Network - 모질라 개발자 네트워크: Firefox를 만드는 사람들이 작성한 개발자용 문서">MDN</abbr>
|
||||
* <abbr title="Input/Output - 입력/출력: 디스크 읽기 또는 쓰기, 네트워크 통신.">I/O</abbr>.
|
||||
|
||||
////
|
||||
|
||||
//// tab | 정보
|
||||
|
||||
"abbr" 요소의 "title" 속성은 몇 가지 구체적인 지침에 따라 번역됩니다.
|
||||
|
||||
번역에서는(영어 단어를 설명하기 위해) 자체 "abbr" 요소를 추가할 수 있으며, LLM은 이를 제거하면 안 됩니다.
|
||||
|
||||
`scripts/translate.py`의 일반 프롬프트에서 `### HTML abbr elements` 섹션을 참고하세요.
|
||||
|
||||
////
|
||||
|
||||
## 제목 { #headings }
|
||||
|
||||
//// tab | 테스트
|
||||
|
||||
### 웹앱 개발하기 - 튜토리얼 { #develop-a-webapp-a-tutorial }
|
||||
|
||||
안녕하세요.
|
||||
|
||||
### 타입 힌트와 -애너테이션 { #type-hints-and-annotations }
|
||||
|
||||
다시 안녕하세요.
|
||||
|
||||
### super- 및 subclasses { #super-and-subclasses }
|
||||
|
||||
다시 안녕하세요.
|
||||
|
||||
////
|
||||
|
||||
//// tab | 정보
|
||||
|
||||
제목에 대한 유일한 강한 규칙은, LLM이 중괄호 안의 해시 부분을 변경하지 않아 링크가 깨지지 않게 하는 것입니다.
|
||||
|
||||
`scripts/translate.py`의 일반 프롬프트에서 `### Headings` 섹션을 참고하세요.
|
||||
|
||||
언어별 지침은 예를 들어 `docs/de/llm-prompt.md`의 `### Headings` 섹션을 참고하세요.
|
||||
|
||||
////
|
||||
|
||||
## 문서에서 사용되는 용어 { #terms-used-in-the-docs }
|
||||
|
||||
//// tab | 테스트
|
||||
|
||||
* 당신
|
||||
* 당신의
|
||||
|
||||
* 예: (e.g.)
|
||||
* 등 (etc.)
|
||||
|
||||
* `int`로서의 `foo`
|
||||
* `str`로서의 `bar`
|
||||
* `list`로서의 `baz`
|
||||
|
||||
* 튜토리얼 - 사용자 가이드
|
||||
* 고급 사용자 가이드
|
||||
* SQLModel 문서
|
||||
* API 문서
|
||||
* 자동 문서
|
||||
|
||||
* Data Science
|
||||
* Deep Learning
|
||||
* Machine Learning
|
||||
* Dependency Injection
|
||||
* HTTP Basic authentication
|
||||
* HTTP Digest
|
||||
* ISO format
|
||||
* JSON Schema 표준
|
||||
* JSON schema
|
||||
* schema definition
|
||||
* Password Flow
|
||||
* Mobile
|
||||
|
||||
* deprecated
|
||||
* designed
|
||||
* invalid
|
||||
* on the fly
|
||||
* standard
|
||||
* default
|
||||
* case-sensitive
|
||||
* case-insensitive
|
||||
|
||||
* 애플리케이션을 서빙하다
|
||||
* 페이지를 서빙하다
|
||||
|
||||
* 앱
|
||||
* 애플리케이션
|
||||
|
||||
* 요청
|
||||
* 응답
|
||||
* 오류 응답
|
||||
|
||||
* 경로 처리
|
||||
* 경로 처리 데코레이터
|
||||
* 경로 처리 함수
|
||||
|
||||
* body
|
||||
* 요청 body
|
||||
* 응답 body
|
||||
* JSON body
|
||||
* form body
|
||||
* file body
|
||||
* 함수 body
|
||||
|
||||
* parameter
|
||||
* body parameter
|
||||
* path parameter
|
||||
* query parameter
|
||||
* cookie parameter
|
||||
* header parameter
|
||||
* form parameter
|
||||
* function parameter
|
||||
|
||||
* event
|
||||
* startup event
|
||||
* 서버 startup
|
||||
* shutdown event
|
||||
* lifespan event
|
||||
|
||||
* handler
|
||||
* event handler
|
||||
* exception handler
|
||||
* 처리하다
|
||||
|
||||
* model
|
||||
* Pydantic model
|
||||
* data model
|
||||
* database model
|
||||
* form model
|
||||
* model object
|
||||
|
||||
* class
|
||||
* base class
|
||||
* parent class
|
||||
* subclass
|
||||
* child class
|
||||
* sibling class
|
||||
* class method
|
||||
|
||||
* header
|
||||
* headers
|
||||
* authorization header
|
||||
* `Authorization` header
|
||||
* forwarded header
|
||||
|
||||
* dependency injection system
|
||||
* dependency
|
||||
* dependable
|
||||
* dependant
|
||||
|
||||
* I/O bound
|
||||
* CPU bound
|
||||
* concurrency
|
||||
* parallelism
|
||||
* multiprocessing
|
||||
|
||||
* env var
|
||||
* environment variable
|
||||
* `PATH`
|
||||
* `PATH` variable
|
||||
|
||||
* authentication
|
||||
* authentication provider
|
||||
* authorization
|
||||
* authorization form
|
||||
* authorization provider
|
||||
* 사용자가 인증한다
|
||||
* 시스템이 사용자를 인증한다
|
||||
|
||||
* CLI
|
||||
* command line interface
|
||||
|
||||
* server
|
||||
* client
|
||||
|
||||
* cloud provider
|
||||
* cloud service
|
||||
|
||||
* development
|
||||
* development stages
|
||||
|
||||
* dict
|
||||
* dictionary
|
||||
* enumeration
|
||||
* enum
|
||||
* enum member
|
||||
|
||||
* encoder
|
||||
* decoder
|
||||
* encode하다
|
||||
* decode하다
|
||||
|
||||
* exception
|
||||
* raise하다
|
||||
|
||||
* expression
|
||||
* statement
|
||||
|
||||
* frontend
|
||||
* backend
|
||||
|
||||
* GitHub discussion
|
||||
* GitHub issue
|
||||
|
||||
* performance
|
||||
* performance optimization
|
||||
|
||||
* return type
|
||||
* return value
|
||||
|
||||
* security
|
||||
* security scheme
|
||||
|
||||
* task
|
||||
* background task
|
||||
* task function
|
||||
|
||||
* template
|
||||
* template engine
|
||||
|
||||
* type annotation
|
||||
* type hint
|
||||
|
||||
* server worker
|
||||
* Uvicorn worker
|
||||
* Gunicorn Worker
|
||||
* worker process
|
||||
* worker class
|
||||
* workload
|
||||
|
||||
* deployment
|
||||
* deploy하다
|
||||
|
||||
* SDK
|
||||
* software development kit
|
||||
|
||||
* `APIRouter`
|
||||
* `requirements.txt`
|
||||
* Bearer Token
|
||||
* breaking change
|
||||
* bug
|
||||
* button
|
||||
* callable
|
||||
* code
|
||||
* commit
|
||||
* context manager
|
||||
* coroutine
|
||||
* database session
|
||||
* disk
|
||||
* domain
|
||||
* engine
|
||||
* fake X
|
||||
* HTTP GET method
|
||||
* item
|
||||
* library
|
||||
* lifespan
|
||||
* lock
|
||||
* middleware
|
||||
* mobile application
|
||||
* module
|
||||
* mounting
|
||||
* network
|
||||
* origin
|
||||
* override
|
||||
* payload
|
||||
* processor
|
||||
* property
|
||||
* proxy
|
||||
* pull request
|
||||
* query
|
||||
* RAM
|
||||
* remote machine
|
||||
* status code
|
||||
* string
|
||||
* tag
|
||||
* web framework
|
||||
* wildcard
|
||||
* return하다
|
||||
* validate하다
|
||||
|
||||
////
|
||||
|
||||
//// tab | 정보
|
||||
|
||||
이것은 문서에서 보이는 (대부분) 기술 용어의 불완전하고 비규범적인 목록입니다. 프롬프트 설계자가 어떤 용어에 대해 LLM에 추가적인 도움이 필요한지 파악하는 데 유용할 수 있습니다. 예를 들어, 좋은 번역을 계속 덜 좋은 번역으로 되돌릴 때, 또는 언어에서 용어의 활용/변화를 처리하는 데 문제가 있을 때 도움이 됩니다.
|
||||
|
||||
예를 들어 `docs/de/llm-prompt.md`의 `### List of English terms and their preferred German translations` 섹션을 참고하세요.
|
||||
|
||||
////
|
||||
@@ -1,247 +0,0 @@
|
||||
# OpenAPI에서 추가 응답 { #additional-responses-in-openapi }
|
||||
|
||||
/// warning | 경고
|
||||
|
||||
이는 꽤 고급 주제입니다.
|
||||
|
||||
**FastAPI**를 막 시작했다면, 이 내용이 필요 없을 수도 있습니다.
|
||||
|
||||
///
|
||||
|
||||
추가 상태 코드, 미디어 타입, 설명 등을 포함한 추가 응답을 선언할 수 있습니다.
|
||||
|
||||
이러한 추가 응답은 OpenAPI 스키마에 포함되므로 API 문서에도 표시됩니다.
|
||||
|
||||
하지만 이러한 추가 응답의 경우, 상태 코드와 콘텐츠를 포함하여 `JSONResponse` 같은 `Response`를 직접 반환하도록 반드시 처리해야 합니다.
|
||||
|
||||
## `model`을 사용한 추가 응답 { #additional-response-with-model }
|
||||
|
||||
*경로 처리 데코레이터*에 `responses` 파라미터를 전달할 수 있습니다.
|
||||
|
||||
이는 `dict`를 받습니다. 키는 각 응답의 상태 코드(예: `200`)이고, 값은 각 응답에 대한 정보를 담은 다른 `dict`입니다.
|
||||
|
||||
각 응답 `dict`에는 `response_model`처럼 Pydantic 모델을 담는 `model` 키가 있을 수 있습니다.
|
||||
|
||||
**FastAPI**는 그 모델을 사용해 JSON Schema를 생성하고, OpenAPI의 올바른 위치에 포함합니다.
|
||||
|
||||
예를 들어, 상태 코드 `404`와 Pydantic 모델 `Message`를 사용하는 다른 응답을 선언하려면 다음과 같이 작성할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/additional_responses/tutorial001_py39.py hl[18,22] *}
|
||||
|
||||
/// note | 참고
|
||||
|
||||
`JSONResponse`를 직접 반환해야 한다는 점을 기억하세요.
|
||||
|
||||
///
|
||||
|
||||
/// info | 정보
|
||||
|
||||
`model` 키는 OpenAPI의 일부가 아닙니다.
|
||||
|
||||
**FastAPI**는 여기에서 Pydantic 모델을 가져와 JSON Schema를 생성하고 올바른 위치에 넣습니다.
|
||||
|
||||
올바른 위치는 다음과 같습니다:
|
||||
|
||||
* 값으로 또 다른 JSON 객체(`dict`)를 가지는 `content` 키 안에:
|
||||
* 미디어 타입(예: `application/json`)을 키로 가지며, 값으로 또 다른 JSON 객체를 포함하고:
|
||||
* `schema` 키가 있고, 그 값이 모델에서 생성된 JSON Schema입니다. 이것이 올바른 위치입니다.
|
||||
* **FastAPI**는 이를 직접 포함하는 대신, OpenAPI의 다른 위치에 있는 전역 JSON Schemas를 참조하도록 여기에서 reference를 추가합니다. 이렇게 하면 다른 애플리케이션과 클라이언트가 그 JSON Schema를 직접 사용할 수 있고, 더 나은 코드 생성 도구 등을 제공할 수 있습니다.
|
||||
|
||||
///
|
||||
|
||||
이 *경로 처리*에 대해 OpenAPI에 생성되는 응답은 다음과 같습니다:
|
||||
|
||||
```JSON hl_lines="3-12"
|
||||
{
|
||||
"responses": {
|
||||
"404": {
|
||||
"description": "Additional Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Item"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
스키마는 OpenAPI 스키마 내부의 다른 위치를 참조합니다:
|
||||
|
||||
```JSON hl_lines="4-16"
|
||||
{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Message": {
|
||||
"title": "Message",
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"title": "Message",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Item": {
|
||||
"title": "Item",
|
||||
"required": [
|
||||
"id",
|
||||
"value"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"title": "Id",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"title": "Value",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": [
|
||||
"loc",
|
||||
"msg",
|
||||
"type"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"msg": {
|
||||
"title": "Message",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"title": "Error Type",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 주요 응답에 대한 추가 미디어 타입 { #additional-media-types-for-the-main-response }
|
||||
|
||||
같은 `responses` 파라미터를 사용해 동일한 주요 응답에 대해 다른 미디어 타입을 추가할 수도 있습니다.
|
||||
|
||||
예를 들어, *경로 처리*가 JSON 객체(미디어 타입 `application/json`) 또는 PNG 이미지(미디어 타입 `image/png`)를 반환할 수 있다고 선언하기 위해 `image/png`라는 추가 미디어 타입을 추가할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *}
|
||||
|
||||
/// note | 참고
|
||||
|
||||
이미지는 `FileResponse`를 사용해 직접 반환해야 한다는 점에 유의하세요.
|
||||
|
||||
///
|
||||
|
||||
/// info | 정보
|
||||
|
||||
`responses` 파라미터에서 다른 미디어 타입을 명시적으로 지정하지 않는 한, FastAPI는 응답이 주요 응답 클래스와 동일한 미디어 타입(기본값 `application/json`)을 가진다고 가정합니다.
|
||||
|
||||
하지만 커스텀 응답 클래스를 지정하면서 미디어 타입을 `None`으로 설정했다면, FastAPI는 연결된 모델이 있는 모든 추가 응답에 대해 `application/json`을 사용합니다.
|
||||
|
||||
///
|
||||
|
||||
## 정보 결합하기 { #combining-information }
|
||||
|
||||
`response_model`, `status_code`, `responses` 파라미터를 포함해 여러 위치의 응답 정보를 결합할 수도 있습니다.
|
||||
|
||||
기본 상태 코드 `200`(또는 필요하다면 커스텀 코드)을 사용하여 `response_model`을 선언하고, 그와 동일한 응답에 대한 추가 정보를 `responses`에서 OpenAPI 스키마에 직접 선언할 수 있습니다.
|
||||
|
||||
**FastAPI**는 `responses`의 추가 정보를 유지하고, 모델의 JSON Schema와 결합합니다.
|
||||
|
||||
예를 들어, Pydantic 모델을 사용하고 커스텀 `description`을 가진 상태 코드 `404` 응답을 선언할 수 있습니다.
|
||||
|
||||
또한 `response_model`을 사용하는 상태 코드 `200` 응답을 선언하되, 커스텀 `example`을 포함할 수도 있습니다:
|
||||
|
||||
{* ../../docs_src/additional_responses/tutorial003_py39.py hl[20:31] *}
|
||||
|
||||
이 모든 내용은 OpenAPI에 결합되어 포함되고, API 문서에 표시됩니다:
|
||||
|
||||
<img src="/img/tutorial/additional-responses/image01.png">
|
||||
|
||||
## 미리 정의된 응답과 커스텀 응답 결합하기 { #combine-predefined-responses-and-custom-ones }
|
||||
|
||||
여러 *경로 처리*에 적용되는 미리 정의된 응답이 필요할 수도 있지만, 각 *경로 처리*마다 필요한 커스텀 응답과 결합하고 싶을 수도 있습니다.
|
||||
|
||||
그런 경우 Python의 `dict` “unpacking” 기법인 `**dict_to_unpack`을 사용할 수 있습니다:
|
||||
|
||||
```Python
|
||||
old_dict = {
|
||||
"old key": "old value",
|
||||
"second old key": "second old value",
|
||||
}
|
||||
new_dict = {**old_dict, "new key": "new value"}
|
||||
```
|
||||
|
||||
여기서 `new_dict`는 `old_dict`의 모든 키-값 쌍에 더해 새 키-값 쌍까지 포함합니다:
|
||||
|
||||
```Python
|
||||
{
|
||||
"old key": "old value",
|
||||
"second old key": "second old value",
|
||||
"new key": "new value",
|
||||
}
|
||||
```
|
||||
|
||||
이 기법을 사용해 *경로 처리*에서 일부 미리 정의된 응답을 재사용하고, 추가 커스텀 응답과 결합할 수 있습니다.
|
||||
|
||||
예를 들어:
|
||||
|
||||
{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *}
|
||||
|
||||
## OpenAPI 응답에 대한 추가 정보 { #more-information-about-openapi-responses }
|
||||
|
||||
응답에 정확히 무엇을 포함할 수 있는지 보려면, OpenAPI 사양의 다음 섹션을 확인하세요:
|
||||
|
||||
* <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#responses-object" class="external-link" target="_blank">OpenAPI Responses Object</a>: `Response Object`를 포함합니다.
|
||||
* <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#response-object" class="external-link" target="_blank">OpenAPI Response Object</a>: `responses` 파라미터 안의 각 응답에 이것의 어떤 항목이든 직접 포함할 수 있습니다. `description`, `headers`, `content`(여기에서 서로 다른 미디어 타입과 JSON Schema를 선언합니다), `links` 등을 포함할 수 있습니다.
|
||||
@@ -1,466 +0,0 @@
|
||||
# 프록시 뒤에서 실행하기 { #behind-a-proxy }
|
||||
|
||||
많은 경우 FastAPI 앱 앞단에 Traefik이나 Nginx 같은 **프록시(proxy)**를 두고 사용합니다.
|
||||
|
||||
이런 프록시는 HTTPS 인증서 처리 등 여러 작업을 담당할 수 있습니다.
|
||||
|
||||
## 프록시 전달 헤더 { #proxy-forwarded-headers }
|
||||
|
||||
애플리케이션 앞단의 **프록시**는 보통 **서버**로 요청을 보내기 전에, 해당 요청이 프록시에 의해 **전달(forwarded)**되었다는 것을 서버가 알 수 있도록 몇몇 헤더를 동적으로 설정합니다. 이를 통해 서버는 도메인을 포함한 원래의 (공개) URL, HTTPS 사용 여부 등 정보를 알 수 있습니다.
|
||||
|
||||
**서버** 프로그램(예: **FastAPI CLI**를 통해 실행되는 **Uvicorn**)은 이런 헤더를 해석할 수 있고, 그 정보를 애플리케이션으로 전달할 수 있습니다.
|
||||
|
||||
하지만 보안상, 서버는 자신이 신뢰할 수 있는 프록시 뒤에 있다는 것을 모르면 해당 헤더를 해석하지 않습니다.
|
||||
|
||||
/// note | 기술 세부사항
|
||||
|
||||
프록시 헤더는 다음과 같습니다:
|
||||
|
||||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For" class="external-link" target="_blank">X-Forwarded-For</a>
|
||||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto" class="external-link" target="_blank">X-Forwarded-Proto</a>
|
||||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Host" class="external-link" target="_blank">X-Forwarded-Host</a>
|
||||
|
||||
///
|
||||
|
||||
### 프록시 전달 헤더 활성화하기 { #enable-proxy-forwarded-headers }
|
||||
|
||||
FastAPI CLI를 *CLI 옵션* `--forwarded-allow-ips`로 실행하고, 전달 헤더를 읽을 수 있도록 신뢰할 IP 주소들을 넘길 수 있습니다.
|
||||
|
||||
`--forwarded-allow-ips="*"`로 설정하면 들어오는 모든 IP를 신뢰합니다.
|
||||
|
||||
**서버**가 신뢰할 수 있는 **프록시** 뒤에 있고 프록시만 서버에 접근한다면, 이는 해당 **프록시**의 IP가 무엇이든 간에 받아들이게 됩니다.
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi run --forwarded-allow-ips="*"
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
### HTTPS에서 리디렉션 { #redirects-with-https }
|
||||
|
||||
예를 들어, *경로 처리* `/items/`를 정의했다고 해봅시다:
|
||||
|
||||
{* ../../docs_src/behind_a_proxy/tutorial001_01_py39.py hl[6] *}
|
||||
|
||||
클라이언트가 `/items`로 접근하면, 기본적으로 `/items/`로 리디렉션됩니다.
|
||||
|
||||
하지만 *CLI 옵션* `--forwarded-allow-ips`를 설정하기 전에는 `http://localhost:8000/items/`로 리디렉션될 수 있습니다.
|
||||
|
||||
그런데 애플리케이션이 `https://mysuperapp.com`에 호스팅되어 있고, 리디렉션도 `https://mysuperapp.com/items/`로 되어야 할 수 있습니다.
|
||||
|
||||
이때 `--proxy-headers`를 설정하면 FastAPI가 올바른 위치로 리디렉션할 수 있습니다. 😎
|
||||
|
||||
```
|
||||
https://mysuperapp.com/items/
|
||||
```
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
HTTPS에 대해 더 알아보려면 가이드 [HTTPS에 대하여](../deployment/https.md){.internal-link target=_blank}를 확인하세요.
|
||||
|
||||
///
|
||||
|
||||
### 프록시 전달 헤더가 동작하는 방식 { #how-proxy-forwarded-headers-work }
|
||||
|
||||
다음은 **프록시**가 클라이언트와 **애플리케이션 서버** 사이에서 전달 헤더를 추가하는 과정을 시각적으로 나타낸 것입니다:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Proxy as Proxy/Load Balancer
|
||||
participant Server as FastAPI Server
|
||||
|
||||
Client->>Proxy: HTTPS Request<br/>Host: mysuperapp.com<br/>Path: /items
|
||||
|
||||
Note over Proxy: Proxy adds forwarded headers
|
||||
|
||||
Proxy->>Server: HTTP Request<br/>X-Forwarded-For: [client IP]<br/>X-Forwarded-Proto: https<br/>X-Forwarded-Host: mysuperapp.com<br/>Path: /items
|
||||
|
||||
Note over Server: Server interprets headers<br/>(if --forwarded-allow-ips is set)
|
||||
|
||||
Server->>Proxy: HTTP Response<br/>with correct HTTPS URLs
|
||||
|
||||
Proxy->>Client: HTTPS Response
|
||||
```
|
||||
|
||||
**프록시**는 원래의 클라이언트 요청을 가로채고, **애플리케이션 서버**로 요청을 전달하기 전에 특수한 *forwarded* 헤더(`X-Forwarded-*`)를 추가합니다.
|
||||
|
||||
이 헤더들은 그렇지 않으면 사라질 수 있는 원래 요청의 정보를 보존합니다:
|
||||
|
||||
* **X-Forwarded-For**: 원래 클라이언트의 IP 주소
|
||||
* **X-Forwarded-Proto**: 원래 프로토콜(`https`)
|
||||
* **X-Forwarded-Host**: 원래 호스트(`mysuperapp.com`)
|
||||
|
||||
**FastAPI CLI**를 `--forwarded-allow-ips`로 설정하면, 이 헤더를 신뢰하고 사용합니다. 예를 들어 리디렉션에서 올바른 URL을 생성하는 데 사용됩니다.
|
||||
|
||||
## 제거된 경로 접두사를 가진 프록시 { #proxy-with-a-stripped-path-prefix }
|
||||
|
||||
애플리케이션에 경로 접두사(prefix)를 추가하는 프록시를 둘 수도 있습니다.
|
||||
|
||||
이런 경우 `root_path`를 사용해 애플리케이션을 구성할 수 있습니다.
|
||||
|
||||
`root_path`는 (FastAPI가 Starlette를 통해 기반으로 하는) ASGI 사양에서 제공하는 메커니즘입니다.
|
||||
|
||||
`root_path`는 이러한 특정 사례를 처리하는 데 사용됩니다.
|
||||
|
||||
또한 서브 애플리케이션을 마운트할 때 내부적으로도 사용됩니다.
|
||||
|
||||
경로 접두사가 제거(stripped)되는 프록시가 있다는 것은, 코드에서는 `/app`에 경로를 선언하지만, 위에 한 겹(프록시)을 추가해 **FastAPI** 애플리케이션을 `/api/v1` 같은 경로 아래에 두는 것을 의미합니다.
|
||||
|
||||
이 경우 원래 경로 `/app`은 실제로 `/api/v1/app`에서 서비스됩니다.
|
||||
|
||||
코드는 모두 `/app`만 있다고 가정하고 작성되어 있는데도 말입니다.
|
||||
|
||||
{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[6] *}
|
||||
|
||||
그리고 프록시는 요청을 앱 서버(아마 FastAPI CLI를 통해 실행되는 Uvicorn)로 전달하기 전에, 동적으로 **경로 접두사**를 **"제거"**합니다. 그래서 애플리케이션은 여전히 `/app`에서 서비스된다고 믿게 되고, 코드 전체를 `/api/v1` 접두사를 포함하도록 수정할 필요가 없어집니다.
|
||||
|
||||
여기까지는 보통 정상적으로 동작합니다.
|
||||
|
||||
하지만 통합 문서 UI(프론트엔드)를 열면, OpenAPI 스키마를 `/api/v1/openapi.json`이 아니라 `/openapi.json`에서 가져오려고 합니다.
|
||||
|
||||
그래서 브라우저에서 실행되는 프론트엔드는 `/openapi.json`에 접근하려고 시도하지만 OpenAPI 스키마를 얻지 못합니다.
|
||||
|
||||
앱에 대해 `/api/v1` 경로 접두사를 가진 프록시가 있으므로, 프론트엔드는 `/api/v1/openapi.json`에서 OpenAPI 스키마를 가져와야 합니다.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
|
||||
browser("Browser")
|
||||
proxy["Proxy on http://0.0.0.0:9999/api/v1/app"]
|
||||
server["Server on http://127.0.0.1:8000/app"]
|
||||
|
||||
browser --> proxy
|
||||
proxy --> server
|
||||
```
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
IP `0.0.0.0`은 보통 해당 머신/서버에서 사용 가능한 모든 IP에서 프로그램이 리슨한다는 의미로 사용됩니다.
|
||||
|
||||
///
|
||||
|
||||
문서 UI는 또한 OpenAPI 스키마에서 이 API `server`가 `/api/v1`(프록시 뒤) 위치에 있다고 선언해야 합니다. 예:
|
||||
|
||||
```JSON hl_lines="4-8"
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
// More stuff here
|
||||
"servers": [
|
||||
{
|
||||
"url": "/api/v1"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
// More stuff here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
이 예시에서 "Proxy"는 **Traefik** 같은 것이고, 서버는 **Uvicorn**으로 실행되는 FastAPI CLI처럼, FastAPI 애플리케이션을 실행하는 구성일 수 있습니다.
|
||||
|
||||
### `root_path` 제공하기 { #providing-the-root-path }
|
||||
|
||||
이를 달성하려면 다음처럼 커맨드 라인 옵션 `--root-path`를 사용할 수 있습니다:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Hypercorn을 사용한다면, Hypercorn에도 `--root-path` 옵션이 있습니다.
|
||||
|
||||
/// note | 기술 세부사항
|
||||
|
||||
ASGI 사양은 이 사용 사례를 위해 `root_path`를 정의합니다.
|
||||
|
||||
그리고 커맨드 라인 옵션 `--root-path`가 그 `root_path`를 제공합니다.
|
||||
|
||||
///
|
||||
|
||||
### 현재 `root_path` 확인하기 { #checking-the-current-root-path }
|
||||
|
||||
요청마다 애플리케이션에서 사용 중인 현재 `root_path`를 얻을 수 있는데, 이는 `scope` 딕셔너리(ASGI 사양의 일부)에 포함되어 있습니다.
|
||||
|
||||
여기서는 데모 목적을 위해 메시지에 포함하고 있습니다.
|
||||
|
||||
{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[8] *}
|
||||
|
||||
그 다음 Uvicorn을 다음과 같이 시작하면:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
응답은 다음과 비슷할 것입니다:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"message": "Hello World",
|
||||
"root_path": "/api/v1"
|
||||
}
|
||||
```
|
||||
|
||||
### FastAPI 앱에서 `root_path` 설정하기 { #setting-the-root-path-in-the-fastapi-app }
|
||||
|
||||
또는 `--root-path` 같은 커맨드 라인 옵션(또는 동등한 방법)을 제공할 수 없는 경우, FastAPI 앱을 생성할 때 `root_path` 파라미터를 설정할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/behind_a_proxy/tutorial002_py39.py hl[3] *}
|
||||
|
||||
`FastAPI`에 `root_path`를 전달하는 것은 Uvicorn이나 Hypercorn에 커맨드 라인 옵션 `--root-path`를 전달하는 것과 동일합니다.
|
||||
|
||||
### `root_path`에 대하여 { #about-root-path }
|
||||
|
||||
서버(Uvicorn)는 그 `root_path`를 앱에 전달하는 것 외에는 다른 용도로 사용하지 않는다는 점을 기억하세요.
|
||||
|
||||
하지만 브라우저로 <a href="http://127.0.0.1:8000/app" class="external-link" target="_blank">http://127.0.0.1:8000/app</a>에 접속하면 정상 응답을 볼 수 있습니다:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"message": "Hello World",
|
||||
"root_path": "/api/v1"
|
||||
}
|
||||
```
|
||||
|
||||
따라서 `http://127.0.0.1:8000/api/v1/app`로 접근될 것이라고 기대하지는 않습니다.
|
||||
|
||||
Uvicorn은 프록시가 `http://127.0.0.1:8000/app`에서 Uvicorn에 접근할 것을 기대하고, 그 위에 `/api/v1` 접두사를 추가하는 것은 프록시의 책임입니다.
|
||||
|
||||
## 제거된 경로 접두사를 가진 프록시에 대하여 { #about-proxies-with-a-stripped-path-prefix }
|
||||
|
||||
경로 접두사가 제거되는 프록시는 구성 방법 중 하나일 뿐이라는 점을 기억하세요.
|
||||
|
||||
많은 경우 기본값은 프록시가 경로 접두사를 제거하지 않는 방식일 것입니다.
|
||||
|
||||
그런 경우(경로 접두사를 제거하지 않는 경우) 프록시는 `https://myawesomeapp.com` 같은 곳에서 리슨하고, 브라우저가 `https://myawesomeapp.com/api/v1/app`로 접근하면, 서버(예: Uvicorn)가 `http://127.0.0.1:8000`에서 리슨하고 있을 때 프록시(경로 접두사를 제거하지 않는)는 동일한 경로로 Uvicorn에 접근합니다: `http://127.0.0.1:8000/api/v1/app`.
|
||||
|
||||
## Traefik으로 로컬 테스트하기 { #testing-locally-with-traefik }
|
||||
|
||||
<a href="https://docs.traefik.io/" class="external-link" target="_blank">Traefik</a>을 사용하면, 경로 접두사가 제거되는 구성을 로컬에서 쉽게 실험할 수 있습니다.
|
||||
|
||||
<a href="https://github.com/containous/traefik/releases" class="external-link" target="_blank">Traefik 다운로드</a>는 단일 바이너리이며, 압축 파일을 풀고 터미널에서 바로 실행할 수 있습니다.
|
||||
|
||||
그 다음 다음 내용을 가진 `traefik.toml` 파일을 생성하세요:
|
||||
|
||||
```TOML hl_lines="3"
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":9999"
|
||||
|
||||
[providers]
|
||||
[providers.file]
|
||||
filename = "routes.toml"
|
||||
```
|
||||
|
||||
이는 Traefik이 9999 포트에서 리슨하고, 다른 파일 `routes.toml`을 사용하도록 지시합니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
표준 HTTP 포트 80 대신 9999 포트를 사용해서, 관리자(`sudo`) 권한으로 실행하지 않아도 되게 했습니다.
|
||||
|
||||
///
|
||||
|
||||
이제 다른 파일 `routes.toml`을 생성하세요:
|
||||
|
||||
```TOML hl_lines="5 12 20"
|
||||
[http]
|
||||
[http.middlewares]
|
||||
|
||||
[http.middlewares.api-stripprefix.stripPrefix]
|
||||
prefixes = ["/api/v1"]
|
||||
|
||||
[http.routers]
|
||||
|
||||
[http.routers.app-http]
|
||||
entryPoints = ["http"]
|
||||
service = "app"
|
||||
rule = "PathPrefix(`/api/v1`)"
|
||||
middlewares = ["api-stripprefix"]
|
||||
|
||||
[http.services]
|
||||
|
||||
[http.services.app]
|
||||
[http.services.app.loadBalancer]
|
||||
[[http.services.app.loadBalancer.servers]]
|
||||
url = "http://127.0.0.1:8000"
|
||||
```
|
||||
|
||||
이 파일은 Traefik이 경로 접두사 `/api/v1`을 사용하도록 설정합니다.
|
||||
|
||||
그리고 Traefik은 요청을 `http://127.0.0.1:8000`에서 실행 중인 Uvicorn으로 전달합니다.
|
||||
|
||||
이제 Traefik을 시작하세요:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ ./traefik --configFile=traefik.toml
|
||||
|
||||
INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
그리고 `--root-path` 옵션을 사용해 앱을 시작하세요:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
### 응답 확인하기 { #check-the-responses }
|
||||
|
||||
이제 Uvicorn의 포트로 된 URL인 <a href="http://127.0.0.1:8000/app" class="external-link" target="_blank">http://127.0.0.1:8000/app</a>로 접속하면 정상 응답을 볼 수 있습니다:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"message": "Hello World",
|
||||
"root_path": "/api/v1"
|
||||
}
|
||||
```
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
`http://127.0.0.1:8000/app`로 접근했는데도 `/api/v1`의 `root_path`가 표시되는 것에 주의하세요. 이는 옵션 `--root-path`에서 가져온 값입니다.
|
||||
|
||||
///
|
||||
|
||||
이제 Traefik의 포트가 포함되고 경로 접두사가 포함된 URL <a href="http://127.0.0.1:9999/api/v1/app" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/app</a>을 여세요.
|
||||
|
||||
동일한 응답을 얻습니다:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"message": "Hello World",
|
||||
"root_path": "/api/v1"
|
||||
}
|
||||
```
|
||||
|
||||
하지만 이번에는 프록시가 제공한 접두사 경로 `/api/v1`이 포함된 URL에서의 응답입니다.
|
||||
|
||||
물론 여기서의 아이디어는 모두가 프록시를 통해 앱에 접근한다는 것이므로, `/api/v1` 경로 접두사가 있는 버전이 "올바른" 접근입니다.
|
||||
|
||||
그리고 경로 접두사가 없는 버전(`http://127.0.0.1:8000/app`)은 Uvicorn이 직접 제공하는 것이며, 오직 _프록시_(Traefik)가 접근하기 위한 용도입니다.
|
||||
|
||||
이는 프록시(Traefik)가 경로 접두사를 어떻게 사용하는지, 그리고 서버(Uvicorn)가 옵션 `--root-path`로부터의 `root_path`를 어떻게 사용하는지를 보여줍니다.
|
||||
|
||||
### 문서 UI 확인하기 { #check-the-docs-ui }
|
||||
|
||||
하지만 재미있는 부분은 여기입니다. ✨
|
||||
|
||||
앱에 접근하는 "공식" 방법은 우리가 정의한 경로 접두사를 가진 프록시를 통해서입니다. 따라서 기대하는 대로, URL에 경로 접두사가 없는 상태에서 Uvicorn이 직접 제공하는 docs UI를 시도하면, 프록시를 통해 접근된다고 가정하고 있기 때문에 동작하지 않습니다.
|
||||
|
||||
<a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>에서 확인할 수 있습니다:
|
||||
|
||||
<img src="/img/tutorial/behind-a-proxy/image01.png">
|
||||
|
||||
하지만 프록시(포트 `9999`)를 사용해 "공식" URL인 `/api/v1/docs`에서 docs UI에 접근하면, 올바르게 동작합니다! 🎉
|
||||
|
||||
<a href="http://127.0.0.1:9999/api/v1/docs" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/docs</a>에서 확인할 수 있습니다:
|
||||
|
||||
<img src="/img/tutorial/behind-a-proxy/image02.png">
|
||||
|
||||
원하던 그대로입니다. ✔️
|
||||
|
||||
이는 FastAPI가 이 `root_path`를 사용해, OpenAPI에서 기본 `server`를 `root_path`가 제공한 URL로 생성하기 때문입니다.
|
||||
|
||||
## 추가 서버 { #additional-servers }
|
||||
|
||||
/// warning | 경고
|
||||
|
||||
이는 더 고급 사용 사례입니다. 건너뛰어도 괜찮습니다.
|
||||
|
||||
///
|
||||
|
||||
기본적으로 **FastAPI**는 OpenAPI 스키마에서 `root_path`의 URL로 `server`를 생성합니다.
|
||||
|
||||
하지만 예를 들어 동일한 docs UI가 스테이징과 프로덕션 환경 모두와 상호작용하도록 하려면, 다른 대안 `servers`를 제공할 수도 있습니다.
|
||||
|
||||
사용자 정의 `servers` 리스트를 전달했고 `root_path`(API가 프록시 뒤에 있기 때문)가 있다면, **FastAPI**는 리스트의 맨 앞에 이 `root_path`를 가진 "server"를 삽입합니다.
|
||||
|
||||
예:
|
||||
|
||||
{* ../../docs_src/behind_a_proxy/tutorial003_py39.py hl[4:7] *}
|
||||
|
||||
다음과 같은 OpenAPI 스키마를 생성합니다:
|
||||
|
||||
```JSON hl_lines="5-7"
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
// More stuff here
|
||||
"servers": [
|
||||
{
|
||||
"url": "/api/v1"
|
||||
},
|
||||
{
|
||||
"url": "https://stag.example.com",
|
||||
"description": "Staging environment"
|
||||
},
|
||||
{
|
||||
"url": "https://prod.example.com",
|
||||
"description": "Production environment"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
// More stuff here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
`root_path`에서 가져온 값인 `/api/v1`의 `url` 값을 가진, 자동 생성된 server에 주목하세요.
|
||||
|
||||
///
|
||||
|
||||
<a href="http://127.0.0.1:9999/api/v1/docs" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/docs</a>의 docs UI에서는 다음처럼 보입니다:
|
||||
|
||||
<img src="/img/tutorial/behind-a-proxy/image03.png">
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
docs UI는 선택한 server와 상호작용합니다.
|
||||
|
||||
///
|
||||
|
||||
/// note | 기술 세부사항
|
||||
|
||||
OpenAPI 사양에서 `servers` 속성은 선택 사항입니다.
|
||||
|
||||
`servers` 파라미터를 지정하지 않고 `root_path`가 `/`와 같다면, 생성된 OpenAPI 스키마의 `servers` 속성은 기본적으로 완전히 생략되며, 이는 `url` 값이 `/`인 단일 server와 동등합니다.
|
||||
|
||||
///
|
||||
|
||||
### `root_path`에서 자동 server 비활성화하기 { #disable-automatic-server-from-root-path }
|
||||
|
||||
**FastAPI**가 `root_path`를 사용한 자동 server를 포함하지 않게 하려면, `root_path_in_servers=False` 파라미터를 사용할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/behind_a_proxy/tutorial004_py39.py hl[9] *}
|
||||
|
||||
그러면 OpenAPI 스키마에 포함되지 않습니다.
|
||||
|
||||
## 서브 애플리케이션 마운트하기 { #mounting-a-sub-application }
|
||||
|
||||
프록시에서 `root_path`를 사용하면서도, [서브 애플리케이션 - 마운트](sub-applications.md){.internal-link target=_blank}에 설명된 것처럼 서브 애플리케이션을 마운트해야 한다면, 기대하는 대로 일반적으로 수행할 수 있습니다.
|
||||
|
||||
FastAPI가 내부적으로 `root_path`를 똑똑하게 사용하므로, 그냥 동작합니다. ✨
|
||||
@@ -1,95 +0,0 @@
|
||||
# Dataclasses 사용하기 { #using-dataclasses }
|
||||
|
||||
FastAPI는 **Pydantic** 위에 구축되어 있으며, 지금까지는 Pydantic 모델을 사용해 요청과 응답을 선언하는 방법을 보여드렸습니다.
|
||||
|
||||
하지만 FastAPI는 <a href="https://docs.python.org/3/library/dataclasses.html" class="external-link" target="_blank">`dataclasses`</a>도 같은 방식으로 사용하는 것을 지원합니다:
|
||||
|
||||
{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *}
|
||||
|
||||
이는 **Pydantic** 덕분에 여전히 지원되는데, Pydantic이 <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/#use-of-stdlib-dataclasses-with-basemodel" class="external-link" target="_blank">`dataclasses`에 대한 내부 지원</a>을 제공하기 때문입니다.
|
||||
|
||||
따라서 위 코드처럼 Pydantic을 명시적으로 사용하지 않더라도, FastAPI는 Pydantic을 사용해 표준 dataclasses를 Pydantic의 dataclasses 변형으로 변환합니다.
|
||||
|
||||
그리고 물론 다음과 같은 기능도 동일하게 지원합니다:
|
||||
|
||||
* 데이터 검증
|
||||
* 데이터 직렬화
|
||||
* 데이터 문서화 등
|
||||
|
||||
이는 Pydantic 모델을 사용할 때와 같은 방식으로 동작합니다. 그리고 실제로도 내부적으로는 Pydantic을 사용해 같은 방식으로 구현됩니다.
|
||||
|
||||
/// info | 정보
|
||||
|
||||
dataclasses는 Pydantic 모델이 할 수 있는 모든 것을 할 수는 없다는 점을 기억하세요.
|
||||
|
||||
그래서 여전히 Pydantic 모델을 사용해야 할 수도 있습니다.
|
||||
|
||||
하지만 이미 여러 dataclasses를 가지고 있다면, 이것은 FastAPI로 웹 API를 구동하는 데 그것들을 활용할 수 있는 좋은 방법입니다. 🤓
|
||||
|
||||
///
|
||||
|
||||
## `response_model`에서 Dataclasses 사용하기 { #dataclasses-in-response-model }
|
||||
|
||||
`response_model` 매개변수에서도 `dataclasses`를 사용할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *}
|
||||
|
||||
dataclass는 자동으로 Pydantic dataclass로 변환됩니다.
|
||||
|
||||
이렇게 하면 해당 스키마가 API docs 사용자 인터페이스에 표시됩니다:
|
||||
|
||||
<img src="/img/tutorial/dataclasses/image01.png">
|
||||
|
||||
## 중첩 데이터 구조에서 Dataclasses 사용하기 { #dataclasses-in-nested-data-structures }
|
||||
|
||||
`dataclasses`를 다른 타입 애너테이션과 조합해 중첩 데이터 구조를 만들 수도 있습니다.
|
||||
|
||||
일부 경우에는 Pydantic 버전의 `dataclasses`를 사용해야 할 수도 있습니다. 예를 들어 자동 생성된 API 문서에서 오류가 발생하는 경우입니다.
|
||||
|
||||
그런 경우 표준 `dataclasses`를 드롭인 대체재인 `pydantic.dataclasses`로 간단히 바꾸면 됩니다:
|
||||
|
||||
{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *}
|
||||
|
||||
1. 표준 `dataclasses`에서 `field`를 계속 임포트합니다.
|
||||
|
||||
2. `pydantic.dataclasses`는 `dataclasses`의 드롭인 대체재입니다.
|
||||
|
||||
3. `Author` dataclass에는 `Item` dataclasses의 리스트가 포함됩니다.
|
||||
|
||||
4. `Author` dataclass가 `response_model` 매개변수로 사용됩니다.
|
||||
|
||||
5. 요청 본문으로 dataclasses와 함께 다른 표준 타입 애너테이션을 사용할 수 있습니다.
|
||||
|
||||
이 경우에는 `Item` dataclasses의 리스트입니다.
|
||||
|
||||
6. 여기서는 dataclasses 리스트인 `items`를 포함하는 딕셔너리를 반환합니다.
|
||||
|
||||
FastAPI는 여전히 데이터를 JSON으로 <abbr title="converting the data to a format that can be transmitted - 데이터를 전송 가능한 형식으로 변환하는 것">serializing</abbr>할 수 있습니다.
|
||||
|
||||
7. 여기서 `response_model`은 `Author` dataclasses 리스트에 대한 타입 애너테이션을 사용합니다.
|
||||
|
||||
다시 말해, `dataclasses`를 표준 타입 애너테이션과 조합할 수 있습니다.
|
||||
|
||||
8. 이 *경로 처리 함수*는 `async def` 대신 일반 `def`를 사용하고 있다는 점에 주목하세요.
|
||||
|
||||
언제나처럼 FastAPI에서는 필요에 따라 `def`와 `async def`를 조합해 사용할 수 있습니다.
|
||||
|
||||
어떤 것을 언제 사용해야 하는지 다시 확인하고 싶다면, [`async`와 `await`](../async.md#in-a-hurry){.internal-link target=_blank} 문서의 _"급하신가요?"_ 섹션을 확인하세요.
|
||||
|
||||
9. 이 *경로 처리 함수*는 dataclasses를(물론 반환할 수도 있지만) 반환하지 않고, 내부 데이터를 담은 딕셔너리들의 리스트를 반환합니다.
|
||||
|
||||
FastAPI는 `response_model` 매개변수(dataclasses 포함)를 사용해 응답을 변환합니다.
|
||||
|
||||
`dataclasses`는 다른 타입 애너테이션과 매우 다양한 조합으로 결합해 복잡한 데이터 구조를 구성할 수 있습니다.
|
||||
|
||||
더 구체적인 내용은 위 코드 내 애너테이션 팁을 확인하세요.
|
||||
|
||||
## 더 알아보기 { #learn-more }
|
||||
|
||||
`dataclasses`를 다른 Pydantic 모델과 조합하거나, 이를 상속하거나, 여러분의 모델에 포함하는 등의 작업도 할 수 있습니다.
|
||||
|
||||
자세한 내용은 <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/" class="external-link" target="_blank">dataclasses에 관한 Pydantic 문서</a>를 참고하세요.
|
||||
|
||||
## 버전 { #version }
|
||||
|
||||
이 기능은 FastAPI `0.67.0` 버전부터 사용할 수 있습니다. 🔖
|
||||
@@ -1,208 +0,0 @@
|
||||
# SDK 생성하기 { #generating-sdks }
|
||||
|
||||
**FastAPI**는 **OpenAPI** 사양을 기반으로 하므로, FastAPI의 API는 많은 도구가 이해할 수 있는 표준 형식으로 설명할 수 있습니다.
|
||||
|
||||
덕분에 여러 언어용 클라이언트 라이브러리(<abbr title="Software Development Kits - 소프트웨어 개발 키트">**SDKs**</abbr>), 최신 **문서**, 그리고 코드와 동기화된 **테스트** 또는 **자동화 워크플로**를 쉽게 생성할 수 있습니다.
|
||||
|
||||
이 가이드에서는 FastAPI 백엔드용 **TypeScript SDK**를 생성하는 방법을 배웁니다.
|
||||
|
||||
## 오픈 소스 SDK 생성기 { #open-source-sdk-generators }
|
||||
|
||||
다양하게 활용할 수 있는 옵션으로 <a href="https://openapi-generator.tech/" class="external-link" target="_blank">OpenAPI Generator</a>가 있으며, **다양한 프로그래밍 언어**를 지원하고 OpenAPI 사양으로부터 SDK를 생성할 수 있습니다.
|
||||
|
||||
**TypeScript 클라이언트**의 경우 <a href="https://heyapi.dev/" class="external-link" target="_blank">Hey API</a>는 TypeScript 생태계에 최적화된 경험을 제공하는 목적에 맞게 설계된 솔루션입니다.
|
||||
|
||||
더 많은 SDK 생성기는 <a href="https://openapi.tools/#sdk" class="external-link" target="_blank">OpenAPI.Tools</a>에서 확인할 수 있습니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
FastAPI는 **OpenAPI 3.1** 사양을 자동으로 생성하므로, 사용하는 도구는 이 버전을 지원해야 합니다.
|
||||
|
||||
///
|
||||
|
||||
## FastAPI 스폰서의 SDK 생성기 { #sdk-generators-from-fastapi-sponsors }
|
||||
|
||||
이 섹션에서는 FastAPI를 후원하는 회사들이 제공하는 **벤처 투자 기반** 및 **기업 지원** 솔루션을 소개합니다. 이 제품들은 고품질로 생성된 SDK에 더해 **추가 기능**과 **통합**을 제공합니다.
|
||||
|
||||
✨ [**FastAPI 후원하기**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨를 통해, 이 회사들은 프레임워크와 그 **생태계**가 건강하고 **지속 가능**하게 유지되도록 돕습니다.
|
||||
|
||||
또한 이들의 후원은 FastAPI **커뮤니티**(여러분)에 대한 강한 헌신을 보여주며, **좋은 서비스**를 제공하는 것뿐 아니라, 견고하고 활발한 프레임워크인 FastAPI를 지원하는 데에도 관심이 있음을 나타냅니다. 🙇
|
||||
|
||||
예를 들어 다음을 사용해 볼 수 있습니다:
|
||||
|
||||
* <a href="https://speakeasy.com/editor?utm_source=fastapi+repo&utm_medium=github+sponsorship" class="external-link" target="_blank">Speakeasy</a>
|
||||
* <a href="https://www.stainless.com/?utm_source=fastapi&utm_medium=referral" class="external-link" target="_blank">Stainless</a>
|
||||
* <a href="https://developers.liblab.com/tutorials/sdk-for-fastapi?utm_source=fastapi" class="external-link" target="_blank">liblab</a>
|
||||
|
||||
이 중 일부는 오픈 소스이거나 무료 티어를 제공하므로, 비용 부담 없이 사용해 볼 수 있습니다. 다른 상용 SDK 생성기도 있으며 온라인에서 찾을 수 있습니다. 🤓
|
||||
|
||||
## TypeScript SDK 만들기 { #create-a-typescript-sdk }
|
||||
|
||||
간단한 FastAPI 애플리케이션으로 시작해 보겠습니다:
|
||||
|
||||
{* ../../docs_src/generate_clients/tutorial001_py39.py hl[7:9,12:13,16:17,21] *}
|
||||
|
||||
*path operation*에서 요청 페이로드와 응답 페이로드에 사용하는 모델을 `Item`, `ResponseMessage` 모델로 정의하고 있다는 점에 주목하세요.
|
||||
|
||||
### API 문서 { #api-docs }
|
||||
|
||||
`/docs`로 이동하면, 요청으로 보낼 데이터와 응답으로 받을 데이터에 대한 **스키마(schemas)**가 있는 것을 볼 수 있습니다:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image01.png">
|
||||
|
||||
이 스키마는 앱에서 모델로 선언되었기 때문에 볼 수 있습니다.
|
||||
|
||||
그 정보는 앱의 **OpenAPI 스키마**에서 사용할 수 있고, 이후 API 문서에 표시됩니다.
|
||||
|
||||
OpenAPI에 포함된 모델의 동일한 정보가 **클라이언트 코드 생성**에 사용될 수 있습니다.
|
||||
|
||||
### Hey API { #hey-api }
|
||||
|
||||
모델이 포함된 FastAPI 앱이 준비되면, Hey API를 사용해 TypeScript 클라이언트를 생성할 수 있습니다. 가장 빠른 방법은 npx를 사용하는 것입니다.
|
||||
|
||||
```sh
|
||||
npx @hey-api/openapi-ts -i http://localhost:8000/openapi.json -o src/client
|
||||
```
|
||||
|
||||
이 명령은 `./src/client`에 TypeScript SDK를 생성합니다.
|
||||
|
||||
<a href="https://heyapi.dev/openapi-ts/get-started" class="external-link" target="_blank">`@hey-api/openapi-ts` 설치 방법</a>과 <a href="https://heyapi.dev/openapi-ts/output" class="external-link" target="_blank">생성된 결과물</a>은 해당 웹사이트에서 확인할 수 있습니다.
|
||||
|
||||
### SDK 사용하기 { #using-the-sdk }
|
||||
|
||||
이제 클라이언트 코드를 import해서 사용할 수 있습니다. 아래처럼 사용할 수 있으며, 메서드에 대한 자동 완성이 제공되는 것을 확인할 수 있습니다:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image02.png">
|
||||
|
||||
보낼 페이로드에 대해서도 자동 완성이 제공됩니다:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image03.png">
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
`name`과 `price`에 대한 자동 완성은 FastAPI 애플리케이션에서 `Item` 모델에 정의된 내용입니다.
|
||||
|
||||
///
|
||||
|
||||
전송하는 데이터에 대해 인라인 오류도 표시됩니다:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image04.png">
|
||||
|
||||
응답 객체도 자동 완성을 제공합니다:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image05.png">
|
||||
|
||||
## 태그가 있는 FastAPI 앱 { #fastapi-app-with-tags }
|
||||
|
||||
대부분의 경우 FastAPI 앱은 더 커지고, 서로 다른 *path operations* 그룹을 분리하기 위해 태그를 사용하게 될 가능성이 큽니다.
|
||||
|
||||
예를 들어 **items** 섹션과 **users** 섹션이 있고, 이를 태그로 분리할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/generate_clients/tutorial002_py39.py hl[21,26,34] *}
|
||||
|
||||
### 태그로 TypeScript 클라이언트 생성하기 { #generate-a-typescript-client-with-tags }
|
||||
|
||||
태그를 사용하는 FastAPI 앱에 대해 클라이언트를 생성하면, 일반적으로 생성된 클라이언트 코드도 태그를 기준으로 분리됩니다.
|
||||
|
||||
이렇게 하면 클라이언트 코드에서 항목들이 올바르게 정렬되고 그룹화됩니다:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image06.png">
|
||||
|
||||
이 경우 다음이 있습니다:
|
||||
|
||||
* `ItemsService`
|
||||
* `UsersService`
|
||||
|
||||
### 클라이언트 메서드 이름 { #client-method-names }
|
||||
|
||||
현재 `createItemItemsPost` 같은 생성된 메서드 이름은 그다지 깔끔하지 않습니다:
|
||||
|
||||
```TypeScript
|
||||
ItemsService.createItemItemsPost({name: "Plumbus", price: 5})
|
||||
```
|
||||
|
||||
...이는 클라이언트 생성기가 각 *path operation*에 대해 OpenAPI 내부의 **operation ID**를 사용하기 때문입니다.
|
||||
|
||||
OpenAPI는 모든 *path operations* 전체에서 operation ID가 각각 유일해야 한다고 요구합니다. 그래서 FastAPI는 operation ID가 유일하도록 **함수 이름**, **경로**, **HTTP method/operation**을 조합해 operation ID를 생성합니다.
|
||||
|
||||
하지만 다음에서 이를 개선하는 방법을 보여드리겠습니다. 🤓
|
||||
|
||||
## 커스텀 Operation ID와 더 나은 메서드 이름 { #custom-operation-ids-and-better-method-names }
|
||||
|
||||
클라이언트에서 **더 단순한 메서드 이름**을 갖도록, operation ID가 **생성되는 방식**을 **수정**할 수 있습니다.
|
||||
|
||||
이 경우 operation ID가 다른 방식으로도 **유일**하도록 보장해야 합니다.
|
||||
|
||||
예를 들어 각 *path operation*이 태그를 갖도록 한 다음, **태그**와 *path operation* **이름**(함수 이름)을 기반으로 operation ID를 생성할 수 있습니다.
|
||||
|
||||
### 유일 ID 생성 함수 커스터마이징 { #custom-generate-unique-id-function }
|
||||
|
||||
FastAPI는 각 *path operation*에 대해 **유일 ID**를 사용하며, 이는 **operation ID** 및 요청/응답에 필요한 커스텀 모델 이름에도 사용됩니다.
|
||||
|
||||
이 함수를 커스터마이징할 수 있습니다. 이 함수는 `APIRoute`를 받아 문자열을 반환합니다.
|
||||
|
||||
예를 들어 아래에서는 첫 번째 태그(대부분 태그는 하나만 있을 것입니다)와 *path operation* 이름(함수 이름)을 사용합니다.
|
||||
|
||||
그 다음 이 커스텀 함수를 `generate_unique_id_function` 매개변수로 **FastAPI**에 전달할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/generate_clients/tutorial003_py39.py hl[6:7,10] *}
|
||||
|
||||
### 커스텀 Operation ID로 TypeScript 클라이언트 생성하기 { #generate-a-typescript-client-with-custom-operation-ids }
|
||||
|
||||
이제 클라이언트를 다시 생성하면, 개선된 메서드 이름을 확인할 수 있습니다:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image07.png">
|
||||
|
||||
보시다시피, 이제 메서드 이름은 태그 다음에 함수 이름이 오며, URL 경로와 HTTP operation의 정보는 포함하지 않습니다.
|
||||
|
||||
### 클라이언트 생성기를 위한 OpenAPI 사양 전처리 { #preprocess-the-openapi-specification-for-the-client-generator }
|
||||
|
||||
생성된 코드에는 여전히 일부 **중복 정보**가 있습니다.
|
||||
|
||||
`ItemsService`(태그에서 가져옴)에 이미 **items**가 포함되어 있어 이 메서드가 items와 관련되어 있음을 알 수 있지만, 메서드 이름에도 태그 이름이 접두사로 붙어 있습니다. 😕
|
||||
|
||||
OpenAPI 전반에서는 operation ID가 **유일**하다는 것을 보장하기 위해 이 방식을 유지하고 싶을 수 있습니다.
|
||||
|
||||
하지만 생성된 클라이언트에서는, 클라이언트를 생성하기 직전에 OpenAPI operation ID를 **수정**해서 메서드 이름을 더 보기 좋고 **깔끔하게** 만들 수 있습니다.
|
||||
|
||||
OpenAPI JSON을 `openapi.json` 파일로 다운로드한 뒤, 아래와 같은 스크립트로 **접두사 태그를 제거**할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/generate_clients/tutorial004_py39.py *}
|
||||
|
||||
//// tab | Node.js
|
||||
|
||||
```Javascript
|
||||
{!> ../../docs_src/generate_clients/tutorial004.js!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
이렇게 하면 operation ID가 `items-get_items` 같은 형태에서 `get_items`로 변경되어, 클라이언트 생성기가 더 단순한 메서드 이름을 생성할 수 있습니다.
|
||||
|
||||
### 전처리된 OpenAPI로 TypeScript 클라이언트 생성하기 { #generate-a-typescript-client-with-the-preprocessed-openapi }
|
||||
|
||||
이제 최종 결과가 `openapi.json` 파일에 있으므로, 입력 위치를 업데이트해야 합니다:
|
||||
|
||||
```sh
|
||||
npx @hey-api/openapi-ts -i ./openapi.json -o src/client
|
||||
```
|
||||
|
||||
새 클라이언트를 생성한 후에는 **깔끔한 메서드 이름**을 가지면서도, **자동 완성**, **인라인 오류** 등은 그대로 제공됩니다:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image08.png">
|
||||
|
||||
## 장점 { #benefits }
|
||||
|
||||
자동으로 생성된 클라이언트를 사용하면 다음에 대해 **자동 완성**을 받을 수 있습니다:
|
||||
|
||||
* 메서드
|
||||
* 본문(body)의 요청 페이로드, 쿼리 파라미터 등
|
||||
* 응답 페이로드
|
||||
|
||||
또한 모든 것에 대해 **인라인 오류**도 확인할 수 있습니다.
|
||||
|
||||
그리고 백엔드 코드를 업데이트한 뒤 프론트엔드를 **재생성(regenerate)**하면, 새 *path operations*가 메서드로 추가되고 기존 것은 제거되며, 그 밖의 변경 사항도 생성된 코드에 반영됩니다. 🤓
|
||||
|
||||
이는 무언가 변경되면 그 변경이 클라이언트 코드에도 자동으로 **반영**된다는 뜻입니다. 또한 클라이언트를 **빌드(build)**하면 사용된 데이터가 **불일치(mismatch)**할 경우 오류가 발생합니다.
|
||||
|
||||
따라서 운영 환경에서 최종 사용자에게 오류가 노출된 뒤 문제를 추적하는 대신, 개발 사이클 초기에 **많은 오류를 매우 빨리 감지**할 수 있습니다. ✨
|
||||
@@ -1,97 +0,0 @@
|
||||
# 고급 Middleware { #advanced-middleware }
|
||||
|
||||
메인 튜토리얼에서 애플리케이션에 [커스텀 Middleware](../tutorial/middleware.md){.internal-link target=_blank}를 추가하는 방법을 읽었습니다.
|
||||
|
||||
그리고 [`CORSMiddleware`로 CORS 처리하기](../tutorial/cors.md){.internal-link target=_blank}도 읽었습니다.
|
||||
|
||||
이 섹션에서는 다른 middleware들을 사용하는 방법을 살펴보겠습니다.
|
||||
|
||||
## ASGI middleware 추가하기 { #adding-asgi-middlewares }
|
||||
|
||||
**FastAPI**는 Starlette를 기반으로 하고 <abbr title="Asynchronous Server Gateway Interface">ASGI</abbr> 사양을 구현하므로, 어떤 ASGI middleware든 사용할 수 있습니다.
|
||||
|
||||
ASGI 사양을 따르기만 하면, FastAPI나 Starlette를 위해 만들어진 middleware가 아니어도 동작합니다.
|
||||
|
||||
일반적으로 ASGI middleware는 첫 번째 인자로 ASGI 앱을 받도록 기대하는 클래스입니다.
|
||||
|
||||
그래서 서드파티 ASGI middleware 문서에서는 아마 다음과 같이 하라고 안내할 것입니다:
|
||||
|
||||
```Python
|
||||
from unicorn import UnicornMiddleware
|
||||
|
||||
app = SomeASGIApp()
|
||||
|
||||
new_app = UnicornMiddleware(app, some_config="rainbow")
|
||||
```
|
||||
|
||||
하지만 FastAPI(정확히는 Starlette)는 더 간단한 방법을 제공하며, 이를 통해 내부 middleware가 서버 오류를 처리하고 커스텀 예외 핸들러가 올바르게 동작하도록 보장합니다.
|
||||
|
||||
이를 위해(그리고 CORS 예제에서처럼) `app.add_middleware()`를 사용합니다.
|
||||
|
||||
```Python
|
||||
from fastapi import FastAPI
|
||||
from unicorn import UnicornMiddleware
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.add_middleware(UnicornMiddleware, some_config="rainbow")
|
||||
```
|
||||
|
||||
`app.add_middleware()`는 첫 번째 인자로 middleware 클래스를 받고, 그 뒤에는 middleware에 전달할 추가 인자들을 받습니다.
|
||||
|
||||
## 통합 middleware { #integrated-middlewares }
|
||||
|
||||
**FastAPI**에는 일반적인 사용 사례를 위한 여러 middleware가 포함되어 있습니다. 다음에서 이를 사용하는 방법을 살펴보겠습니다.
|
||||
|
||||
/// note | 기술 세부사항
|
||||
|
||||
다음 예제에서는 `from starlette.middleware.something import SomethingMiddleware`를 사용해도 됩니다.
|
||||
|
||||
**FastAPI**는 개발자 편의를 위해 `fastapi.middleware`에 여러 middleware를 제공하지만, 사용 가능한 대부분의 middleware는 Starlette에서 직접 제공됩니다.
|
||||
|
||||
///
|
||||
|
||||
## `HTTPSRedirectMiddleware` { #httpsredirectmiddleware }
|
||||
|
||||
들어오는 모든 요청이 `https` 또는 `wss`여야 하도록 강제합니다.
|
||||
|
||||
`http` 또는 `ws`로 들어오는 모든 요청은 대신 보안 스킴으로 리디렉션됩니다.
|
||||
|
||||
{* ../../docs_src/advanced_middleware/tutorial001_py39.py hl[2,6] *}
|
||||
|
||||
## `TrustedHostMiddleware` { #trustedhostmiddleware }
|
||||
|
||||
HTTP Host Header 공격을 방어하기 위해, 들어오는 모든 요청에 올바르게 설정된 `Host` 헤더가 있어야 하도록 강제합니다.
|
||||
|
||||
{* ../../docs_src/advanced_middleware/tutorial002_py39.py hl[2,6:8] *}
|
||||
|
||||
다음 인자들을 지원합니다:
|
||||
|
||||
* `allowed_hosts` - 호스트명으로 허용할 도메인 이름 목록입니다. `*.example.com` 같은 와일드카드 도메인으로 서브도메인을 매칭하는 것도 지원합니다. 어떤 호스트명이든 허용하려면 `allowed_hosts=["*"]`를 사용하거나 middleware를 생략하세요.
|
||||
* `www_redirect` - True로 설정하면, 허용된 호스트의 non-www 버전으로 들어오는 요청을 www 버전으로 리디렉션합니다. 기본값은 `True`입니다.
|
||||
|
||||
들어오는 요청이 올바르게 검증되지 않으면 `400` 응답이 전송됩니다.
|
||||
|
||||
## `GZipMiddleware` { #gzipmiddleware }
|
||||
|
||||
`Accept-Encoding` 헤더에 `"gzip"`이 포함된 어떤 요청이든 GZip 응답을 처리합니다.
|
||||
|
||||
이 middleware는 일반 응답과 스트리밍 응답을 모두 처리합니다.
|
||||
|
||||
{* ../../docs_src/advanced_middleware/tutorial003_py39.py hl[2,6] *}
|
||||
|
||||
다음 인자들을 지원합니다:
|
||||
|
||||
* `minimum_size` - 바이트 단위로 지정한 최소 크기보다 작은 응답은 GZip으로 압축하지 않습니다. 기본값은 `500`입니다.
|
||||
* `compresslevel` - GZip 압축 중에 사용됩니다. 1부터 9까지의 정수입니다. 기본값은 `9`입니다. 값이 낮을수록 압축은 더 빠르지만 파일 크기는 더 커지고, 값이 높을수록 압축은 더 느리지만 파일 크기는 더 작아집니다.
|
||||
|
||||
## 다른 middleware { #other-middlewares }
|
||||
|
||||
다른 ASGI middleware도 많이 있습니다.
|
||||
|
||||
예를 들어:
|
||||
|
||||
* <a href="https://github.com/encode/uvicorn/blob/master/uvicorn/middleware/proxy_headers.py" class="external-link" target="_blank">Uvicorn의 `ProxyHeadersMiddleware`</a>
|
||||
* <a href="https://github.com/florimondmanca/msgpack-asgi" class="external-link" target="_blank">MessagePack</a>
|
||||
|
||||
사용 가능한 다른 middleware를 보려면 <a href="https://www.starlette.dev/middleware/" class="external-link" target="_blank">Starlette의 Middleware 문서</a>와 <a href="https://github.com/florimondmanca/awesome-asgi" class="external-link" target="_blank">ASGI Awesome List</a>를 확인하세요.
|
||||
@@ -1,186 +0,0 @@
|
||||
# OpenAPI 콜백 { #openapi-callbacks }
|
||||
|
||||
다른 사람이 만든 *external API*(아마도 당신의 API를 *사용*할 동일한 개발자)가 요청을 트리거하도록 만드는 *경로 처리*를 가진 API를 만들 수 있습니다.
|
||||
|
||||
당신의 API 앱이 *external API*를 호출할 때 일어나는 과정을 "callback"이라고 합니다. 외부 개발자가 작성한 소프트웨어가 당신의 API로 요청을 보낸 다음, 당신의 API가 다시 *external API*로 요청을 보내 *되돌려 호출*하기 때문입니다(아마도 같은 개발자가 만든 API일 것입니다).
|
||||
|
||||
이 경우, 그 *external API*가 어떤 형태여야 하는지 문서화하고 싶을 수 있습니다. 어떤 *경로 처리*를 가져야 하는지, 어떤 body를 기대하는지, 어떤 응답을 반환해야 하는지 등입니다.
|
||||
|
||||
## 콜백이 있는 앱 { #an-app-with-callbacks }
|
||||
|
||||
예시로 확인해 보겠습니다.
|
||||
|
||||
청구서를 생성할 수 있는 앱을 개발한다고 가정해 보세요.
|
||||
|
||||
이 청구서는 `id`, `title`(선택 사항), `customer`, `total`을 갖습니다.
|
||||
|
||||
당신의 API 사용자(외부 개발자)는 POST 요청으로 당신의 API에서 청구서를 생성합니다.
|
||||
|
||||
그 다음 당신의 API는(가정해 보면):
|
||||
|
||||
* 청구서를 외부 개발자의 고객에게 전송합니다.
|
||||
* 돈을 수금합니다.
|
||||
* API 사용자(외부 개발자)의 API로 다시 알림을 보냅니다.
|
||||
* 이는 (당신의 API에서) 그 외부 개발자가 제공하는 어떤 *external API*로 POST 요청을 보내는 방식으로 수행됩니다(이것이 "callback"입니다).
|
||||
|
||||
## 일반적인 **FastAPI** 앱 { #the-normal-fastapi-app }
|
||||
|
||||
먼저 콜백을 추가하기 전, 일반적인 API 앱이 어떻게 생겼는지 보겠습니다.
|
||||
|
||||
`Invoice` body를 받는 *경로 처리*와, 콜백을 위한 URL을 담는 쿼리 파라미터 `callback_url`이 있을 것입니다.
|
||||
|
||||
이 부분은 꽤 일반적이며, 대부분의 코드는 이미 익숙할 것입니다:
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *}
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
`callback_url` 쿼리 파라미터는 Pydantic의 <a href="https://docs.pydantic.dev/latest/api/networks/" class="external-link" target="_blank">Url</a> 타입을 사용합니다.
|
||||
|
||||
///
|
||||
|
||||
유일하게 새로운 것은 *경로 처리 데코레이터*의 인자로 `callbacks=invoices_callback_router.routes`가 들어간다는 점입니다. 이것이 무엇인지 다음에서 보겠습니다.
|
||||
|
||||
## 콜백 문서화하기 { #documenting-the-callback }
|
||||
|
||||
실제 콜백 코드는 당신의 API 앱에 크게 의존합니다.
|
||||
|
||||
그리고 앱마다 많이 달라질 수 있습니다.
|
||||
|
||||
다음처럼 한두 줄의 코드일 수도 있습니다:
|
||||
|
||||
```Python
|
||||
callback_url = "https://example.com/api/v1/invoices/events/"
|
||||
httpx.post(callback_url, json={"description": "Invoice paid", "paid": True})
|
||||
```
|
||||
|
||||
하지만 콜백에서 가장 중요한 부분은, 당신의 API 사용자(외부 개발자)가 콜백 요청 body로 *당신의 API*가 보낼 데이터 등에 맞춰 *external API*를 올바르게 구현하도록 보장하는 것입니다.
|
||||
|
||||
그래서 다음으로 할 일은, *당신의 API*에서 보내는 콜백을 받기 위해 그 *external API*가 어떤 형태여야 하는지 문서화하는 코드를 추가하는 것입니다.
|
||||
|
||||
그 문서는 당신의 API에서 `/docs`의 Swagger UI에 표시되며, 외부 개발자들이 *external API*를 어떻게 만들어야 하는지 알 수 있게 해줍니다.
|
||||
|
||||
이 예시는 콜백 자체(한 줄 코드로도 될 수 있음)를 구현하지 않고, 문서화 부분만 구현합니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
실제 콜백은 단지 HTTP 요청입니다.
|
||||
|
||||
콜백을 직접 구현할 때는 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a>나 <a href="https://requests.readthedocs.io/" class="external-link" target="_blank">Requests</a> 같은 것을 사용할 수 있습니다.
|
||||
|
||||
///
|
||||
|
||||
## 콜백 문서화 코드 작성하기 { #write-the-callback-documentation-code }
|
||||
|
||||
이 코드는 앱에서 실행되지 않습니다. 그 *external API*가 어떤 형태여야 하는지 *문서화*하는 데만 필요합니다.
|
||||
|
||||
하지만 **FastAPI**로 API의 자동 문서를 쉽게 생성하는 방법은 이미 알고 있습니다.
|
||||
|
||||
따라서 그와 같은 지식을 사용해 *external API*가 어떻게 생겨야 하는지 문서화할 것입니다... 즉 외부 API가 구현해야 하는 *경로 처리(들)*(당신의 API가 호출할 것들)을 만들어서 말입니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
콜백을 문서화하는 코드를 작성할 때는, 자신이 그 *외부 개발자*라고 상상하는 것이 유용할 수 있습니다. 그리고 지금은 *당신의 API*가 아니라 *external API*를 구현하고 있다고 생각해 보세요.
|
||||
|
||||
이 관점(외부 개발자의 관점)을 잠시 채택하면, 그 *external API*를 위해 파라미터, body용 Pydantic 모델, 응답 등을 어디에 두어야 하는지가 더 명확하게 느껴질 수 있습니다.
|
||||
|
||||
///
|
||||
|
||||
### 콜백 `APIRouter` 생성하기 { #create-a-callback-apirouter }
|
||||
|
||||
먼저 하나 이상의 콜백을 담을 새 `APIRouter`를 만듭니다.
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *}
|
||||
|
||||
### 콜백 *경로 처리* 생성하기 { #create-the-callback-path-operation }
|
||||
|
||||
콜백 *경로 처리*를 만들려면 위에서 만든 동일한 `APIRouter`를 사용합니다.
|
||||
|
||||
일반적인 FastAPI *경로 처리*처럼 보일 것입니다:
|
||||
|
||||
* 아마도 받아야 할 body 선언이 있을 것입니다(예: `body: InvoiceEvent`).
|
||||
* 그리고 반환해야 할 응답 선언도 있을 수 있습니다(예: `response_model=InvoiceEventReceived`).
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *}
|
||||
|
||||
일반적인 *경로 처리*와의 주요 차이점은 2가지입니다:
|
||||
|
||||
* 실제 코드를 가질 필요가 없습니다. 당신의 앱은 이 코드를 절대 호출하지 않기 때문입니다. 이는 *external API*를 문서화하는 데만 사용됩니다. 따라서 함수는 그냥 `pass`만 있어도 됩니다.
|
||||
* *path*에는 <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#key-expression" class="external-link" target="_blank">OpenAPI 3 expression</a>(자세한 내용은 아래 참고)이 포함될 수 있으며, 이를 통해 *당신의 API*로 보내진 원래 요청의 파라미터와 일부 값을 변수로 사용할 수 있습니다.
|
||||
|
||||
### 콜백 경로 표현식 { #the-callback-path-expression }
|
||||
|
||||
콜백 *path*는 *당신의 API*로 보내진 원래 요청의 일부를 포함할 수 있는 <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#key-expression" class="external-link" target="_blank">OpenAPI 3 expression</a>을 가질 수 있습니다.
|
||||
|
||||
이 경우, 다음 `str`입니다:
|
||||
|
||||
```Python
|
||||
"{$callback_url}/invoices/{$request.body.id}"
|
||||
```
|
||||
|
||||
따라서 당신의 API 사용자(외부 개발자)가 *당신의 API*로 다음 요청을 보내고:
|
||||
|
||||
```
|
||||
https://yourapi.com/invoices/?callback_url=https://www.external.org/events
|
||||
```
|
||||
|
||||
JSON body가 다음과 같다면:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"id": "2expen51ve",
|
||||
"customer": "Mr. Richie Rich",
|
||||
"total": "9999"
|
||||
}
|
||||
```
|
||||
|
||||
그러면 *당신의 API*는 청구서를 처리하고, 나중에 어느 시점에서 `callback_url`(즉 *external API*)로 콜백 요청을 보냅니다:
|
||||
|
||||
```
|
||||
https://www.external.org/events/invoices/2expen51ve
|
||||
```
|
||||
|
||||
그리고 다음과 같은 JSON body를 포함할 것입니다:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"description": "Payment celebration",
|
||||
"paid": true
|
||||
}
|
||||
```
|
||||
|
||||
또한 그 *external API*로부터 다음과 같은 JSON body 응답을 기대합니다:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
```
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
콜백 URL에는 `callback_url` 쿼리 파라미터로 받은 URL(`https://www.external.org/events`)뿐 아니라, JSON body 안의 청구서 `id`(`2expen51ve`)도 함께 사용된다는 점에 주목하세요.
|
||||
|
||||
///
|
||||
|
||||
### 콜백 라우터 추가하기 { #add-the-callback-router }
|
||||
|
||||
이 시점에서, 위에서 만든 콜백 라우터 안에 *콜백 경로 처리(들)*(즉 *external developer*가 *external API*에 구현해야 하는 것들)을 준비했습니다.
|
||||
|
||||
이제 *당신의 API 경로 처리 데코레이터*에서 `callbacks` 파라미터를 사용해, 그 콜백 라우터의 `.routes` 속성(실제로는 routes/*경로 처리*의 `list`)을 전달합니다:
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *}
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
`callback=`에 라우터 자체(`invoices_callback_router`)를 넘기는 것이 아니라, `invoices_callback_router.routes`처럼 `.routes` 속성을 넘긴다는 점에 주목하세요.
|
||||
|
||||
///
|
||||
|
||||
### 문서 확인하기 { #check-the-docs }
|
||||
|
||||
이제 앱을 실행하고 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>로 이동하세요.
|
||||
|
||||
*경로 처리*에 대해 "Callbacks" 섹션을 포함한 문서가 표시되며, *external API*가 어떤 형태여야 하는지 확인할 수 있습니다:
|
||||
|
||||
<img src="/img/tutorial/openapi-callbacks/image01.png">
|
||||
@@ -1,55 +0,0 @@
|
||||
# OpenAPI Webhooks { #openapi-webhooks }
|
||||
|
||||
앱이 어떤 데이터와 함께 (요청을 보내서) *사용자의* 앱을 호출할 수 있고, 보통 어떤 **이벤트**를 **알리기** 위해 그렇게 할 수 있다는 것을 API **사용자**에게 알려야 하는 경우가 있습니다.
|
||||
|
||||
이는 사용자가 여러분의 API로 요청을 보내는 일반적인 과정 대신, **여러분의 API**(또는 앱)가 **사용자의 시스템**(사용자의 API, 사용자의 앱)으로 **요청을 보낼 수 있다**는 의미입니다.
|
||||
|
||||
이를 보통 **webhook**이라고 합니다.
|
||||
|
||||
## Webhooks 단계 { #webhooks-steps }
|
||||
|
||||
일반적인 과정은, 여러분이 코드에서 보낼 메시지, 즉 **요청 본문(body)**이 무엇인지 **정의**하는 것입니다.
|
||||
|
||||
또한 여러분의 앱이 어떤 **시점**에 그 요청(또는 이벤트)을 보낼지도 어떤 방식으로든 정의합니다.
|
||||
|
||||
그리고 **사용자**는 (예: 어딘가의 웹 대시보드에서) 여러분의 앱이 그 요청을 보내야 할 **URL**을 어떤 방식으로든 정의합니다.
|
||||
|
||||
webhook의 URL을 등록하는 방법과 실제로 그 요청을 보내는 코드에 대한 모든 **로직**은 여러분에게 달려 있습니다. **여러분의 코드**에서 원하는 방식으로 작성하면 됩니다.
|
||||
|
||||
## **FastAPI**와 OpenAPI로 webhooks 문서화하기 { #documenting-webhooks-with-fastapi-and-openapi }
|
||||
|
||||
**FastAPI**에서는 OpenAPI를 사용해, 이러한 webhook의 이름, 여러분의 앱이 보낼 수 있는 HTTP 작업 타입(예: `POST`, `PUT` 등), 그리고 여러분의 앱이 보낼 요청 **본문(body)**을 정의할 수 있습니다.
|
||||
|
||||
이렇게 하면 사용자가 여러분의 **webhook** 요청을 받기 위해 **자신들의 API를 구현**하기가 훨씬 쉬워지고, 경우에 따라서는 자신의 API 코드 일부를 자동 생성할 수도 있습니다.
|
||||
|
||||
/// info | 정보
|
||||
|
||||
Webhooks는 OpenAPI 3.1.0 이상에서 사용할 수 있으며, FastAPI `0.99.0` 이상에서 지원됩니다.
|
||||
|
||||
///
|
||||
|
||||
## webhooks가 있는 앱 { #an-app-with-webhooks }
|
||||
|
||||
**FastAPI** 애플리케이션을 만들면, *경로 처리*를 정의하는 것과 같은 방식으로(예: `@app.webhooks.post()`), *webhooks*를 정의하는 데 사용할 수 있는 `webhooks` 속성이 있습니다.
|
||||
|
||||
{* ../../docs_src/openapi_webhooks/tutorial001_py39.py hl[9:13,36:53] *}
|
||||
|
||||
여러분이 정의한 webhook은 **OpenAPI** 스키마와 자동 **docs UI**에 포함됩니다.
|
||||
|
||||
/// info | 정보
|
||||
|
||||
`app.webhooks` 객체는 실제로 `APIRouter`일 뿐이며, 여러 파일로 앱을 구조화할 때 사용하는 것과 동일한 타입입니다.
|
||||
|
||||
///
|
||||
|
||||
webhook에서는 실제로(`/items/` 같은) *경로(path)*를 선언하지 않는다는 점에 유의하세요. 그곳에 전달하는 텍스트는 webhook의 **식별자**(이벤트 이름)일 뿐입니다. 예를 들어 `@app.webhooks.post("new-subscription")`에서 webhook 이름은 `new-subscription`입니다.
|
||||
|
||||
이는 **사용자**가 webhook 요청을 받고 싶은 실제 **URL 경로**를 다른 방식(예: 웹 대시보드)으로 정의할 것이라고 기대하기 때문입니다.
|
||||
|
||||
### 문서 확인하기 { #check-the-docs }
|
||||
|
||||
이제 앱을 실행하고 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>로 이동하세요.
|
||||
|
||||
문서에 일반적인 *경로 처리*가 보이고, 이제는 일부 **webhooks**도 함께 보일 것입니다:
|
||||
|
||||
<img src="/img/tutorial/openapi-webhooks/image01.png">
|
||||
@@ -1,172 +0,0 @@
|
||||
# 경로 처리 고급 구성 { #path-operation-advanced-configuration }
|
||||
|
||||
## OpenAPI operationId { #openapi-operationid }
|
||||
|
||||
/// warning | 경고
|
||||
|
||||
OpenAPI “전문가”가 아니라면, 아마 이 내용은 필요하지 않을 것입니다.
|
||||
|
||||
///
|
||||
|
||||
매개변수 `operation_id`를 사용해 *경로 처리*에 사용할 OpenAPI `operationId`를 설정할 수 있습니다.
|
||||
|
||||
각 작업마다 고유하도록 보장해야 합니다.
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *}
|
||||
|
||||
### *경로 처리 함수* 이름을 operationId로 사용하기 { #using-the-path-operation-function-name-as-the-operationid }
|
||||
|
||||
API의 함수 이름을 `operationId`로 사용하고 싶다면, 모든 API를 순회하면서 `APIRoute.name`을 사용해 각 *경로 처리*의 `operation_id`를 덮어쓸 수 있습니다.
|
||||
|
||||
모든 *경로 처리*를 추가한 뒤에 수행해야 합니다.
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial002_py39.py hl[2, 12:21, 24] *}
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
`app.openapi()`를 수동으로 호출한다면, 그 전에 `operationId`들을 업데이트해야 합니다.
|
||||
|
||||
///
|
||||
|
||||
/// warning | 경고
|
||||
|
||||
이렇게 할 경우, 각 *경로 처리 함수*의 이름이 고유하도록 보장해야 합니다.
|
||||
|
||||
서로 다른 모듈(파이썬 파일)에 있어도 마찬가지입니다.
|
||||
|
||||
///
|
||||
|
||||
## OpenAPI에서 제외하기 { #exclude-from-openapi }
|
||||
|
||||
생성된 OpenAPI 스키마(따라서 자동 문서화 시스템)에서 특정 *경로 처리*를 제외하려면, `include_in_schema` 매개변수를 `False`로 설정하세요:
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial003_py39.py hl[6] *}
|
||||
|
||||
## docstring에서 고급 설명 가져오기 { #advanced-description-from-docstring }
|
||||
|
||||
OpenAPI에 사용할 *경로 처리 함수*의 docstring 줄 수를 제한할 수 있습니다.
|
||||
|
||||
`\f`(이스케이프된 "form feed" 문자)를 추가하면 **FastAPI**는 이 지점에서 OpenAPI에 사용할 출력 내용을 잘라냅니다.
|
||||
|
||||
문서에는 표시되지 않지만, Sphinx 같은 다른 도구는 나머지 부분을 사용할 수 있습니다.
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *}
|
||||
|
||||
## 추가 응답 { #additional-responses }
|
||||
|
||||
*경로 처리*에 대해 `response_model`과 `status_code`를 선언하는 방법을 이미 보셨을 것입니다.
|
||||
|
||||
이는 *경로 처리*의 기본 응답에 대한 메타데이터를 정의합니다.
|
||||
|
||||
모델, 상태 코드 등과 함께 추가 응답도 선언할 수 있습니다.
|
||||
|
||||
이에 대한 문서의 전체 장이 있으니, [OpenAPI의 추가 응답](additional-responses.md){.internal-link target=_blank}에서 읽어보세요.
|
||||
|
||||
## OpenAPI Extra { #openapi-extra }
|
||||
|
||||
애플리케이션에서 *경로 처리*를 선언하면, **FastAPI**는 OpenAPI 스키마에 포함될 해당 *경로 처리*의 관련 메타데이터를 자동으로 생성합니다.
|
||||
|
||||
/// note | 기술 세부사항
|
||||
|
||||
OpenAPI 명세에서는 이를 <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#operation-object" class="external-link" target="_blank">Operation Object</a>라고 부릅니다.
|
||||
|
||||
///
|
||||
|
||||
여기에는 *경로 처리*에 대한 모든 정보가 있으며, 자동 문서를 생성하는 데 사용됩니다.
|
||||
|
||||
`tags`, `parameters`, `requestBody`, `responses` 등이 포함됩니다.
|
||||
|
||||
이 *경로 처리* 전용 OpenAPI 스키마는 보통 **FastAPI**가 자동으로 생성하지만, 확장할 수도 있습니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
이는 저수준 확장 지점입니다.
|
||||
|
||||
추가 응답만 선언하면 된다면, 더 편리한 방법은 [OpenAPI의 추가 응답](additional-responses.md){.internal-link target=_blank}을 사용하는 것입니다.
|
||||
|
||||
///
|
||||
|
||||
`openapi_extra` 매개변수를 사용해 *경로 처리*의 OpenAPI 스키마를 확장할 수 있습니다.
|
||||
|
||||
### OpenAPI 확장 { #openapi-extensions }
|
||||
|
||||
예를 들어 `openapi_extra`는 [OpenAPI Extensions](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions)를 선언하는 데 도움이 될 수 있습니다:
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial005_py39.py hl[6] *}
|
||||
|
||||
자동 API 문서를 열면, 해당 특정 *경로 처리*의 하단에 확장이 표시됩니다.
|
||||
|
||||
<img src="/img/tutorial/path-operation-advanced-configuration/image01.png">
|
||||
|
||||
또한 API의 `/openapi.json`에서 결과 OpenAPI를 보면, 특정 *경로 처리*의 일부로 확장이 포함된 것도 확인할 수 있습니다:
|
||||
|
||||
```JSON hl_lines="22"
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "FastAPI",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"summary": "Read Items",
|
||||
"operationId": "read_items_items__get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-aperture-labs-portal": "blue"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 사용자 정의 OpenAPI *경로 처리* 스키마 { #custom-openapi-path-operation-schema }
|
||||
|
||||
`openapi_extra`의 딕셔너리는 *경로 처리*에 대해 자동으로 생성된 OpenAPI 스키마와 깊게 병합됩니다.
|
||||
|
||||
따라서 자동 생성된 스키마에 추가 데이터를 더할 수 있습니다.
|
||||
|
||||
예를 들어 Pydantic과 함께 FastAPI의 자동 기능을 사용하지 않고, 자체 코드로 요청을 읽고 검증하기로 결정할 수도 있지만, OpenAPI 스키마에는 여전히 그 요청을 정의하고 싶을 수 있습니다.
|
||||
|
||||
그럴 때 `openapi_extra`를 사용할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial006_py39.py hl[19:36, 39:40] *}
|
||||
|
||||
이 예시에서는 어떤 Pydantic 모델도 선언하지 않았습니다. 사실 요청 바디는 JSON으로 <abbr title="converted from some plain format, like bytes, into Python objects - bytes 같은 일반 형식에서 Python 객체로 변환">parsed</abbr>되지도 않고, `bytes`로 직접 읽습니다. 그리고 함수 `magic_data_reader()`가 어떤 방식으로든 이를 파싱하는 역할을 담당합니다.
|
||||
|
||||
그럼에도 불구하고, 요청 바디에 대해 기대하는 스키마를 선언할 수 있습니다.
|
||||
|
||||
### 사용자 정의 OpenAPI 콘텐츠 타입 { #custom-openapi-content-type }
|
||||
|
||||
같은 트릭을 사용하면, Pydantic 모델을 이용해 JSON Schema를 정의하고 이를 *경로 처리*의 사용자 정의 OpenAPI 스키마 섹션에 포함시킬 수 있습니다.
|
||||
|
||||
요청의 데이터 타입이 JSON이 아니더라도 이렇게 할 수 있습니다.
|
||||
|
||||
예를 들어 이 애플리케이션에서는 Pydantic 모델에서 JSON Schema를 추출하는 FastAPI의 통합 기능도, JSON에 대한 자동 검증도 사용하지 않습니다. 실제로 요청 콘텐츠 타입을 JSON이 아니라 YAML로 선언합니다:
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *}
|
||||
|
||||
그럼에도 기본 통합 기능을 사용하지 않더라도, YAML로 받고자 하는 데이터에 대한 JSON Schema를 수동으로 생성하기 위해 Pydantic 모델을 여전히 사용합니다.
|
||||
|
||||
그 다음 요청을 직접 사용하고, 바디를 `bytes`로 추출합니다. 이는 FastAPI가 요청 페이로드를 JSON으로 파싱하려고 시도조차 하지 않는다는 뜻입니다.
|
||||
|
||||
그리고 코드에서 YAML 콘텐츠를 직접 파싱한 뒤, 다시 같은 Pydantic 모델을 사용해 YAML 콘텐츠를 검증합니다:
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *}
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
여기서는 같은 Pydantic 모델을 재사용합니다.
|
||||
|
||||
하지만 마찬가지로, 다른 방식으로 검증할 수도 있습니다.
|
||||
|
||||
///
|
||||
@@ -1,107 +0,0 @@
|
||||
# HTTP Basic Auth { #http-basic-auth }
|
||||
|
||||
가장 단순한 경우에는 HTTP Basic Auth를 사용할 수 있습니다.
|
||||
|
||||
HTTP Basic Auth에서는 애플리케이션이 사용자명과 비밀번호가 들어 있는 헤더를 기대합니다.
|
||||
|
||||
이를 받지 못하면 HTTP 401 "Unauthorized" 오류를 반환합니다.
|
||||
|
||||
그리고 값이 `Basic`이고 선택적으로 `realm` 파라미터를 포함하는 `WWW-Authenticate` 헤더를 반환합니다.
|
||||
|
||||
이는 브라우저가 사용자명과 비밀번호를 입력하는 통합 프롬프트를 표시하도록 알려줍니다.
|
||||
|
||||
그다음 사용자명과 비밀번호를 입력하면, 브라우저가 자동으로 해당 값을 헤더에 담아 전송합니다.
|
||||
|
||||
## 간단한 HTTP Basic Auth { #simple-http-basic-auth }
|
||||
|
||||
* `HTTPBasic`과 `HTTPBasicCredentials`를 임포트합니다.
|
||||
* `HTTPBasic`을 사용해 "`security` scheme"을 생성합니다.
|
||||
* *경로 처리*에서 dependency로 해당 `security`를 사용합니다.
|
||||
* `HTTPBasicCredentials` 타입의 객체를 반환합니다:
|
||||
* 전송된 `username`과 `password`를 포함합니다.
|
||||
|
||||
{* ../../docs_src/security/tutorial006_an_py39.py hl[4,8,12] *}
|
||||
|
||||
처음으로 URL을 열어보면(또는 문서에서 "Execute" 버튼을 클릭하면) 브라우저가 사용자명과 비밀번호를 물어봅니다:
|
||||
|
||||
<img src="/img/tutorial/security/image12.png">
|
||||
|
||||
## 사용자명 확인하기 { #check-the-username }
|
||||
|
||||
더 완전한 예시입니다.
|
||||
|
||||
dependency를 사용해 사용자명과 비밀번호가 올바른지 확인하세요.
|
||||
|
||||
이를 위해 Python 표준 모듈 <a href="https://docs.python.org/3/library/secrets.html" class="external-link" target="_blank">`secrets`</a>를 사용해 사용자명과 비밀번호를 확인합니다.
|
||||
|
||||
`secrets.compare_digest()`는 `bytes` 또는 ASCII 문자(영어에서 사용하는 문자)만 포함한 `str`을 받아야 합니다. 즉, `Sebastián`의 `á` 같은 문자가 있으면 동작하지 않습니다.
|
||||
|
||||
이를 처리하기 위해 먼저 `username`과 `password`를 UTF-8로 인코딩해서 `bytes`로 변환합니다.
|
||||
|
||||
그런 다음 `secrets.compare_digest()`를 사용해 `credentials.username`이 `"stanleyjobson"`이고 `credentials.password`가 `"swordfish"`인지 확실히 확인할 수 있습니다.
|
||||
|
||||
{* ../../docs_src/security/tutorial007_an_py39.py hl[1,12:24] *}
|
||||
|
||||
이는 다음과 비슷합니다:
|
||||
|
||||
```Python
|
||||
if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"):
|
||||
# Return some error
|
||||
...
|
||||
```
|
||||
|
||||
하지만 `secrets.compare_digest()`를 사용하면 "timing attacks"라고 불리는 한 유형의 공격에 대해 안전해집니다.
|
||||
|
||||
### Timing Attacks { #timing-attacks }
|
||||
|
||||
그렇다면 "timing attack"이란 무엇일까요?
|
||||
|
||||
공격자들이 사용자명과 비밀번호를 추측하려고 한다고 가정해봅시다.
|
||||
|
||||
그리고 사용자명 `johndoe`, 비밀번호 `love123`으로 요청을 보냅니다.
|
||||
|
||||
그러면 애플리케이션의 Python 코드는 대략 다음과 같을 것입니다:
|
||||
|
||||
```Python
|
||||
if "johndoe" == "stanleyjobson" and "love123" == "swordfish":
|
||||
...
|
||||
```
|
||||
|
||||
하지만 Python이 `johndoe`의 첫 글자 `j`를 `stanleyjobson`의 첫 글자 `s`와 비교하는 순간, 두 문자열이 같지 않다는 것을 이미 알게 되어 `False`를 반환합니다. 이는 “나머지 글자들을 비교하느라 계산을 더 낭비할 필요가 없다”고 판단하기 때문입니다. 그리고 애플리케이션은 "Incorrect username or password"라고 말합니다.
|
||||
|
||||
그런데 공격자들이 사용자명을 `stanleyjobsox`, 비밀번호를 `love123`으로 다시 시도합니다.
|
||||
|
||||
그러면 애플리케이션 코드는 다음과 비슷하게 동작합니다:
|
||||
|
||||
```Python
|
||||
if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish":
|
||||
...
|
||||
```
|
||||
|
||||
Python은 두 문자열이 같지 않다는 것을 알아차리기 전까지 `stanleyjobsox`와 `stanleyjobson` 양쪽의 `stanleyjobso` 전체를 비교해야 합니다. 그래서 "Incorrect username or password"라고 응답하기까지 추가로 몇 마이크로초가 더 걸릴 것입니다.
|
||||
|
||||
#### 응답 시간은 공격자에게 도움이 됩니다 { #the-time-to-answer-helps-the-attackers }
|
||||
|
||||
이 시점에서 서버가 "Incorrect username or password" 응답을 보내는 데 몇 마이크로초 더 걸렸다는 것을 알아채면, 공격자들은 _무언가_ 맞았다는 것(초기 몇 글자가 맞았다는 것)을 알게 됩니다.
|
||||
|
||||
그리고 `johndoe`보다는 `stanleyjobsox`에 더 가까운 값을 시도해야 한다는 것을 알고 다시 시도할 수 있습니다.
|
||||
|
||||
#### "전문적인" 공격 { #a-professional-attack }
|
||||
|
||||
물론 공격자들은 이런 작업을 손으로 하지 않습니다. 보통 초당 수천~수백만 번 테스트할 수 있는 프로그램을 작성할 것이고, 한 번에 정답 글자 하나씩 추가로 얻어낼 수 있습니다.
|
||||
|
||||
그렇게 하면 몇 분 또는 몇 시간 만에, 응답에 걸린 시간만을 이용해(우리 애플리케이션의 “도움”을 받아) 올바른 사용자명과 비밀번호를 추측할 수 있게 됩니다.
|
||||
|
||||
#### `secrets.compare_digest()`로 해결하기 { #fix-it-with-secrets-compare-digest }
|
||||
|
||||
하지만 우리 코드는 실제로 `secrets.compare_digest()`를 사용하고 있습니다.
|
||||
|
||||
요약하면, `stanleyjobsox`와 `stanleyjobson`을 비교하는 데 걸리는 시간은 `johndoe`와 `stanleyjobson`을 비교하는 데 걸리는 시간과 같아집니다. 비밀번호도 마찬가지입니다.
|
||||
|
||||
이렇게 애플리케이션 코드에서 `secrets.compare_digest()`를 사용하면, 이러한 범위의 보안 공격 전반에 대해 안전해집니다.
|
||||
|
||||
### 오류 반환하기 { #return-the-error }
|
||||
|
||||
자격 증명이 올바르지 않다고 판단되면, 상태 코드 401(자격 증명이 제공되지 않았을 때와 동일)을 사용하는 `HTTPException`을 반환하고 브라우저가 로그인 프롬프트를 다시 표시하도록 `WWW-Authenticate` 헤더를 추가하세요:
|
||||
|
||||
{* ../../docs_src/security/tutorial007_an_py39.py hl[26:30] *}
|
||||
@@ -1,19 +0,0 @@
|
||||
# 고급 보안 { #advanced-security }
|
||||
|
||||
## 추가 기능 { #additional-features }
|
||||
|
||||
[튜토리얼 - 사용자 가이드: 보안](../../tutorial/security/index.md){.internal-link target=_blank}에서 다룬 내용 외에도, 보안을 처리하기 위한 몇 가지 추가 기능이 있습니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
다음 섹션들은 **반드시 "고급"이라고 할 수는 없습니다**.
|
||||
|
||||
그리고 사용 사례에 따라, 그중 하나에 해결책이 있을 수도 있습니다.
|
||||
|
||||
///
|
||||
|
||||
## 먼저 튜토리얼 읽기 { #read-the-tutorial-first }
|
||||
|
||||
다음 섹션은 주요 [튜토리얼 - 사용자 가이드: 보안](../../tutorial/security/index.md){.internal-link target=_blank}을 이미 읽었다고 가정합니다.
|
||||
|
||||
모두 동일한 개념을 기반으로 하지만, 몇 가지 추가 기능을 사용할 수 있게 해줍니다.
|
||||
@@ -1,274 +0,0 @@
|
||||
# OAuth2 스코프 { #oauth2-scopes }
|
||||
|
||||
**FastAPI**에서 OAuth2 스코프를 직접 사용할 수 있으며, 자연스럽게 동작하도록 통합되어 있습니다.
|
||||
|
||||
이를 통해 OAuth2 표준을 따르는 더 세밀한 권한 시스템을 OpenAPI 애플리케이션(및 API 문서)에 통합할 수 있습니다.
|
||||
|
||||
스코프를 사용하는 OAuth2는 Facebook, Google, GitHub, Microsoft, X(Twitter) 등 많은 대형 인증 제공자가 사용하는 메커니즘입니다. 이들은 이를 통해 사용자와 애플리케이션에 특정 권한을 제공합니다.
|
||||
|
||||
Facebook, Google, GitHub, Microsoft, X(Twitter)로 “로그인”할 때마다, 해당 애플리케이션은 스코프가 있는 OAuth2를 사용하고 있습니다.
|
||||
|
||||
이 섹션에서는 **FastAPI** 애플리케이션에서 동일한 “스코프가 있는 OAuth2”로 인증(Authentication)과 인가(Authorization)를 관리하는 방법을 확인합니다.
|
||||
|
||||
/// warning | 경고
|
||||
|
||||
이 섹션은 다소 고급 내용입니다. 이제 막 시작했다면 건너뛰어도 됩니다.
|
||||
|
||||
OAuth2 스코프가 반드시 필요한 것은 아니며, 인증과 인가는 원하는 방식으로 처리할 수 있습니다.
|
||||
|
||||
하지만 스코프가 있는 OAuth2는 (OpenAPI와 함께) API 및 API 문서에 깔끔하게 통합될 수 있습니다.
|
||||
|
||||
그럼에도 불구하고, 해당 스코프(또는 그 밖의 어떤 보안/인가 요구사항이든)는 코드에서 필요에 맞게 직접 강제해야 합니다.
|
||||
|
||||
많은 경우 스코프가 있는 OAuth2는 과한 선택일 수 있습니다.
|
||||
|
||||
하지만 필요하다고 알고 있거나 궁금하다면 계속 읽어보세요.
|
||||
|
||||
///
|
||||
|
||||
## OAuth2 스코프와 OpenAPI { #oauth2-scopes-and-openapi }
|
||||
|
||||
OAuth2 사양은 “스코프(scopes)”를 공백으로 구분된 문자열 목록으로 정의합니다.
|
||||
|
||||
각 문자열의 내용은 어떤 형식이든 될 수 있지만, 공백을 포함하면 안 됩니다.
|
||||
|
||||
이 스코프들은 “권한”을 나타냅니다.
|
||||
|
||||
OpenAPI(예: API 문서)에서는 “security schemes”를 정의할 수 있습니다.
|
||||
|
||||
이 security scheme 중 하나가 OAuth2를 사용한다면, 스코프도 선언하고 사용할 수 있습니다.
|
||||
|
||||
각 “스코프”는 (공백 없는) 문자열일 뿐입니다.
|
||||
|
||||
보통 다음과 같이 특정 보안 권한을 선언하는 데 사용합니다:
|
||||
|
||||
* `users:read` 또는 `users:write` 는 흔한 예시입니다.
|
||||
* `instagram_basic` 는 Facebook/Instagram에서 사용합니다.
|
||||
* `https://www.googleapis.com/auth/drive` 는 Google에서 사용합니다.
|
||||
|
||||
/// info | 정보
|
||||
|
||||
OAuth2에서 “스코프”는 필요한 특정 권한을 선언하는 문자열일 뿐입니다.
|
||||
|
||||
`:` 같은 다른 문자가 있거나 URL이어도 상관없습니다.
|
||||
|
||||
그런 세부사항은 구현에 따라 달라집니다.
|
||||
|
||||
OAuth2 입장에서는 그저 문자열입니다.
|
||||
|
||||
///
|
||||
|
||||
## 전체 개요 { #global-view }
|
||||
|
||||
먼저, 메인 **튜토리얼 - 사용자 가이드**의 [비밀번호(및 해싱)를 사용하는 OAuth2, JWT 토큰을 사용하는 Bearer](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank} 예제에서 어떤 부분이 바뀌는지 빠르게 살펴보겠습니다. 이제 OAuth2 스코프를 사용합니다:
|
||||
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[5,9,13,47,65,106,108:116,122:126,130:136,141,157] *}
|
||||
|
||||
이제 변경 사항을 단계별로 살펴보겠습니다.
|
||||
|
||||
## OAuth2 보안 스킴 { #oauth2-security-scheme }
|
||||
|
||||
첫 번째 변경 사항은 이제 사용 가능한 스코프 2개(`me`, `items`)로 OAuth2 보안 스킴을 선언한다는 점입니다.
|
||||
|
||||
`scopes` 매개변수는 각 스코프를 키로 하고, 설명을 값으로 하는 `dict`를 받습니다:
|
||||
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[63:66] *}
|
||||
|
||||
이제 스코프를 선언했기 때문에, 로그인/인가할 때 API 문서에 스코프가 표시됩니다.
|
||||
|
||||
그리고 접근을 허용할 스코프를 선택할 수 있게 됩니다: `me`와 `items`.
|
||||
|
||||
이는 Facebook, Google, GitHub 등으로 로그인하면서 권한을 부여할 때 사용되는 것과 동일한 메커니즘입니다:
|
||||
|
||||
<img src="/img/tutorial/security/image11.png">
|
||||
|
||||
## 스코프를 포함한 JWT 토큰 { #jwt-token-with-scopes }
|
||||
|
||||
이제 토큰 *경로 처리*를 수정해, 요청된 스코프를 반환하도록 합니다.
|
||||
|
||||
여전히 동일한 `OAuth2PasswordRequestForm`을 사용합니다. 여기에는 요청에서 받은 각 스코프를 담는 `scopes` 속성이 있으며, 타입은 `str`의 `list`입니다.
|
||||
|
||||
그리고 JWT 토큰의 일부로 스코프를 반환합니다.
|
||||
|
||||
/// danger | 위험
|
||||
|
||||
단순화를 위해, 여기서는 요청으로 받은 스코프를 그대로 토큰에 추가하고 있습니다.
|
||||
|
||||
하지만 실제 애플리케이션에서는 보안을 위해, 사용자가 실제로 가질 수 있는 스코프만(또는 미리 정의한 것만) 추가하도록 반드시 확인해야 합니다.
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[157] *}
|
||||
|
||||
## *경로 처리*와 의존성에서 스코프 선언하기 { #declare-scopes-in-path-operations-and-dependencies }
|
||||
|
||||
이제 `/users/me/items/`에 대한 *경로 처리*가 스코프 `items`를 요구한다고 선언합니다.
|
||||
|
||||
이를 위해 `fastapi`에서 `Security`를 import하여 사용합니다.
|
||||
|
||||
`Security`는 (`Depends`처럼) 의존성을 선언하는 데 사용할 수 있지만, `Security`는 스코프(문자열) 목록을 받는 `scopes` 매개변수도 받습니다.
|
||||
|
||||
이 경우, 의존성 함수 `get_current_active_user`를 `Security`에 전달합니다(`Depends`로 할 때와 같은 방식).
|
||||
|
||||
하지만 스코프 `list`도 함께 전달합니다. 여기서는 스코프 하나만: `items`(더 많을 수도 있습니다).
|
||||
|
||||
또한 의존성 함수 `get_current_active_user`는 `Depends`뿐 아니라 `Security`로도 하위 의존성을 선언할 수 있습니다. 자체 하위 의존성 함수(`get_current_user`)와 추가 스코프 요구사항을 선언합니다.
|
||||
|
||||
이 경우에는 스코프 `me`를 요구합니다(여러 스코프를 요구할 수도 있습니다).
|
||||
|
||||
/// note | 참고
|
||||
|
||||
반드시 서로 다른 곳에 서로 다른 스코프를 추가해야 하는 것은 아닙니다.
|
||||
|
||||
여기서는 **FastAPI**가 서로 다른 레벨에서 선언된 스코프를 어떻게 처리하는지 보여주기 위해 이렇게 합니다.
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[5,141,172] *}
|
||||
|
||||
/// info | 기술 세부사항
|
||||
|
||||
`Security`는 실제로 `Depends`의 서브클래스이며, 나중에 보게 될 추가 매개변수 하나만 더 있습니다.
|
||||
|
||||
하지만 `Depends` 대신 `Security`를 사용하면, **FastAPI**는 보안 스코프를 선언할 수 있음을 알고 내부적으로 이를 사용하며, OpenAPI로 API를 문서화할 수 있습니다.
|
||||
|
||||
하지만 `fastapi`에서 `Query`, `Path`, `Depends`, `Security` 등을 import할 때, 이것들은 실제로 특수한 클래스를 반환하는 함수입니다.
|
||||
|
||||
///
|
||||
|
||||
## `SecurityScopes` 사용하기 { #use-securityscopes }
|
||||
|
||||
이제 의존성 `get_current_user`를 업데이트합니다.
|
||||
|
||||
이는 위의 의존성들이 사용하는 것입니다.
|
||||
|
||||
여기에서 앞서 만든 동일한 OAuth2 스킴을 의존성으로 선언하여 사용합니다: `oauth2_scheme`.
|
||||
|
||||
이 의존성 함수 자체에는 스코프 요구사항이 없기 때문에, `oauth2_scheme`와 함께 `Depends`를 사용할 수 있습니다. 보안 스코프를 지정할 필요가 없을 때는 `Security`를 쓸 필요가 없습니다.
|
||||
|
||||
또한 `fastapi.security`에서 import한 `SecurityScopes` 타입의 특별한 매개변수를 선언합니다.
|
||||
|
||||
이 `SecurityScopes` 클래스는 `Request`와 비슷합니다(`Request`는 요청 객체를 직접 얻기 위해 사용했습니다).
|
||||
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[9,106] *}
|
||||
|
||||
## `scopes` 사용하기 { #use-the-scopes }
|
||||
|
||||
매개변수 `security_scopes`의 타입은 `SecurityScopes`입니다.
|
||||
|
||||
여기에는 `scopes` 속성이 있으며, 자기 자신과 이 함수를 하위 의존성으로 사용하는 모든 의존성이 요구하는 스코프 전체를 담은 `list`를 가집니다. 즉, 모든 “dependants”... 다소 헷갈릴 수 있는데, 아래에서 다시 설명합니다.
|
||||
|
||||
`security_scopes` 객체(`SecurityScopes` 클래스)에는 또한 `scope_str` 속성이 있는데, 공백으로 구분된 단일 문자열로 스코프들을 담고 있습니다(이를 사용할 것입니다).
|
||||
|
||||
나중에 여러 지점에서 재사용(`raise`)할 수 있는 `HTTPException`을 생성합니다.
|
||||
|
||||
이 예외에는 필요한 스코프(있다면)를 공백으로 구분된 문자열(`scope_str`)로 포함합니다. 그리고 그 스코프 문자열을 `WWW-Authenticate` 헤더에 넣습니다(이는 사양의 일부입니다).
|
||||
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[106,108:116] *}
|
||||
|
||||
## `username`과 데이터 형태 검증하기 { #verify-the-username-and-data-shape }
|
||||
|
||||
`username`을 얻었는지 확인하고, 스코프를 추출합니다.
|
||||
|
||||
그런 다음 Pydantic 모델로 데이터를 검증합니다(`ValidationError` 예외를 잡습니다). JWT 토큰을 읽거나 Pydantic으로 데이터를 검증하는 과정에서 오류가 나면, 앞에서 만든 `HTTPException`을 raise합니다.
|
||||
|
||||
이를 위해 Pydantic 모델 `TokenData`에 새 속성 `scopes`를 추가합니다.
|
||||
|
||||
Pydantic으로 데이터를 검증하면, 예를 들어 스코프가 정확히 `str`의 `list`이고 `username`이 `str`인지 등을 보장할 수 있습니다.
|
||||
|
||||
예를 들어 `dict`나 다른 형태라면, 나중에 애플리케이션이 어느 시점에 깨지면서 보안 위험이 될 수 있습니다.
|
||||
|
||||
또한 해당 username을 가진 사용자가 있는지 확인하고, 없다면 앞에서 만든 동일한 예외를 raise합니다.
|
||||
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[47,117:129] *}
|
||||
|
||||
## `scopes` 검증하기 { #verify-the-scopes }
|
||||
|
||||
이제 이 의존성과 모든 dependant( *경로 처리* 포함)가 요구하는 모든 스코프가, 받은 토큰의 스코프에 포함되어 있는지 확인합니다. 그렇지 않으면 `HTTPException`을 raise합니다.
|
||||
|
||||
이를 위해, 모든 스코프를 `str`로 담고 있는 `security_scopes.scopes`를 사용합니다.
|
||||
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[130:136] *}
|
||||
|
||||
## 의존성 트리와 스코프 { #dependency-tree-and-scopes }
|
||||
|
||||
이 의존성 트리와 스코프를 다시 살펴보겠습니다.
|
||||
|
||||
`get_current_active_user` 의존성은 `get_current_user`를 하위 의존성으로 가지므로, `get_current_active_user`에서 선언된 스코프 `"me"`는 `get_current_user`에 전달되는 `security_scopes.scopes`의 요구 스코프 목록에 포함됩니다.
|
||||
|
||||
*경로 처리* 자체도 스코프 `"items"`를 선언하므로, 이것 또한 `get_current_user`에 전달되는 `security_scopes.scopes` 목록에 포함됩니다.
|
||||
|
||||
의존성과 스코프의 계층 구조는 다음과 같습니다:
|
||||
|
||||
* *경로 처리* `read_own_items`는:
|
||||
* 의존성과 함께 요구 스코프 `["items"]`를 가집니다:
|
||||
* `get_current_active_user`:
|
||||
* 의존성 함수 `get_current_active_user`는:
|
||||
* 의존성과 함께 요구 스코프 `["me"]`를 가집니다:
|
||||
* `get_current_user`:
|
||||
* 의존성 함수 `get_current_user`는:
|
||||
* 자체적으로는 요구 스코프가 없습니다.
|
||||
* `oauth2_scheme`를 사용하는 의존성이 있습니다.
|
||||
* `SecurityScopes` 타입의 `security_scopes` 매개변수가 있습니다:
|
||||
* 이 `security_scopes` 매개변수는 위에서 선언된 모든 스코프를 담은 `list`인 `scopes` 속성을 가지므로:
|
||||
* *경로 처리* `read_own_items`의 경우 `security_scopes.scopes`에는 `["me", "items"]`가 들어갑니다.
|
||||
* *경로 처리* `read_users_me`의 경우 `security_scopes.scopes`에는 `["me"]`가 들어갑니다. 이는 의존성 `get_current_active_user`에서 선언되기 때문입니다.
|
||||
* *경로 처리* `read_system_status`의 경우 `security_scopes.scopes`에는 `[]`(없음)가 들어갑니다. `scopes`가 있는 `Security`를 선언하지 않았고, 그 의존성인 `get_current_user`도 `scopes`를 선언하지 않았기 때문입니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
여기서 중요한 “마법 같은” 점은 `get_current_user`가 각 *경로 처리*마다 검사해야 하는 `scopes` 목록이 달라진다는 것입니다.
|
||||
|
||||
이는 특정 *경로 처리*에 대한 의존성 트리에서, 각 *경로 처리*와 각 의존성에 선언된 `scopes`에 따라 달라집니다.
|
||||
|
||||
///
|
||||
|
||||
## `SecurityScopes`에 대한 추가 설명 { #more-details-about-securityscopes }
|
||||
|
||||
`SecurityScopes`는 어느 지점에서든, 그리고 여러 곳에서 사용할 수 있으며, “루트” 의존성에만 있어야 하는 것은 아닙니다.
|
||||
|
||||
`SecurityScopes`는 **해당 특정** *경로 처리*와 **해당 특정** 의존성 트리에 대해, 현재 `Security` 의존성과 모든 dependant에 선언된 보안 스코프를 항상 갖고 있습니다.
|
||||
|
||||
`SecurityScopes`에는 dependant가 선언한 모든 스코프가 포함되므로, 중앙의 의존성 함수에서 토큰이 필요한 스코프를 가지고 있는지 검증한 다음, 서로 다른 *경로 처리*에서 서로 다른 스코프 요구사항을 선언할 수 있습니다.
|
||||
|
||||
이들은 각 *경로 처리*마다 독립적으로 검사됩니다.
|
||||
|
||||
## 확인하기 { #check-it }
|
||||
|
||||
API 문서를 열면, 인증하고 인가할 스코프를 지정할 수 있습니다.
|
||||
|
||||
<img src="/img/tutorial/security/image11.png">
|
||||
|
||||
어떤 스코프도 선택하지 않으면 “인증”은 되지만, `/users/me/` 또는 `/users/me/items/`에 접근하려고 하면 권한이 충분하지 않다는 오류가 발생합니다. `/status/`에는 여전히 접근할 수 있습니다.
|
||||
|
||||
그리고 스코프 `me`는 선택했지만 스코프 `items`는 선택하지 않았다면, `/users/me/`에는 접근할 수 있지만 `/users/me/items/`에는 접근할 수 없습니다.
|
||||
|
||||
이는 사용자가 애플리케이션에 얼마나 많은 권한을 부여했는지에 따라, 제3자 애플리케이션이 사용자로부터 제공받은 토큰으로 이 *경로 처리*들 중 하나에 접근하려고 할 때 발생하는 상황과 같습니다.
|
||||
|
||||
## 제3자 통합에 대해 { #about-third-party-integrations }
|
||||
|
||||
이 예제에서는 OAuth2 “password” 플로우를 사용하고 있습니다.
|
||||
|
||||
이는 보통 자체 프론트엔드가 있는 우리 애플리케이션에 로그인할 때 적합합니다.
|
||||
|
||||
우리가 이를 통제하므로 `username`과 `password`를 받는 것을 신뢰할 수 있기 때문입니다.
|
||||
|
||||
하지만 다른 사람들이 연결할 OAuth2 애플리케이션(즉, Facebook, Google, GitHub 등과 동등한 인증 제공자를 만들고 있다면)을 구축한다면, 다른 플로우 중 하나를 사용해야 합니다.
|
||||
|
||||
가장 흔한 것은 implicit 플로우입니다.
|
||||
|
||||
가장 안전한 것은 code 플로우이지만, 더 많은 단계가 필요해 구현이 더 복잡합니다. 복잡하기 때문에 많은 제공자는 implicit 플로우를 권장하게 됩니다.
|
||||
|
||||
/// note | 참고
|
||||
|
||||
인증 제공자마다 자신들의 브랜드의 일부로 만들기 위해, 각 플로우를 서로 다른 방식으로 이름 붙이는 경우가 흔합니다.
|
||||
|
||||
하지만 결국, 동일한 OAuth2 표준을 구현하고 있는 것입니다.
|
||||
|
||||
///
|
||||
|
||||
**FastAPI**는 `fastapi.security.oauth2`에 이러한 모든 OAuth2 인증 플로우를 위한 유틸리티를 포함합니다.
|
||||
|
||||
## 데코레이터 `dependencies`에서의 `Security` { #security-in-decorator-dependencies }
|
||||
|
||||
[경로 처리 데코레이터의 의존성](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}에서 설명한 것처럼 데코레이터의 `dependencies` 매개변수에 `Depends`의 `list`를 정의할 수 있는 것과 같은 방식으로, 거기에서 `scopes`와 함께 `Security`를 사용할 수도 있습니다.
|
||||
@@ -1,302 +0,0 @@
|
||||
# 설정과 환경 변수 { #settings-and-environment-variables }
|
||||
|
||||
많은 경우 애플리케이션에는 외부 설정이나 구성(예: secret key, 데이터베이스 자격 증명, 이메일 서비스 자격 증명 등)이 필요할 수 있습니다.
|
||||
|
||||
이러한 설정 대부분은 데이터베이스 URL처럼 변동 가능(변경될 수 있음)합니다. 그리고 많은 설정은 secret처럼 민감할 수 있습니다.
|
||||
|
||||
이 때문에 보통 애플리케이션이 읽어들이는 환경 변수로 이를 제공하는 것이 일반적입니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
환경 변수를 이해하려면 [환경 변수](../environment-variables.md){.internal-link target=_blank}를 읽어보세요.
|
||||
|
||||
///
|
||||
|
||||
## 타입과 검증 { #types-and-validation }
|
||||
|
||||
이 환경 변수들은 Python 외부에 있으며 다른 프로그램 및 시스템의 나머지 부분(그리고 Linux, Windows, macOS 같은 서로 다른 운영체제와도)과 호환되어야 하므로, 텍스트 문자열만 다룰 수 있습니다.
|
||||
|
||||
즉, Python에서 환경 변수로부터 읽어온 어떤 값이든 `str`이 되며, 다른 타입으로의 변환이나 검증은 코드에서 수행해야 합니다.
|
||||
|
||||
## Pydantic `Settings` { #pydantic-settings }
|
||||
|
||||
다행히 Pydantic은 <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/" class="external-link" target="_blank">Pydantic: Settings management</a>를 통해 환경 변수에서 오는 이러한 설정을 처리할 수 있는 훌륭한 유틸리티를 제공합니다.
|
||||
|
||||
### `pydantic-settings` 설치하기 { #install-pydantic-settings }
|
||||
|
||||
먼저 [가상 환경](../virtual-environments.md){.internal-link target=_blank}을 만들고 활성화한 다음, `pydantic-settings` 패키지를 설치하세요:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ pip install pydantic-settings
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
또는 다음처럼 `all` extras를 설치하면 함께 포함됩니다:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ pip install "fastapi[all]"
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
### `Settings` 객체 만들기 { #create-the-settings-object }
|
||||
|
||||
Pydantic에서 `BaseSettings`를 import하고, Pydantic 모델과 매우 비슷하게 서브클래스를 만드세요.
|
||||
|
||||
Pydantic 모델과 같은 방식으로, 타입 어노테이션(그리고 필요하다면 기본값)과 함께 클래스 속성을 선언합니다.
|
||||
|
||||
다양한 데이터 타입, `Field()`로 추가 검증 등 Pydantic 모델에서 사용하는 동일한 검증 기능과 도구를 모두 사용할 수 있습니다.
|
||||
|
||||
{* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *}
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
빠르게 복사/붙여넣기할 예시가 필요하다면, 이 예시는 사용하지 말고 아래의 마지막 예시를 사용하세요.
|
||||
|
||||
///
|
||||
|
||||
그 다음, 해당 `Settings` 클래스의 인스턴스(여기서는 `settings` 객체)를 생성하면 Pydantic이 대소문자를 구분하지 않고 환경 변수를 읽습니다. 따라서 대문자 변수 `APP_NAME`도 `app_name` 속성에 대해 읽힙니다.
|
||||
|
||||
이후 데이터를 변환하고 검증합니다. 그래서 그 `settings` 객체를 사용할 때는 선언한 타입의 데이터를 갖게 됩니다(예: `items_per_user`는 `int`가 됩니다).
|
||||
|
||||
### `settings` 사용하기 { #use-the-settings }
|
||||
|
||||
이제 애플리케이션에서 새 `settings` 객체를 사용할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/settings/tutorial001_py39.py hl[18:20] *}
|
||||
|
||||
### 서버 실행하기 { #run-the-server }
|
||||
|
||||
다음으로 환경 변수를 통해 구성을 전달하면서 서버를 실행합니다. 예를 들어 다음처럼 `ADMIN_EMAIL`과 `APP_NAME`을 설정할 수 있습니다:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.py
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
하나의 명령에 여러 env var를 설정하려면 공백으로 구분하고, 모두 명령 앞에 두세요.
|
||||
|
||||
///
|
||||
|
||||
그러면 `admin_email` 설정은 `"deadpool@example.com"`으로 설정됩니다.
|
||||
|
||||
`app_name`은 `"ChimichangApp"`이 됩니다.
|
||||
|
||||
그리고 `items_per_user`는 기본값 `50`을 유지합니다.
|
||||
|
||||
## 다른 모듈의 설정 { #settings-in-another-module }
|
||||
|
||||
[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}에서 본 것처럼, 설정을 다른 모듈 파일에 넣을 수도 있습니다.
|
||||
|
||||
예를 들어 `config.py` 파일을 다음처럼 만들 수 있습니다:
|
||||
|
||||
{* ../../docs_src/settings/app01_py39/config.py *}
|
||||
|
||||
그리고 `main.py` 파일에서 이를 사용합니다:
|
||||
|
||||
{* ../../docs_src/settings/app01_py39/main.py hl[3,11:13] *}
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}에서 본 것처럼 `__init__.py` 파일도 필요합니다.
|
||||
|
||||
///
|
||||
|
||||
## 의존성에서 설정 사용하기 { #settings-in-a-dependency }
|
||||
|
||||
어떤 경우에는 어디서나 사용되는 전역 `settings` 객체를 두는 대신, 의존성에서 설정을 제공하는 것이 유용할 수 있습니다.
|
||||
|
||||
이는 특히 테스트 중에 유용할 수 있는데, 사용자 정의 설정으로 의존성을 override하기가 매우 쉽기 때문입니다.
|
||||
|
||||
### config 파일 { #the-config-file }
|
||||
|
||||
이전 예시에서 이어서, `config.py` 파일은 다음과 같을 수 있습니다:
|
||||
|
||||
{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *}
|
||||
|
||||
이제는 기본 인스턴스 `settings = Settings()`를 생성하지 않는다는 점에 유의하세요.
|
||||
|
||||
### 메인 앱 파일 { #the-main-app-file }
|
||||
|
||||
이제 새로운 `config.Settings()`를 반환하는 의존성을 생성합니다.
|
||||
|
||||
{* ../../docs_src/settings/app02_an_py39/main.py hl[6,12:13] *}
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
`@lru_cache`는 조금 뒤에 다룹니다.
|
||||
|
||||
지금은 `get_settings()`가 일반 함수라고 가정해도 됩니다.
|
||||
|
||||
///
|
||||
|
||||
그 다음 *경로 처리 함수*에서 이를 의존성으로 요구하고, 필요한 어디서든 사용할 수 있습니다.
|
||||
|
||||
{* ../../docs_src/settings/app02_an_py39/main.py hl[17,19:21] *}
|
||||
|
||||
### 설정과 테스트 { #settings-and-testing }
|
||||
|
||||
그 다음, `get_settings`에 대한 의존성 override를 만들어 테스트 중에 다른 설정 객체를 제공하기가 매우 쉬워집니다:
|
||||
|
||||
{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *}
|
||||
|
||||
의존성 override에서는 새 `Settings` 객체를 생성할 때 `admin_email`의 새 값을 설정하고, 그 새 객체를 반환합니다.
|
||||
|
||||
그 다음 그것이 사용되는지 테스트할 수 있습니다.
|
||||
|
||||
## `.env` 파일 읽기 { #reading-a-env-file }
|
||||
|
||||
많이 바뀔 수 있는 설정이 많고, 서로 다른 환경에서 사용한다면, 이를 파일에 넣어 환경 변수인 것처럼 읽는 것이 유용할 수 있습니다.
|
||||
|
||||
이 관행은 충분히 흔해서 이름도 있는데, 이러한 환경 변수들은 보통 `.env` 파일에 두며, 그 파일을 "dotenv"라고 부릅니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
점(`.`)으로 시작하는 파일은 Linux, macOS 같은 Unix 계열 시스템에서 숨김 파일입니다.
|
||||
|
||||
하지만 dotenv 파일이 꼭 그 정확한 파일명을 가져야 하는 것은 아닙니다.
|
||||
|
||||
///
|
||||
|
||||
Pydantic은 외부 라이브러리를 사용해 이런 유형의 파일에서 읽는 기능을 지원합니다. 자세한 내용은 <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/#dotenv-env-support" class="external-link" target="_blank">Pydantic Settings: Dotenv (.env) support</a>를 참고하세요.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
이를 사용하려면 `pip install python-dotenv`가 필요합니다.
|
||||
|
||||
///
|
||||
|
||||
### `.env` 파일 { #the-env-file }
|
||||
|
||||
다음과 같은 `.env` 파일을 둘 수 있습니다:
|
||||
|
||||
```bash
|
||||
ADMIN_EMAIL="deadpool@example.com"
|
||||
APP_NAME="ChimichangApp"
|
||||
```
|
||||
|
||||
### `.env`에서 설정 읽기 { #read-settings-from-env }
|
||||
|
||||
그리고 `config.py`를 다음처럼 업데이트합니다:
|
||||
|
||||
{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *}
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
`model_config` 속성은 Pydantic 설정을 위한 것입니다. 자세한 내용은 <a href="https://docs.pydantic.dev/latest/concepts/config/" class="external-link" target="_blank">Pydantic: Concepts: Configuration</a>을 참고하세요.
|
||||
|
||||
///
|
||||
|
||||
여기서는 Pydantic `Settings` 클래스 안에 config `env_file`을 정의하고, 사용하려는 dotenv 파일의 파일명을 값으로 설정합니다.
|
||||
|
||||
### `lru_cache`로 `Settings`를 한 번만 생성하기 { #creating-the-settings-only-once-with-lru-cache }
|
||||
|
||||
디스크에서 파일을 읽는 것은 보통 비용이 큰(느린) 작업이므로, 각 요청마다 읽기보다는 한 번만 수행하고 동일한 settings 객체를 재사용하는 것이 좋습니다.
|
||||
|
||||
하지만 매번 다음을 수행하면:
|
||||
|
||||
```Python
|
||||
Settings()
|
||||
```
|
||||
|
||||
새 `Settings` 객체가 생성되고, 생성 시점에 `.env` 파일을 다시 읽게 됩니다.
|
||||
|
||||
의존성 함수가 단순히 다음과 같다면:
|
||||
|
||||
```Python
|
||||
def get_settings():
|
||||
return Settings()
|
||||
```
|
||||
|
||||
요청마다 객체를 생성하게 되고, 요청마다 `.env` 파일을 읽게 됩니다. ⚠️
|
||||
|
||||
하지만 위에 `@lru_cache` 데코레이터를 사용하고 있으므로, `Settings` 객체는 최초 호출 시 딱 한 번만 생성됩니다. ✔️
|
||||
|
||||
{* ../../docs_src/settings/app03_an_py39/main.py hl[1,11] *}
|
||||
|
||||
그 다음 요청들에서 의존성으로 `get_settings()`가 다시 호출될 때마다, `get_settings()`의 내부 코드를 실행해서 새 `Settings` 객체를 만드는 대신, 첫 호출에서 반환된 동일한 객체를 계속 반환합니다.
|
||||
|
||||
#### `lru_cache` Technical Details { #lru-cache-technical-details }
|
||||
|
||||
`@lru_cache`는 데코레이션한 함수가 매번 다시 계산하는 대신, 첫 번째에 반환된 동일한 값을 반환하도록 함수를 수정합니다(즉, 매번 함수 코드를 실행하지 않습니다).
|
||||
|
||||
따라서 아래의 함수는 인자 조합마다 한 번씩 실행됩니다. 그리고 각각의 인자 조합에 대해 반환된 값은, 함수가 정확히 같은 인자 조합으로 호출될 때마다 반복해서 사용됩니다.
|
||||
|
||||
예를 들어 다음 함수가 있다면:
|
||||
|
||||
```Python
|
||||
@lru_cache
|
||||
def say_hi(name: str, salutation: str = "Ms."):
|
||||
return f"Hello {salutation} {name}"
|
||||
```
|
||||
|
||||
프로그램은 다음과 같이 실행될 수 있습니다:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
|
||||
participant code as Code
|
||||
participant function as say_hi()
|
||||
participant execute as Execute function
|
||||
|
||||
rect rgba(0, 255, 0, .1)
|
||||
code ->> function: say_hi(name="Camila")
|
||||
function ->> execute: execute function code
|
||||
execute ->> code: return the result
|
||||
end
|
||||
|
||||
rect rgba(0, 255, 255, .1)
|
||||
code ->> function: say_hi(name="Camila")
|
||||
function ->> code: return stored result
|
||||
end
|
||||
|
||||
rect rgba(0, 255, 0, .1)
|
||||
code ->> function: say_hi(name="Rick")
|
||||
function ->> execute: execute function code
|
||||
execute ->> code: return the result
|
||||
end
|
||||
|
||||
rect rgba(0, 255, 0, .1)
|
||||
code ->> function: say_hi(name="Rick", salutation="Mr.")
|
||||
function ->> execute: execute function code
|
||||
execute ->> code: return the result
|
||||
end
|
||||
|
||||
rect rgba(0, 255, 255, .1)
|
||||
code ->> function: say_hi(name="Rick")
|
||||
function ->> code: return stored result
|
||||
end
|
||||
|
||||
rect rgba(0, 255, 255, .1)
|
||||
code ->> function: say_hi(name="Camila")
|
||||
function ->> code: return stored result
|
||||
end
|
||||
```
|
||||
|
||||
우리의 의존성 `get_settings()`의 경우, 함수가 어떤 인자도 받지 않으므로 항상 같은 값을 반환합니다.
|
||||
|
||||
이렇게 하면 거의 전역 변수처럼 동작합니다. 하지만 의존성 함수를 사용하므로 테스트를 위해 쉽게 override할 수 있습니다.
|
||||
|
||||
`@lru_cache`는 Python 표준 라이브러리의 `functools`에 포함되어 있으며, 자세한 내용은 <a href="https://docs.python.org/3/library/functools.html#functools.lru_cache" class="external-link" target="_blank">`@lru_cache`에 대한 Python 문서</a>에서 확인할 수 있습니다.
|
||||
|
||||
## 정리 { #recap }
|
||||
|
||||
Pydantic Settings를 사용하면 Pydantic 모델의 모든 강력한 기능을 활용해 애플리케이션의 설정 또는 구성을 처리할 수 있습니다.
|
||||
|
||||
* 의존성을 사용하면 테스트를 단순화할 수 있습니다.
|
||||
* `.env` 파일을 사용할 수 있습니다.
|
||||
* `@lru_cache`를 사용하면 각 요청마다 dotenv 파일을 반복해서 읽는 것을 피하면서도, 테스트 중에는 이를 override할 수 있습니다.
|
||||
@@ -1,485 +0,0 @@
|
||||
# 대안, 영감, 비교 { #alternatives-inspiration-and-comparisons }
|
||||
|
||||
**FastAPI**에 영감을 준 것들, 대안과의 비교, 그리고 그로부터 무엇을 배웠는지에 대한 내용입니다.
|
||||
|
||||
## 소개 { #intro }
|
||||
|
||||
다른 사람들의 이전 작업이 없었다면 **FastAPI**는 존재하지 않았을 것입니다.
|
||||
|
||||
그 전에 만들어진 많은 도구들이 **FastAPI**의 탄생에 영감을 주었습니다.
|
||||
|
||||
저는 여러 해 동안 새로운 framework를 만드는 것을 피하고 있었습니다. 먼저 **FastAPI**가 다루는 모든 기능을 여러 서로 다른 framework, plug-in, 도구를 사용해 해결해 보려고 했습니다.
|
||||
|
||||
하지만 어느 시점에는, 이전 도구들의 가장 좋은 아이디어를 가져와 가능한 최선의 방식으로 조합하고, 이전에는 존재하지 않았던 언어 기능(Python 3.6+ type hints)을 활용해 이 모든 기능을 제공하는 무언가를 만드는 것 외에는 다른 선택지가 없었습니다.
|
||||
|
||||
## 이전 도구들 { #previous-tools }
|
||||
|
||||
### <a href="https://www.djangoproject.com/" class="external-link" target="_blank">Django</a> { #django }
|
||||
|
||||
가장 인기 있는 Python framework이며 널리 신뢰받고 있습니다. Instagram 같은 시스템을 만드는 데 사용됩니다.
|
||||
|
||||
상대적으로 관계형 데이터베이스(예: MySQL 또는 PostgreSQL)와 강하게 결합되어 있어서, NoSQL 데이터베이스(예: Couchbase, MongoDB, Cassandra 등)를 주 저장 엔진으로 사용하는 것은 그리 쉽지 않습니다.
|
||||
|
||||
백엔드에서 HTML을 생성하기 위해 만들어졌지, 현대적인 프런트엔드(예: React, Vue.js, Angular)나 다른 시스템(예: <abbr title="Internet of Things - 사물 인터넷">IoT</abbr> 기기)에서 사용되는 API를 만들기 위해 설계된 것은 아닙니다.
|
||||
|
||||
### <a href="https://www.django-rest-framework.org/" class="external-link" target="_blank">Django REST Framework</a> { #django-rest-framework }
|
||||
|
||||
Django REST framework는 Django를 기반으로 Web API를 구축하기 위한 유연한 toolkit으로 만들어졌고, Django의 API 기능을 개선하기 위한 목적이었습니다.
|
||||
|
||||
Mozilla, Red Hat, Eventbrite를 포함해 많은 회사에서 사용합니다.
|
||||
|
||||
**자동 API 문서화**의 초기 사례 중 하나였고, 이것이 특히 **FastAPI**를 "찾게 된" 첫 아이디어 중 하나였습니다.
|
||||
|
||||
/// note | 참고
|
||||
|
||||
Django REST Framework는 Tom Christie가 만들었습니다. **FastAPI**의 기반이 되는 Starlette와 Uvicorn을 만든 사람과 동일합니다.
|
||||
|
||||
///
|
||||
|
||||
/// check | **FastAPI**에 영감을 준 것
|
||||
|
||||
자동 API 문서화 웹 사용자 인터페이스를 제공하기.
|
||||
|
||||
///
|
||||
|
||||
### <a href="https://flask.palletsprojects.com" class="external-link" target="_blank">Flask</a> { #flask }
|
||||
|
||||
Flask는 "microframework"로, Django에 기본으로 포함된 데이터베이스 통합이나 여러 기능들을 포함하지 않습니다.
|
||||
|
||||
이 단순함과 유연성 덕분에 NoSQL 데이터베이스를 주 데이터 저장 시스템으로 사용하는 같은 작업이 가능합니다.
|
||||
|
||||
매우 단순하기 때문에 비교적 직관적으로 배울 수 있지만, 문서가 어떤 지점에서는 다소 기술적으로 깊어지기도 합니다.
|
||||
|
||||
또한 데이터베이스, 사용자 관리, 혹은 Django에 미리 구축되어 있는 다양한 기능들이 꼭 필요하지 않은 다른 애플리케이션에도 흔히 사용됩니다. 물론 이런 기능들 중 다수는 plug-in으로 추가할 수 있습니다.
|
||||
|
||||
이런 구성요소의 분리와, 필요한 것만 정확히 덧붙여 확장할 수 있는 "microframework"라는 점은 제가 유지하고 싶었던 핵심 특성이었습니다.
|
||||
|
||||
Flask의 단순함을 고려하면 API를 구축하는 데 잘 맞는 것처럼 보였습니다. 다음으로 찾고자 했던 것은 Flask용 "Django REST Framework"였습니다.
|
||||
|
||||
/// check | **FastAPI**에 영감을 준 것
|
||||
|
||||
micro-framework가 되기. 필요한 도구와 구성요소를 쉽게 조합할 수 있도록 하기.
|
||||
|
||||
단순하고 사용하기 쉬운 routing 시스템을 갖기.
|
||||
|
||||
///
|
||||
|
||||
### <a href="https://requests.readthedocs.io" class="external-link" target="_blank">Requests</a> { #requests }
|
||||
|
||||
**FastAPI**는 실제로 **Requests**의 대안이 아닙니다. 둘의 범위는 매우 다릅니다.
|
||||
|
||||
실제로 FastAPI 애플리케이션 *내부에서* Requests를 사용하는 경우도 흔합니다.
|
||||
|
||||
그럼에도 FastAPI는 Requests로부터 꽤 많은 영감을 얻었습니다.
|
||||
|
||||
**Requests**는 (클라이언트로서) API와 *상호작용*하기 위한 라이브러리이고, **FastAPI**는 (서버로서) API를 *구축*하기 위한 라이브러리입니다.
|
||||
|
||||
대략 말하면 서로 반대편에 있으며, 서로를 보완합니다.
|
||||
|
||||
Requests는 매우 단순하고 직관적인 설계를 가졌고, 합리적인 기본값을 바탕으로 사용하기가 아주 쉽습니다. 동시에 매우 강력하고 커스터마이징도 가능합니다.
|
||||
|
||||
그래서 공식 웹사이트에서 말하듯이:
|
||||
|
||||
> Requests is one of the most downloaded Python packages of all time
|
||||
|
||||
사용 방법은 매우 간단합니다. 예를 들어 `GET` 요청을 하려면 다음처럼 작성합니다:
|
||||
|
||||
```Python
|
||||
response = requests.get("http://example.com/some/url")
|
||||
```
|
||||
|
||||
이에 대응하는 FastAPI의 API *경로 처리*는 다음과 같이 보일 수 있습니다:
|
||||
|
||||
```Python hl_lines="1"
|
||||
@app.get("/some/url")
|
||||
def read_url():
|
||||
return {"message": "Hello World"}
|
||||
```
|
||||
|
||||
`requests.get(...)`와 `@app.get(...)`의 유사성을 확인해 보세요.
|
||||
|
||||
/// check | **FastAPI**에 영감을 준 것
|
||||
|
||||
* 단순하고 직관적인 API를 갖기.
|
||||
* HTTP method 이름(operations)을 직접, 직관적이고 명확한 방식으로 사용하기.
|
||||
* 합리적인 기본값을 제공하되, 강력한 커스터마이징을 가능하게 하기.
|
||||
|
||||
///
|
||||
|
||||
### <a href="https://swagger.io/" class="external-link" target="_blank">Swagger</a> / <a href="https://github.com/OAI/OpenAPI-Specification/" class="external-link" target="_blank">OpenAPI</a> { #swagger-openapi }
|
||||
|
||||
제가 Django REST Framework에서 가장 원했던 주요 기능은 자동 API 문서화였습니다.
|
||||
|
||||
그 후 JSON(또는 JSON의 확장인 YAML)을 사용해 API를 문서화하는 표준인 Swagger가 있다는 것을 알게 되었습니다.
|
||||
|
||||
그리고 Swagger API를 위한 웹 사용자 인터페이스도 이미 만들어져 있었습니다. 그래서 API에 대한 Swagger 문서를 생성할 수 있다면, 이 웹 사용자 인터페이스를 자동으로 사용할 수 있게 됩니다.
|
||||
|
||||
어느 시점에 Swagger는 Linux Foundation으로 넘어가 OpenAPI로 이름이 바뀌었습니다.
|
||||
|
||||
그래서 2.0 버전을 이야기할 때는 "Swagger"라고 말하는 것이 일반적이고, 3+ 버전은 "OpenAPI"라고 말하는 것이 일반적입니다.
|
||||
|
||||
/// check | **FastAPI**에 영감을 준 것
|
||||
|
||||
커스텀 schema 대신, API 사양을 위한 열린 표준을 채택하고 사용하기.
|
||||
|
||||
또한 표준 기반의 사용자 인터페이스 도구를 통합하기:
|
||||
|
||||
* <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>
|
||||
* <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>
|
||||
|
||||
이 두 가지는 꽤 대중적이고 안정적이기 때문에 선택되었습니다. 하지만 간단히 검색해보면 OpenAPI를 위한 대안 UI가 수십 가지나 있다는 것을 알 수 있습니다(**FastAPI**와 함께 사용할 수 있습니다).
|
||||
|
||||
///
|
||||
|
||||
### Flask REST framework들 { #flask-rest-frameworks }
|
||||
|
||||
Flask REST framework는 여러 개가 있지만, 시간을 들여 조사해 본 결과, 상당수가 중단되었거나 방치되어 있었고, 해결되지 않은 여러 이슈 때문에 적합하지 않은 경우가 많았습니다.
|
||||
|
||||
### <a href="https://marshmallow.readthedocs.io/en/stable/" class="external-link" target="_blank">Marshmallow</a> { #marshmallow }
|
||||
|
||||
API 시스템에 필요한 주요 기능 중 하나는 데이터 "<abbr title="also called marshalling, conversion - 마샬링, 변환이라고도 합니다">serialization</abbr>"입니다. 이는 코드(Python)에서 데이터를 가져와 네트워크로 전송할 수 있는 형태로 변환하는 것을 의미합니다. 예를 들어 데이터베이스의 데이터를 담은 객체를 JSON 객체로 변환하거나, `datetime` 객체를 문자열로 변환하는 등의 작업입니다.
|
||||
|
||||
API에 또 하나 크게 필요한 기능은 데이터 검증입니다. 특정 파라미터를 기준으로 데이터가 유효한지 확인하는 것입니다. 예를 들어 어떤 필드가 `int`인지, 임의의 문자열이 아닌지 확인하는 식입니다. 이는 특히 들어오는 데이터에 유용합니다.
|
||||
|
||||
데이터 검증 시스템이 없다면, 모든 검사를 코드에서 수동으로 해야 합니다.
|
||||
|
||||
이런 기능들을 제공하기 위해 Marshmallow가 만들어졌습니다. 훌륭한 라이브러리이며, 저도 이전에 많이 사용했습니다.
|
||||
|
||||
하지만 Python type hints가 존재하기 전에 만들어졌습니다. 그래서 각 <abbr title="the definition of how data should be formed - 데이터가 어떻게 구성되어야 하는지에 대한 정의">schema</abbr>를 정의하려면 Marshmallow가 제공하는 특정 유틸리티와 클래스를 사용해야 합니다.
|
||||
|
||||
/// check | **FastAPI**에 영감을 준 것
|
||||
|
||||
데이터 타입과 검증을 제공하는 "schema"를 코드로 정의하고, 이를 자동으로 활용하기.
|
||||
|
||||
///
|
||||
|
||||
### <a href="https://webargs.readthedocs.io/en/latest/" class="external-link" target="_blank">Webargs</a> { #webargs }
|
||||
|
||||
API에 필요한 또 다른 큰 기능은 들어오는 요청에서 데이터를 <abbr title="reading and converting to Python data - 읽어서 Python 데이터로 변환하기">parsing</abbr>하는 것입니다.
|
||||
|
||||
Webargs는 Flask를 포함한 여러 framework 위에서 이를 제공하기 위해 만들어진 도구입니다.
|
||||
|
||||
내부적으로 Marshmallow를 사용해 데이터 검증을 수행합니다. 그리고 같은 개발자들이 만들었습니다.
|
||||
|
||||
아주 훌륭한 도구이며, 저도 **FastAPI**를 만들기 전에 많이 사용했습니다.
|
||||
|
||||
/// info | 정보
|
||||
|
||||
Webargs는 Marshmallow와 같은 개발자들이 만들었습니다.
|
||||
|
||||
///
|
||||
|
||||
/// check | **FastAPI**에 영감을 준 것
|
||||
|
||||
들어오는 요청 데이터의 자동 검증을 갖기.
|
||||
|
||||
///
|
||||
|
||||
### <a href="https://apispec.readthedocs.io/en/stable/" class="external-link" target="_blank">APISpec</a> { #apispec }
|
||||
|
||||
Marshmallow와 Webargs는 plug-in 형태로 검증, parsing, serialization을 제공합니다.
|
||||
|
||||
하지만 문서화는 여전히 부족했습니다. 그래서 APISpec이 만들어졌습니다.
|
||||
|
||||
이는 여러 framework를 위한 plug-in이며(Starlette용 plug-in도 있습니다).
|
||||
|
||||
작동 방식은, 각 route를 처리하는 함수의 docstring 안에 YAML 형식으로 schema 정의를 작성하고,
|
||||
|
||||
그로부터 OpenAPI schema를 생성합니다.
|
||||
|
||||
Flask, Starlette, Responder 등에서 이런 방식으로 동작합니다.
|
||||
|
||||
하지만 다시, Python 문자열 내부(큰 YAML)에서 micro-syntax를 다루어야 한다는 문제가 있습니다.
|
||||
|
||||
에디터가 이를 크게 도와주지 못합니다. 또한 파라미터나 Marshmallow schema를 수정해놓고 YAML docstring도 같이 수정하는 것을 잊어버리면, 생성된 schema는 오래된 상태가 됩니다.
|
||||
|
||||
/// info | 정보
|
||||
|
||||
APISpec은 Marshmallow와 같은 개발자들이 만들었습니다.
|
||||
|
||||
///
|
||||
|
||||
/// check | **FastAPI**에 영감을 준 것
|
||||
|
||||
API를 위한 열린 표준인 OpenAPI를 지원하기.
|
||||
|
||||
///
|
||||
|
||||
### <a href="https://flask-apispec.readthedocs.io/en/latest/" class="external-link" target="_blank">Flask-apispec</a> { #flask-apispec }
|
||||
|
||||
Flask plug-in으로, Webargs, Marshmallow, APISpec을 묶어줍니다.
|
||||
|
||||
Webargs와 Marshmallow의 정보를 사용해 APISpec으로 OpenAPI schema를 자동 생성합니다.
|
||||
|
||||
훌륭한 도구인데도 과소평가되어 있습니다. 다른 많은 Flask plug-in보다 훨씬 더 유명해져야 합니다. 문서가 너무 간결하고 추상적이라서 그럴 수도 있습니다.
|
||||
|
||||
이 도구는 Python docstring 내부에 YAML(또 다른 문법)을 작성해야 하는 문제를 해결했습니다.
|
||||
|
||||
Flask + Flask-apispec + Marshmallow + Webargs 조합은 **FastAPI**를 만들기 전까지 제가 가장 좋아하던 백엔드 stack이었습니다.
|
||||
|
||||
이를 사용하면서 여러 Flask full-stack generator가 만들어졌습니다. 이것들이 지금까지 저(그리고 여러 외부 팀)가 사용해 온 주요 stack입니다:
|
||||
|
||||
* <a href="https://github.com/tiangolo/full-stack" class="external-link" target="_blank">https://github.com/tiangolo/full-stack</a>
|
||||
* <a href="https://github.com/tiangolo/full-stack-flask-couchbase" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-flask-couchbase</a>
|
||||
* <a href="https://github.com/tiangolo/full-stack-flask-couchdb" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-flask-couchdb</a>
|
||||
|
||||
그리고 이 동일한 full-stack generator들이 [**FastAPI** Project Generators](project-generation.md){.internal-link target=_blank}의 기반이 되었습니다.
|
||||
|
||||
/// info | 정보
|
||||
|
||||
Flask-apispec은 Marshmallow와 같은 개발자들이 만들었습니다.
|
||||
|
||||
///
|
||||
|
||||
/// check | **FastAPI**에 영감을 준 것
|
||||
|
||||
serialization과 validation을 정의하는 동일한 코드로부터 OpenAPI schema를 자동 생성하기.
|
||||
|
||||
///
|
||||
|
||||
### <a href="https://nestjs.com/" class="external-link" target="_blank">NestJS</a> (그리고 <a href="https://angular.io/" class="external-link" target="_blank">Angular</a>) { #nestjs-and-angular }
|
||||
|
||||
이건 Python도 아닙니다. NestJS는 Angular에서 영감을 받은 JavaScript(TypeScript) NodeJS framework입니다.
|
||||
|
||||
Flask-apispec으로 할 수 있는 것과 어느 정도 비슷한 것을 달성합니다.
|
||||
|
||||
Angular 2에서 영감을 받은 의존성 주입 시스템이 통합되어 있습니다. 제가 아는 다른 의존성 주입 시스템들처럼 "injectable"을 사전에 등록해야 하므로, 장황함과 코드 반복이 늘어납니다.
|
||||
|
||||
파라미터가 TypeScript 타입(Python type hints와 유사함)으로 설명되기 때문에 에디터 지원은 꽤 좋습니다.
|
||||
|
||||
하지만 TypeScript 데이터는 JavaScript로 컴파일된 뒤에는 보존되지 않기 때문에, 타입에 의존해 검증, serialization, 문서화를 동시에 정의할 수 없습니다. 이 점과 일부 설계 결정 때문에, 검증/serialization/자동 schema 생성을 하려면 여러 곳에 decorator를 추가해야 하며, 결과적으로 매우 장황해집니다.
|
||||
|
||||
중첩 모델을 잘 처리하지 못합니다. 즉, 요청의 JSON body가 내부 필드를 가진 JSON 객체이고 그 내부 필드들이 다시 중첩된 JSON 객체인 경우, 제대로 문서화하고 검증할 수 없습니다.
|
||||
|
||||
/// check | **FastAPI**에 영감을 준 것
|
||||
|
||||
Python 타입을 사용해 뛰어난 에디터 지원을 제공하기.
|
||||
|
||||
강력한 의존성 주입 시스템을 갖추기. 코드 반복을 최소화하는 방법을 찾기.
|
||||
|
||||
///
|
||||
|
||||
### <a href="https://sanic.readthedocs.io/en/latest/" class="external-link" target="_blank">Sanic</a> { #sanic }
|
||||
|
||||
`asyncio` 기반의 매우 빠른 Python framework 중 초기 사례였습니다. Flask와 매우 유사하게 만들어졌습니다.
|
||||
|
||||
/// note | 기술 세부사항
|
||||
|
||||
기본 Python `asyncio` 루프 대신 <a href="https://github.com/MagicStack/uvloop" class="external-link" target="_blank">`uvloop`</a>를 사용했습니다. 이것이 매우 빠르게 만든 요인입니다.
|
||||
|
||||
이는 Uvicorn과 Starlette에 명확히 영감을 주었고, 현재 공개 benchmark에서는 이 둘이 Sanic보다 더 빠릅니다.
|
||||
|
||||
///
|
||||
|
||||
/// check | **FastAPI**에 영감을 준 것
|
||||
|
||||
미친 성능을 낼 수 있는 방법을 찾기.
|
||||
|
||||
그래서 **FastAPI**는 Starlette를 기반으로 합니다. Starlette는 사용 가능한 framework 중 가장 빠르기 때문입니다(서드파티 benchmark로 테스트됨).
|
||||
|
||||
///
|
||||
|
||||
### <a href="https://falconframework.org/" class="external-link" target="_blank">Falcon</a> { #falcon }
|
||||
|
||||
Falcon은 또 다른 고성능 Python framework로, 최소한으로 설계되었고 Hug 같은 다른 framework의 기반으로 동작하도록 만들어졌습니다.
|
||||
|
||||
함수가 두 개의 파라미터(하나는 "request", 하나는 "response")를 받도록 설계되어 있습니다. 그런 다음 request에서 일부를 "읽고", response에 일부를 "작성"합니다. 이 설계 때문에, 표준 Python type hints를 함수 파라미터로 사용해 요청 파라미터와 body를 선언하는 것이 불가능합니다.
|
||||
|
||||
따라서 데이터 검증, serialization, 문서화는 자동으로 되지 않고 코드로 해야 합니다. 또는 Hug처럼 Falcon 위에 framework를 얹어 구현해야 합니다. request 객체 하나와 response 객체 하나를 파라미터로 받는 Falcon의 설계에서 영감을 받은 다른 framework에서도 같은 구분이 나타납니다.
|
||||
|
||||
/// check | **FastAPI**에 영감을 준 것
|
||||
|
||||
훌륭한 성능을 얻는 방법을 찾기.
|
||||
|
||||
Hug(= Falcon 기반)과 함께, 함수에서 `response` 파라미터를 선언하도록 **FastAPI**에 영감을 주었습니다.
|
||||
|
||||
다만 FastAPI에서는 선택 사항이며, 주로 헤더, 쿠키, 그리고 대체 status code를 설정하는 데 사용됩니다.
|
||||
|
||||
///
|
||||
|
||||
### <a href="https://moltenframework.com/" class="external-link" target="_blank">Molten</a> { #molten }
|
||||
|
||||
**FastAPI**를 만들기 시작한 초기 단계에서 Molten을 알게 되었고, 꽤 비슷한 아이디어를 갖고 있었습니다:
|
||||
|
||||
* Python type hints 기반
|
||||
* 이 타입으로부터 검증과 문서화 생성
|
||||
* 의존성 주입 시스템
|
||||
|
||||
Pydantic 같은 서드파티 라이브러리를 사용해 데이터 검증/serialization/문서화를 하지 않고 자체 구현을 사용합니다. 그래서 이런 데이터 타입 정의를 쉽게 재사용하기는 어렵습니다.
|
||||
|
||||
조금 더 장황한 설정이 필요합니다. 또한 WSGI(ASGI가 아니라) 기반이므로, Uvicorn, Starlette, Sanic 같은 도구가 제공하는 고성능을 활용하도록 설계되지 않았습니다.
|
||||
|
||||
의존성 주입 시스템은 의존성을 사전에 등록해야 하고, 선언된 타입을 기반으로 의존성을 해결합니다. 따라서 특정 타입을 제공하는 "component"를 두 개 이상 선언할 수 없습니다.
|
||||
|
||||
Route는 한 곳에서 선언하고, 다른 곳에 선언된 함수를 사용합니다(엔드포인트를 처리하는 함수 바로 위에 둘 수 있는 decorator를 사용하는 대신). 이는 Flask(및 Starlette)보다는 Django 방식에 가깝습니다. 코드에서 상대적으로 강하게 결합된 것들을 분리해 놓습니다.
|
||||
|
||||
/// check | **FastAPI**에 영감을 준 것
|
||||
|
||||
모델 속성의 "default" 값으로 데이터 타입에 대한 추가 검증을 정의하기. 이는 에디터 지원을 개선하며, 이전에는 Pydantic에 없었습니다.
|
||||
|
||||
이것은 실제로 Pydantic의 일부를 업데이트하여 같은 검증 선언 스타일을 지원하도록 하는 데 영감을 주었습니다(이 기능은 이제 Pydantic에 이미 포함되어 있습니다).
|
||||
|
||||
///
|
||||
|
||||
### <a href="https://github.com/hugapi/hug" class="external-link" target="_blank">Hug</a> { #hug }
|
||||
|
||||
Hug는 Python type hints를 사용해 API 파라미터 타입을 선언하는 기능을 구현한 초기 framework 중 하나였습니다. 이는 다른 도구들도 같은 방식을 하도록 영감을 준 훌륭한 아이디어였습니다.
|
||||
|
||||
표준 Python 타입 대신 커스텀 타입을 선언에 사용했지만, 여전히 큰 진전이었습니다.
|
||||
|
||||
또한 전체 API를 JSON으로 선언하는 커스텀 schema를 생성한 초기 framework 중 하나였습니다.
|
||||
|
||||
OpenAPI나 JSON Schema 같은 표준을 기반으로 하지 않았기 때문에 Swagger UI 같은 다른 도구와 통합하는 것은 직관적이지 않았습니다. 하지만 역시 매우 혁신적인 아이디어였습니다.
|
||||
|
||||
흥미롭고 흔치 않은 기능이 하나 있습니다. 같은 framework로 API뿐 아니라 CLI도 만들 수 있습니다.
|
||||
|
||||
동기식 Python 웹 framework의 이전 표준(WSGI) 기반이어서 Websockets와 다른 것들을 처리할 수는 없지만, 성능은 여전히 높습니다.
|
||||
|
||||
/// info | 정보
|
||||
|
||||
Hug는 Timothy Crosley가 만들었습니다. Python 파일에서 import를 자동으로 정렬하는 훌륭한 도구인 <a href="https://github.com/timothycrosley/isort" class="external-link" target="_blank">`isort`</a>의 제작자이기도 합니다.
|
||||
|
||||
///
|
||||
|
||||
/// check | **FastAPI**에 영감을 준 아이디어들
|
||||
|
||||
Hug는 APIStar의 일부에 영감을 주었고, 저는 APIStar와 함께 Hug를 가장 유망한 도구 중 하나로 보았습니다.
|
||||
|
||||
Hug는 Python type hints로 파라미터를 선언하고, API를 정의하는 schema를 자동으로 생성하도록 **FastAPI**에 영감을 주었습니다.
|
||||
|
||||
Hug는 헤더와 쿠키를 설정하기 위해 함수에 `response` 파라미터를 선언하도록 **FastAPI**에 영감을 주었습니다.
|
||||
|
||||
///
|
||||
|
||||
### <a href="https://github.com/encode/apistar" class="external-link" target="_blank">APIStar</a> (<= 0.5) { #apistar-0-5 }
|
||||
|
||||
**FastAPI**를 만들기로 결정하기 직전에 **APIStar** 서버를 발견했습니다. 찾고 있던 거의 모든 것을 갖추고 있었고 설계도 훌륭했습니다.
|
||||
|
||||
NestJS와 Molten보다 앞서, Python type hints를 사용해 파라미터와 요청을 선언하는 framework 구현을 제가 처음 본 사례들 중 하나였습니다. Hug와 거의 같은 시기에 발견했습니다. 하지만 APIStar는 OpenAPI 표준을 사용했습니다.
|
||||
|
||||
여러 위치에서 동일한 type hints를 기반으로 자동 데이터 검증, 데이터 serialization, OpenAPI schema 생성을 제공했습니다.
|
||||
|
||||
Body schema 정의는 Pydantic처럼 동일한 Python type hints를 사용하지는 않았고 Marshmallow와 조금 더 비슷해서 에디터 지원은 그만큼 좋지 않았지만, 그래도 APIStar는 당시 사용할 수 있는 최선의 선택지였습니다.
|
||||
|
||||
당시 최고의 성능 benchmark를 가졌습니다(Starlette에 의해서만 추월됨).
|
||||
|
||||
처음에는 자동 API 문서화 웹 UI가 없었지만, Swagger UI를 추가할 수 있다는 것을 알고 있었습니다.
|
||||
|
||||
의존성 주입 시스템도 있었습니다. 위에서 언급한 다른 도구들처럼 component의 사전 등록이 필요했지만, 여전히 훌륭한 기능이었습니다.
|
||||
|
||||
보안 통합이 없어서 전체 프로젝트에서 사용해 볼 수는 없었습니다. 그래서 Flask-apispec 기반 full-stack generator로 갖추고 있던 모든 기능을 대체할 수 없었습니다. 그 기능을 추가하는 pull request를 만드는 것이 제 백로그에 있었습니다.
|
||||
|
||||
하지만 이후 프로젝트의 초점이 바뀌었습니다.
|
||||
|
||||
더 이상 API web framework가 아니게 되었는데, 제작자가 Starlette에 집중해야 했기 때문입니다.
|
||||
|
||||
이제 APIStar는 web framework가 아니라 OpenAPI 사양을 검증하기 위한 도구 모음입니다.
|
||||
|
||||
/// info | 정보
|
||||
|
||||
APIStar는 Tom Christie가 만들었습니다. 다음을 만든 사람과 동일합니다:
|
||||
|
||||
* Django REST Framework
|
||||
* Starlette(**FastAPI**의 기반)
|
||||
* Uvicorn(Starlette와 **FastAPI**에서 사용)
|
||||
|
||||
///
|
||||
|
||||
/// check | **FastAPI**에 영감을 준 것
|
||||
|
||||
존재하게 만들기.
|
||||
|
||||
동일한 Python 타입으로 여러 가지(데이터 검증, serialization, 문서화)를 선언하면서 동시에 뛰어난 에디터 지원을 제공한다는 아이디어는 제가 매우 훌륭하다고 생각했습니다.
|
||||
|
||||
그리고 오랫동안 비슷한 framework를 찾아 여러 대안을 테스트한 끝에, APIStar가 그때 이용 가능한 최선의 선택지였습니다.
|
||||
|
||||
그 후 APIStar 서버가 더는 존재하지 않게 되고 Starlette가 만들어졌는데, 이는 그런 시스템을 위한 더 새롭고 더 나은 기반이었습니다. 이것이 **FastAPI**를 만들게 된 최종 영감이었습니다.
|
||||
|
||||
저는 **FastAPI**를 APIStar의 "정신적 후계자"로 생각합니다. 동시에, 이 모든 이전 도구들에서 배운 것들을 바탕으로 기능, typing 시스템, 그리고 다른 부분들을 개선하고 확장했습니다.
|
||||
|
||||
///
|
||||
|
||||
## **FastAPI**가 사용하는 것 { #used-by-fastapi }
|
||||
|
||||
### <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> { #pydantic }
|
||||
|
||||
Pydantic은 Python type hints를 기반으로 데이터 검증, serialization, 문서화(JSON Schema 사용)를 정의하는 라이브러리입니다.
|
||||
|
||||
그 덕분에 매우 직관적입니다.
|
||||
|
||||
Marshmallow와 비교할 수 있습니다. 다만 benchmark에서 Marshmallow보다 빠릅니다. 그리고 동일한 Python type hints를 기반으로 하므로 에디터 지원도 훌륭합니다.
|
||||
|
||||
/// check | **FastAPI**가 이를 사용하는 목적
|
||||
|
||||
모든 데이터 검증, 데이터 serialization, 자동 모델 문서화(JSON Schema 기반)를 처리하기.
|
||||
|
||||
그 다음 **FastAPI**는 그 JSON Schema 데이터를 가져와 OpenAPI에 포함시키며, 그 외에도 여러 작업을 수행합니다.
|
||||
|
||||
///
|
||||
|
||||
### <a href="https://www.starlette.dev/" class="external-link" target="_blank">Starlette</a> { #starlette }
|
||||
|
||||
Starlette는 경량 <abbr title="The new standard for building asynchronous Python web applications - 비동기 Python 웹 애플리케이션을 구축하기 위한 새로운 표준">ASGI</abbr> framework/toolkit으로, 고성능 asyncio 서비스를 만들기에 이상적입니다.
|
||||
|
||||
매우 단순하고 직관적입니다. 쉽게 확장할 수 있도록 설계되었고, 모듈식 component를 갖습니다.
|
||||
|
||||
다음이 포함됩니다:
|
||||
|
||||
* 정말 인상적인 성능.
|
||||
* WebSocket 지원.
|
||||
* 프로세스 내 백그라운드 작업.
|
||||
* 시작 및 종료 이벤트.
|
||||
* HTTPX 기반의 테스트 클라이언트.
|
||||
* CORS, GZip, Static Files, Streaming responses.
|
||||
* 세션 및 쿠키 지원.
|
||||
* 100% 테스트 커버리지.
|
||||
* 100% 타입 주석이 달린 코드베이스.
|
||||
* 소수의 필수 의존성.
|
||||
|
||||
Starlette는 현재 테스트된 Python framework 중 가장 빠릅니다. 단, framework가 아니라 서버인 Uvicorn이 더 빠릅니다.
|
||||
|
||||
Starlette는 웹 microframework의 기본 기능을 모두 제공합니다.
|
||||
|
||||
하지만 자동 데이터 검증, serialization, 문서화는 제공하지 않습니다.
|
||||
|
||||
그것이 **FastAPI**가 위에 추가하는 핵심 중 하나이며, 모두 Python type hints(Pydantic 사용)를 기반으로 합니다. 여기에 더해 의존성 주입 시스템, 보안 유틸리티, OpenAPI schema 생성 등도 포함됩니다.
|
||||
|
||||
/// note | 기술 세부사항
|
||||
|
||||
ASGI는 Django 코어 팀 멤버들이 개발 중인 새로운 "표준"입니다. 아직 "Python 표준"(PEP)은 아니지만, 그 방향으로 진행 중입니다.
|
||||
|
||||
그럼에도 이미 여러 도구에서 "표준"으로 사용되고 있습니다. 이는 상호운용성을 크게 개선합니다. 예를 들어 Uvicorn을 다른 ASGI 서버(예: Daphne 또는 Hypercorn)로 교체할 수도 있고, `python-socketio` 같은 ASGI 호환 도구를 추가할 수도 있습니다.
|
||||
|
||||
///
|
||||
|
||||
/// check | **FastAPI**가 이를 사용하는 목적
|
||||
|
||||
핵심 웹 부분을 모두 처리하기. 그 위에 기능을 추가하기.
|
||||
|
||||
`FastAPI` 클래스 자체는 `Starlette` 클래스를 직접 상속합니다.
|
||||
|
||||
따라서 Starlette로 할 수 있는 모든 것은 기본적으로 **FastAPI**로도 직접 할 수 있습니다. 즉, **FastAPI**는 사실상 Starlette에 강력한 기능을 더한 것입니다.
|
||||
|
||||
///
|
||||
|
||||
### <a href="https://www.uvicorn.dev/" class="external-link" target="_blank">Uvicorn</a> { #uvicorn }
|
||||
|
||||
Uvicorn은 uvloop과 httptools로 구축된 초고속 ASGI 서버입니다.
|
||||
|
||||
web framework가 아니라 서버입니다. 예를 들어 경로 기반 routing을 위한 도구는 제공하지 않습니다. 그런 것은 Starlette(또는 **FastAPI**) 같은 framework가 위에서 제공합니다.
|
||||
|
||||
Starlette와 **FastAPI**에서 권장하는 서버입니다.
|
||||
|
||||
/// check | **FastAPI**가 이를 권장하는 방식
|
||||
|
||||
**FastAPI** 애플리케이션을 실행하기 위한 주요 웹 서버.
|
||||
|
||||
또한 `--workers` 커맨드라인 옵션을 사용하면 비동기 멀티프로세스 서버로 실행할 수도 있습니다.
|
||||
|
||||
자세한 내용은 [배포](deployment/index.md){.internal-link target=_blank} 섹션을 확인하세요.
|
||||
|
||||
///
|
||||
|
||||
## 벤치마크와 속도 { #benchmarks-and-speed }
|
||||
|
||||
Uvicorn, Starlette, FastAPI 사이의 차이를 이해하고 비교하려면 [벤치마크](benchmarks.md){.internal-link target=_blank} 섹션을 확인하세요.
|
||||
@@ -1,321 +0,0 @@
|
||||
# 배포 개념 { #deployments-concepts }
|
||||
|
||||
**FastAPI** 애플리케이션(사실 어떤 종류의 웹 API든)을 배포할 때는, 여러분이 신경 써야 할 여러 개념이 있습니다. 그리고 이 개념들을 활용하면 **애플리케이션을 배포하기 위한 가장 적절한 방법**을 찾을 수 있습니다.
|
||||
|
||||
중요한 개념 몇 가지는 다음과 같습니다:
|
||||
|
||||
* 보안 - HTTPS
|
||||
* 시작 시 실행
|
||||
* 재시작
|
||||
* 복제(실행 중인 프로세스 수)
|
||||
* 메모리
|
||||
* 시작 전 사전 단계
|
||||
|
||||
이것들이 **배포**에 어떤 영향을 주는지 살펴보겠습니다.
|
||||
|
||||
결국 최종 목표는 **API 클라이언트에 서비스를 제공**할 때 **보안**을 보장하고, **중단을 피하며**, **컴퓨팅 리소스**(예: 원격 서버/가상 머신)를 가능한 한 효율적으로 사용하는 것입니다. 🚀
|
||||
|
||||
여기서 이 **개념들**을 조금 더 설명하겠습니다. 그러면 서로 매우 다른 환경, 심지어 아직 존재하지 않는 **미래**의 환경에서도 API를 어떻게 배포할지 결정하는 데 필요한 **직관**을 얻을 수 있을 것입니다.
|
||||
|
||||
이 개념들을 고려하면, 여러분은 **자신의 API**를 배포하기 위한 최선의 방법을 **평가하고 설계**할 수 있습니다.
|
||||
|
||||
다음 장들에서는 FastAPI 애플리케이션을 배포하기 위한 더 **구체적인 레시피**를 제공하겠습니다.
|
||||
|
||||
하지만 지금은, 이 중요한 **개념적 아이디어**들을 확인해 봅시다. 이 개념들은 다른 어떤 종류의 웹 API에도 동일하게 적용됩니다. 💡
|
||||
|
||||
## 보안 - HTTPS { #security-https }
|
||||
|
||||
[이전 HTTPS 장](https.md){.internal-link target=_blank}에서 HTTPS가 API에 암호화를 제공하는 방식에 대해 배웠습니다.
|
||||
|
||||
또한 HTTPS는 일반적으로 애플리케이션 서버 바깥의 **외부** 컴포넌트인 **TLS Termination Proxy**가 제공한다는 것도 확인했습니다.
|
||||
|
||||
그리고 **HTTPS 인증서 갱신**을 담당하는 무언가가 필요합니다. 같은 컴포넌트가 그 역할을 할 수도 있고, 다른 무언가가 담당할 수도 있습니다.
|
||||
|
||||
### HTTPS를 위한 도구 예시 { #example-tools-for-https }
|
||||
|
||||
TLS Termination Proxy로 사용할 수 있는 도구는 예를 들어 다음과 같습니다:
|
||||
|
||||
* Traefik
|
||||
* 인증서 갱신을 자동으로 처리 ✨
|
||||
* Caddy
|
||||
* 인증서 갱신을 자동으로 처리 ✨
|
||||
* Nginx
|
||||
* 인증서 갱신을 위해 Certbot 같은 외부 컴포넌트 사용
|
||||
* HAProxy
|
||||
* 인증서 갱신을 위해 Certbot 같은 외부 컴포넌트 사용
|
||||
* Nginx 같은 Ingress Controller를 사용하는 Kubernetes
|
||||
* 인증서 갱신을 위해 cert-manager 같은 외부 컴포넌트 사용
|
||||
* 클라우드 제공자가 서비스 일부로 내부적으로 처리(아래를 읽어보세요 👇)
|
||||
|
||||
또 다른 선택지는 HTTPS 설정을 포함해 더 많은 일을 대신해주는 **클라우드 서비스**를 사용하는 것입니다. 제약이 있거나 비용이 더 들 수도 있습니다. 하지만 그 경우에는 TLS Termination Proxy를 직접 설정할 필요가 없습니다.
|
||||
|
||||
다음 장에서 구체적인 예시를 보여드리겠습니다.
|
||||
|
||||
---
|
||||
|
||||
다음으로 고려할 개념들은 실제로 여러분의 API를 실행하는 프로그램(예: Uvicorn)과 관련된 내용입니다.
|
||||
|
||||
## 프로그램과 프로세스 { #program-and-process }
|
||||
|
||||
실행 중인 "**프로세스**"에 대해 많이 이야기하게 될 텐데, 이 말이 무엇을 의미하는지, 그리고 "**프로그램**"이라는 단어와 무엇이 다른지 명확히 해두는 것이 유용합니다.
|
||||
|
||||
### 프로그램이란 { #what-is-a-program }
|
||||
|
||||
**프로그램**이라는 단어는 보통 여러 가지를 가리키는 데 사용됩니다:
|
||||
|
||||
* 여러분이 작성하는 **코드**, 즉 **Python 파일**들
|
||||
* 운영체제에서 **실행**할 수 있는 **파일**, 예: `python`, `python.exe`, `uvicorn`
|
||||
* 운영체제에서 **실행 중**인 특정 프로그램으로, CPU를 사용하고 메모리에 내용을 저장합니다. 이것을 **프로세스**라고도 합니다.
|
||||
|
||||
### 프로세스란 { #what-is-a-process }
|
||||
|
||||
**프로세스**라는 단어는 보통 더 구체적으로, 운영체제에서 실행 중인 것(위 마지막 항목처럼)만을 가리키는 데 사용됩니다:
|
||||
|
||||
* 운영체제에서 **실행 중**인 특정 프로그램
|
||||
* 파일이나 코드를 의미하는 것이 아니라, 운영체제가 **실제로 실행**하고 관리하는 대상을 **구체적으로** 의미합니다.
|
||||
* 어떤 프로그램이든 어떤 코드든, **실행**될 때만 무언가를 **할 수 있습니다**. 즉, **프로세스가 실행 중**일 때입니다.
|
||||
* 프로세스는 여러분이, 혹은 운영체제가 **종료**(또는 “kill”)할 수 있습니다. 그러면 실행이 멈추고, 더 이상 **아무것도 할 수 없습니다**.
|
||||
* 컴퓨터에서 실행 중인 각 애플리케이션 뒤에는 프로세스가 있습니다. 실행 중인 프로그램, 각 창 등도 마찬가지입니다. 그리고 컴퓨터가 켜져 있는 동안 보통 많은 프로세스가 **동시에** 실행됩니다.
|
||||
* **같은 프로그램**의 **여러 프로세스**가 동시에 실행될 수도 있습니다.
|
||||
|
||||
운영체제의 “작업 관리자(task manager)”나 “시스템 모니터(system monitor)”(또는 비슷한 도구)를 확인해 보면, 이런 프로세스가 많이 실행 중인 것을 볼 수 있습니다.
|
||||
|
||||
또 예를 들어, 같은 브라우저 프로그램(Firefox, Chrome, Edge 등)을 실행하는 프로세스가 여러 개 있는 것도 보일 가능성이 큽니다. 보통 탭마다 하나의 프로세스를 실행하고, 그 외에도 추가 프로세스 몇 개가 더 있습니다.
|
||||
|
||||
<img class="shadow" src="/img/deployment/concepts/image01.png">
|
||||
|
||||
---
|
||||
|
||||
이제 **프로세스**와 **프로그램**의 차이를 알았으니, 배포에 대한 이야기를 계속해 보겠습니다.
|
||||
|
||||
## 시작 시 실행 { #running-on-startup }
|
||||
|
||||
대부분의 경우 웹 API를 만들면, 클라이언트가 언제나 접근할 수 있도록 **항상 실행**되고 중단되지 않기를 원합니다. 물론 특정 상황에서만 실행하고 싶은 특별한 이유가 있을 수는 있지만, 대부분은 지속적으로 실행되며 **사용 가능**한 상태이기를 원합니다.
|
||||
|
||||
### 원격 서버에서 { #in-a-remote-server }
|
||||
|
||||
원격 서버(클라우드 서버, 가상 머신 등)를 설정할 때, 가장 단순한 방법은 로컬 개발 때처럼 수동으로 `fastapi run`(Uvicorn을 사용합니다)이나 비슷한 명령을 실행하는 것입니다.
|
||||
|
||||
이 방식은 동작하고, **개발 중에는** 유용합니다.
|
||||
|
||||
하지만 서버에 대한 연결이 끊기면, 실행 중인 **프로세스**도 아마 종료될 것입니다.
|
||||
|
||||
또 서버가 재시작되면(예: 업데이트 이후, 혹은 클라우드 제공자의 마이그레이션 이후) 여러분은 아마 **알아차리지 못할** 겁니다. 그 결과, 프로세스를 수동으로 다시 시작해야 한다는 사실도 모르게 됩니다. 그러면 API는 그냥 죽은 상태로 남습니다. 😱
|
||||
|
||||
### 시작 시 자동 실행 { #run-automatically-on-startup }
|
||||
|
||||
일반적으로 서버 프로그램(예: Uvicorn)은 서버가 시작될 때 자동으로 시작되고, **사람의 개입** 없이도 FastAPI 앱을 실행하는 프로세스가 항상 실행 중이도록(예: FastAPI 앱을 실행하는 Uvicorn) 구성하고 싶을 것입니다.
|
||||
|
||||
### 별도의 프로그램 { #separate-program }
|
||||
|
||||
이를 위해 보통 애플리케이션이 시작 시 실행되도록 보장하는 **별도의 프로그램**을 둡니다. 그리고 많은 경우, 데이터베이스 같은 다른 컴포넌트나 애플리케이션도 함께 실행되도록 보장합니다.
|
||||
|
||||
### 시작 시 실행을 위한 도구 예시 { #example-tools-to-run-at-startup }
|
||||
|
||||
이 역할을 할 수 있는 도구 예시는 다음과 같습니다:
|
||||
|
||||
* Docker
|
||||
* Kubernetes
|
||||
* Docker Compose
|
||||
* Swarm Mode의 Docker
|
||||
* Systemd
|
||||
* Supervisor
|
||||
* 클라우드 제공자가 서비스 일부로 내부적으로 처리
|
||||
* 기타...
|
||||
|
||||
다음 장에서 더 구체적인 예시를 제공하겠습니다.
|
||||
|
||||
## 재시작 { #restarts }
|
||||
|
||||
애플리케이션이 시작 시 실행되도록 보장하는 것과 비슷하게, 장애가 발생했을 때 **재시작**되도록 보장하고 싶을 것입니다.
|
||||
|
||||
### 우리는 실수합니다 { #we-make-mistakes }
|
||||
|
||||
사람은 언제나 **실수**합니다. 소프트웨어에는 거의 *항상* 여기저기에 숨은 **버그**가 있습니다. 🐛
|
||||
|
||||
그리고 개발자는 버그를 발견하고 새로운 기능을 구현하면서 코드를 계속 개선합니다(새로운 버그도 추가할 수 있겠죠 😅).
|
||||
|
||||
### 작은 오류는 자동으로 처리됨 { #small-errors-automatically-handled }
|
||||
|
||||
FastAPI로 웹 API를 만들 때 코드에 오류가 있으면, FastAPI는 보통 그 오류를 발생시킨 단일 요청 안에만 문제를 가둡니다. 🛡
|
||||
|
||||
클라이언트는 해당 요청에 대해 **500 Internal Server Error**를 받지만, 애플리케이션은 완전히 크래시하지 않고 다음 요청부터는 계속 동작합니다.
|
||||
|
||||
### 더 큰 오류 - 크래시 { #bigger-errors-crashes }
|
||||
|
||||
그럼에도 불구하고, 우리가 작성한 코드가 **전체 애플리케이션을 크래시**시켜 Uvicorn과 Python 자체가 종료되는 경우가 있을 수 있습니다. 💥
|
||||
|
||||
그래도 한 군데 오류 때문에 애플리케이션이 죽은 채로 남아 있기를 바라지는 않을 것입니다. 망가진 경로 처리를 제외한 나머지 *경로 처리*라도 **계속 실행**되기를 원할 가능성이 큽니다.
|
||||
|
||||
### 크래시 후 재시작 { #restart-after-crash }
|
||||
|
||||
하지만 실행 중인 **프로세스**가 크래시하는 정말 심각한 오류의 경우에는, 적어도 몇 번은 프로세스를 **재시작**하도록 담당하는 외부 컴포넌트가 필요합니다...
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
...다만 애플리케이션 전체가 **즉시 계속 크래시**한다면, 무한히 재시작하는 것은 아마 의미가 없을 것입니다. 그런 경우에는 개발 중에, 또는 최소한 배포 직후에 알아차릴 가능성이 큽니다.
|
||||
|
||||
그러니 여기서는, 특정한 경우에만 전체가 크래시할 수 있고 **미래**에도 그럴 수 있으며, 그래도 재시작하는 것이 의미 있는 주요 사례에 집중해 봅시다.
|
||||
|
||||
///
|
||||
|
||||
애플리케이션을 재시작하는 역할은 **외부 컴포넌트**가 맡는 편이 보통 좋습니다. 그 시점에는 Uvicorn과 Python을 포함한 애플리케이션이 이미 크래시했기 때문에, 같은 앱의 같은 코드 안에서 이를 해결할 방법이 없기 때문입니다.
|
||||
|
||||
### 자동 재시작을 위한 도구 예시 { #example-tools-to-restart-automatically }
|
||||
|
||||
대부분의 경우 **시작 시 실행**에 사용한 도구가 자동 **재시작**도 함께 처리합니다.
|
||||
|
||||
예를 들어 다음이 가능합니다:
|
||||
|
||||
* Docker
|
||||
* Kubernetes
|
||||
* Docker Compose
|
||||
* Swarm Mode의 Docker
|
||||
* Systemd
|
||||
* Supervisor
|
||||
* 클라우드 제공자가 서비스 일부로 내부적으로 처리
|
||||
* 기타...
|
||||
|
||||
## 복제 - 프로세스와 메모리 { #replication-processes-and-memory }
|
||||
|
||||
FastAPI 애플리케이션은 Uvicorn을 실행하는 `fastapi` 명령 같은 서버 프로그램을 사용하면, **하나의 프로세스**로 실행하더라도 여러 클라이언트를 동시에 처리할 수 있습니다.
|
||||
|
||||
하지만 많은 경우, 여러 워커 프로세스를 동시에 실행하고 싶을 것입니다.
|
||||
|
||||
### 여러 프로세스 - 워커 { #multiple-processes-workers }
|
||||
|
||||
단일 프로세스가 처리할 수 있는 것보다 클라이언트가 더 많고(예: 가상 머신이 그리 크지 않을 때), 서버 CPU에 **여러 코어**가 있다면, 같은 애플리케이션을 실행하는 **여러 프로세스**를 동시에 띄우고 요청을 분산시킬 수 있습니다.
|
||||
|
||||
같은 API 프로그램을 **여러 프로세스**로 실행할 때, 이 프로세스들을 보통 **workers**라고 부릅니다.
|
||||
|
||||
### 워커 프로세스와 포트 { #worker-processes-and-ports }
|
||||
|
||||
[HTTPS에 대한 문서](https.md){.internal-link target=_blank}에서, 서버에서 하나의 포트와 IP 주소 조합에는 하나의 프로세스만 리스닝할 수 있다는 것을 기억하시나요?
|
||||
|
||||
이것은 여전히 사실입니다.
|
||||
|
||||
따라서 **여러 프로세스**를 동시에 실행하려면, 먼저 **포트에서 리스닝하는 단일 프로세스**가 있어야 하고, 그 프로세스가 어떤 방식으로든 각 워커 프로세스로 통신을 전달해야 합니다.
|
||||
|
||||
### 프로세스당 메모리 { #memory-per-process }
|
||||
|
||||
이제 프로그램이 메모리에 무언가를 로드한다고 해봅시다. 예를 들어 머신러닝 모델을 변수에 올리거나 큰 파일 내용을 변수에 올리는 경우입니다. 이런 것들은 서버의 **메모리(RAM)**를 어느 정도 사용합니다.
|
||||
|
||||
그리고 여러 프로세스는 보통 **메모리를 공유하지 않습니다**. 즉, 각 실행 중인 프로세스는 자체 변수와 메모리를 갖습니다. 코드에서 메모리를 많이 사용한다면, **각 프로세스**가 그만큼의 메모리를 사용하게 됩니다.
|
||||
|
||||
### 서버 메모리 { #server-memory }
|
||||
|
||||
예를 들어 코드가 크기 **1 GB**의 머신러닝 모델을 로드한다고 해봅시다. API를 프로세스 하나로 실행하면 RAM을 최소 1GB 사용합니다. 그리고 **4개 프로세스**(워커 4개)를 시작하면 각각 1GB RAM을 사용합니다. 즉 총 **4 GB RAM**을 사용합니다.
|
||||
|
||||
그런데 원격 서버나 가상 머신의 RAM이 3GB뿐이라면, 4GB를 넘게 로드하려고 할 때 문제가 생깁니다. 🚨
|
||||
|
||||
### 여러 프로세스 - 예시 { #multiple-processes-an-example }
|
||||
|
||||
이 예시에서는 **Manager Process**가 두 개의 **Worker Processes**를 시작하고 제어합니다.
|
||||
|
||||
이 Manager Process는 아마 IP의 **포트**에서 리스닝하는 역할을 합니다. 그리고 모든 통신을 워커 프로세스로 전달합니다.
|
||||
|
||||
워커 프로세스들이 실제로 애플리케이션을 실행하며, **요청**을 받아 **응답**을 반환하는 주요 연산을 수행하고, RAM에 변수로 로드한 모든 내용을 담습니다.
|
||||
|
||||
<img src="/img/deployment/concepts/process-ram.drawio.svg">
|
||||
|
||||
그리고 물론 같은 머신에는 애플리케이션 외에도 **다른 프로세스**들이 실행 중일 가능성이 큽니다.
|
||||
|
||||
흥미로운 점은 각 프로세스의 **CPU 사용률**은 시간에 따라 크게 **변동**할 수 있지만, **메모리(RAM)**는 보통 대체로 **안정적**으로 유지된다는 것입니다.
|
||||
|
||||
매번 비슷한 양의 연산을 수행하는 API이고 클라이언트가 많다면, **CPU 사용률**도 (급격히 오르내리기보다는) *안정적일* 가능성이 큽니다.
|
||||
|
||||
### 복제 도구와 전략 예시 { #examples-of-replication-tools-and-strategies }
|
||||
|
||||
이를 달성하는 접근 방식은 여러 가지가 있을 수 있으며, 다음 장들에서 Docker와 컨테이너를 설명할 때 구체적인 전략을 더 알려드리겠습니다.
|
||||
|
||||
고려해야 할 주요 제약은 **공개 IP**의 **포트**를 처리하는 **단일** 컴포넌트가 있어야 한다는 점입니다. 그리고 그 컴포넌트는 복제된 **프로세스/워커**로 통신을 **전달**할 방법이 있어야 합니다.
|
||||
|
||||
가능한 조합과 전략 몇 가지는 다음과 같습니다:
|
||||
|
||||
* `--workers` 옵션을 사용한 **Uvicorn**
|
||||
* 하나의 Uvicorn **프로세스 매니저**가 **IP**와 **포트**에서 리스닝하고, **여러 Uvicorn 워커 프로세스**를 시작합니다.
|
||||
* **Kubernetes** 및 기타 분산 **컨테이너 시스템**
|
||||
* **Kubernetes** 레이어의 무언가가 **IP**와 **포트**에서 리스닝합니다. 그리고 **여러 컨테이너**를 두어 복제하며, 각 컨테이너에는 **하나의 Uvicorn 프로세스**가 실행됩니다.
|
||||
* 이를 대신 처리해주는 **클라우드 서비스**
|
||||
* 클라우드 서비스가 **복제를 대신 처리**해줄 가능성이 큽니다. 실행할 **프로세스**나 사용할 **컨테이너 이미지**를 정의하게 해줄 수도 있지만, 어떤 경우든 대개 **단일 Uvicorn 프로세스**를 기준으로 하고, 클라우드 서비스가 이를 복제하는 역할을 맡습니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
**컨테이너**, Docker, Kubernetes에 대한 일부 내용이 아직은 잘 이해되지 않아도 괜찮습니다.
|
||||
|
||||
다음 장에서 컨테이너 이미지, Docker, Kubernetes 등을 더 설명하겠습니다: [컨테이너에서 FastAPI - Docker](docker.md){.internal-link target=_blank}.
|
||||
|
||||
///
|
||||
|
||||
## 시작 전 사전 단계 { #previous-steps-before-starting }
|
||||
|
||||
애플리케이션을 **시작하기 전에** 어떤 단계를 수행하고 싶은 경우가 많습니다.
|
||||
|
||||
예를 들어 **데이터베이스 마이그레이션**을 실행하고 싶을 수 있습니다.
|
||||
|
||||
하지만 대부분의 경우, 이런 단계는 **한 번만** 수행하고 싶을 것입니다.
|
||||
|
||||
그래서 애플리케이션을 시작하기 전에 그 **사전 단계**를 수행할 **단일 프로세스**를 두고 싶을 것입니다.
|
||||
|
||||
또한 이후에 애플리케이션 자체를 **여러 프로세스**(여러 워커)로 시작하더라도, 사전 단계를 수행하는 프로세스는 *반드시* 하나만 실행되도록 해야 합니다. 만약 사전 단계를 **여러 프로세스**가 수행하면, **병렬로** 실행하면서 작업이 **중복**될 수 있습니다. 그리고 데이터베이스 마이그레이션처럼 민감한 작업이라면 서로 충돌을 일으킬 수 있습니다.
|
||||
|
||||
물론 사전 단계를 여러 번 실행해도 문제가 없는 경우도 있습니다. 그런 경우에는 처리하기가 훨씬 쉽습니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
또한 설정에 따라, 어떤 경우에는 애플리케이션을 시작하기 전에 **사전 단계가 전혀 필요 없을** 수도 있다는 점을 기억하세요.
|
||||
|
||||
그런 경우에는 이런 것들을 전혀 걱정할 필요가 없습니다. 🤷
|
||||
|
||||
///
|
||||
|
||||
### 사전 단계 전략 예시 { #examples-of-previous-steps-strategies }
|
||||
|
||||
이는 여러분이 **시스템을 배포하는 방식**에 크게 좌우되며, 프로그램을 시작하는 방식, 재시작 처리 방식 등과도 연결되어 있을 가능성이 큽니다.
|
||||
|
||||
가능한 아이디어는 다음과 같습니다:
|
||||
|
||||
* 앱 컨테이너보다 먼저 실행되는 Kubernetes의 “Init Container”
|
||||
* 사전 단계를 실행한 다음 애플리케이션을 시작하는 bash 스크립트
|
||||
* 이 bash 스크립트를 시작/재시작하고, 오류를 감지하는 등의 방법도 여전히 필요합니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
컨테이너로 이를 처리하는 더 구체적인 예시는 다음 장에서 제공하겠습니다: [컨테이너에서 FastAPI - Docker](docker.md){.internal-link target=_blank}.
|
||||
|
||||
///
|
||||
|
||||
## 리소스 활용 { #resource-utilization }
|
||||
|
||||
서버는 여러분이 프로그램으로 소비하거나 **활용(utilize)**할 수 있는 **리소스**입니다. CPU의 계산 시간과 사용 가능한 RAM 메모리가 대표적입니다.
|
||||
|
||||
시스템 리소스를 얼마나 소비/활용하고 싶으신가요? “많지 않게”라고 생각하기 쉽지만, 실제로는 **크래시하지 않는 선에서 가능한 한 많이** 사용하고 싶을 가능성이 큽니다.
|
||||
|
||||
서버 3대를 비용을 내고 쓰고 있는데 RAM과 CPU를 조금만 사용한다면, 아마 **돈을 낭비**하고 💸, **서버 전력도 낭비**하고 🌎, 기타 등등이 될 수 있습니다.
|
||||
|
||||
그 경우에는 서버를 2대만 두고, 각 서버의 리소스(CPU, 메모리, 디스크, 네트워크 대역폭 등)를 더 높은 비율로 사용하는 것이 더 나을 수 있습니다.
|
||||
|
||||
반대로 서버 2대를 두고 CPU와 RAM을 **100%** 사용하고 있다면, 어느 시점에 프로세스 하나가 더 많은 메모리를 요청하게 되고, 서버는 디스크를 “메모리”처럼 사용해야 할 수도 있습니다(수천 배 느릴 수 있습니다). 또는 심지어 **크래시**할 수도 있습니다. 혹은 어떤 프로세스가 계산을 해야 하는데 CPU가 다시 비워질 때까지 기다려야 할 수도 있습니다.
|
||||
|
||||
이 경우에는 **서버 한 대를 추가**로 확보하고 일부 프로세스를 그쪽에서 실행해, 모두가 **충분한 RAM과 CPU 시간**을 갖도록 하는 편이 더 낫습니다.
|
||||
|
||||
또 어떤 이유로 API 사용량이 **급증(spike)**할 가능성도 있습니다. 바이럴이 되었거나, 다른 서비스나 봇이 사용하기 시작했을 수도 있습니다. 그런 경우를 대비해 추가 리소스를 확보해두고 싶을 수 있습니다.
|
||||
|
||||
리소스 활용률 목표로 **임의의 수치**를 정할 수 있습니다. 예를 들어 **50%에서 90% 사이**처럼요. 요점은, 이런 것들이 배포를 조정할 때 측정하고 튜닝하는 주요 지표가 될 가능성이 크다는 것입니다.
|
||||
|
||||
`htop` 같은 간단한 도구로 서버의 CPU와 RAM 사용량, 또는 각 프로세스별 사용량을 볼 수 있습니다. 혹은 서버 여러 대에 분산될 수도 있는 더 복잡한 모니터링 도구를 사용할 수도 있습니다.
|
||||
|
||||
## 요약 { #recap }
|
||||
|
||||
여기까지 애플리케이션 배포 방식을 결정할 때 염두에 두어야 할 주요 개념들을 읽었습니다:
|
||||
|
||||
* 보안 - HTTPS
|
||||
* 시작 시 실행
|
||||
* 재시작
|
||||
* 복제(실행 중인 프로세스 수)
|
||||
* 메모리
|
||||
* 시작 전 사전 단계
|
||||
|
||||
이 아이디어들을 이해하고 적용하는 방법을 알면, 배포를 구성하고 조정할 때 필요한 직관을 얻는 데 도움이 될 것입니다. 🤓
|
||||
|
||||
다음 섹션에서는 따라 할 수 있는 가능한 전략의 더 구체적인 예시를 제공하겠습니다. 🚀
|
||||
@@ -1,65 +0,0 @@
|
||||
# FastAPI Cloud { #fastapi-cloud }
|
||||
|
||||
**한 번의 명령**으로 FastAPI 앱을 <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>에 배포할 수 있습니다. 아직이라면 대기자 명단에 등록해 보세요. 🚀
|
||||
|
||||
## 로그인하기 { #login }
|
||||
|
||||
먼저 **FastAPI Cloud** 계정이 이미 있는지 확인하세요(대기자 명단에서 초대해 드렸을 거예요 😉).
|
||||
|
||||
그다음 로그인합니다:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi login
|
||||
|
||||
You are logged in to FastAPI Cloud 🚀
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## 배포하기 { #deploy }
|
||||
|
||||
이제 **한 번의 명령**으로 앱을 배포합니다:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi deploy
|
||||
|
||||
Deploying to FastAPI Cloud...
|
||||
|
||||
✅ Deployment successful!
|
||||
|
||||
🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
이게 전부입니다! 이제 해당 URL에서 앱에 접근할 수 있습니다. ✨
|
||||
|
||||
## FastAPI Cloud 소개 { #about-fastapi-cloud }
|
||||
|
||||
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>**는 **FastAPI**를 만든 동일한 저자와 팀이 구축했습니다.
|
||||
|
||||
최소한의 노력으로 API를 **구축**, **배포**, **접근**하는 과정을 간소화합니다.
|
||||
|
||||
FastAPI로 앱을 만들 때의 동일한 **개발자 경험**을, 클라우드에 **배포**할 때도 제공합니다. 🎉
|
||||
|
||||
또한 앱을 배포할 때 보통 필요한 대부분의 것들도 처리해 줍니다. 예를 들면:
|
||||
|
||||
* HTTPS
|
||||
* 요청을 기반으로 자동 스케일링하는 복제(Replication)
|
||||
* 등
|
||||
|
||||
FastAPI Cloud는 *FastAPI and friends* 오픈 소스 프로젝트의 주요 스폰서이자 자금 지원 제공자입니다. ✨
|
||||
|
||||
## 다른 클라우드 제공업체에 배포하기 { #deploy-to-other-cloud-providers }
|
||||
|
||||
FastAPI는 오픈 소스이며 표준을 기반으로 합니다. 원하는 어떤 클라우드 제공업체에도 FastAPI 앱을 배포할 수 있습니다.
|
||||
|
||||
해당 클라우드 제공업체의 가이드를 따라 FastAPI 앱을 배포하세요. 🤓
|
||||
|
||||
## 자체 서버에 배포하기 { #deploy-your-own-server }
|
||||
|
||||
또한 이 **Deployment** 가이드에서 이후에 모든 세부사항을 알려드릴 거예요. 그래서 무슨 일이 일어나고 있는지, 무엇이 필요하며, 본인의 서버를 포함해 직접 FastAPI 앱을 어떻게 배포하는지까지 이해할 수 있게 될 것입니다. 🤓
|
||||
@@ -1,231 +0,0 @@
|
||||
# HTTPS 알아보기 { #about-https }
|
||||
|
||||
HTTPS는 그냥 “켜져 있거나” 아니면 “꺼져 있는” 것이라고 생각하기 쉽습니다.
|
||||
|
||||
하지만 실제로는 훨씬 더 복잡합니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
바쁘거나 별로 신경 쓰고 싶지 않다면, 다음 섹션에서 다양한 기법으로 모든 것을 설정하는 단계별 안내를 계속 보세요.
|
||||
|
||||
///
|
||||
|
||||
소비자 관점에서 **HTTPS의 기본을 배우려면** <a href="https://howhttps.works/" class="external-link" target="_blank">https://howhttps.works/</a>를 확인하세요.
|
||||
|
||||
이제 **개발자 관점**에서 HTTPS를 생각할 때 염두에 두어야 할 여러 가지가 있습니다:
|
||||
|
||||
* HTTPS를 사용하려면, **서버**가 **제3자**가 발급한 **"인증서(certificates)"**를 **보유**해야 합니다.
|
||||
* 이 인증서는 실제로 제3자가 “생성”해 주는 것이고, 서버가 만드는 것이 아니라 제3자로부터 **발급/획득**하는 것입니다.
|
||||
* 인증서에는 **유효 기간**이 있습니다.
|
||||
* 즉, **만료**됩니다.
|
||||
* 그리고 나면 제3자로부터 다시 **갱신**해서 **재발급/재획득**해야 합니다.
|
||||
* 연결의 암호화는 **TCP 레벨**에서 일어납니다.
|
||||
* 이는 **HTTP보다 한 계층 아래**입니다.
|
||||
* 따라서 **인증서와 암호화** 처리는 **HTTP 이전**에 수행됩니다.
|
||||
* **TCP는 "도메인"을 모릅니다.** IP 주소만 압니다.
|
||||
* 어떤 **특정 도메인**을 요청했는지에 대한 정보는 **HTTP 데이터**에 들어 있습니다.
|
||||
* **HTTPS 인증서**는 특정 **도메인**을 “인증”하지만, 프로토콜과 암호화는 TCP 레벨에서 일어나며, 어떤 도메인을 다루는지 **알기 전에** 처리됩니다.
|
||||
* **기본적으로** 이는 IP 주소 하나당 **HTTPS 인증서 하나만** 둘 수 있다는 뜻입니다.
|
||||
* 서버가 아무리 크든, 그 위에 올린 각 애플리케이션이 아무리 작든 상관없습니다.
|
||||
* 하지만 이에 대한 **해결책**이 있습니다.
|
||||
* **TLS** 프로토콜(HTTP 이전, TCP 레벨에서 암호화를 처리하는 것)에 대한 **확장** 중에 **<a href="https://en.wikipedia.org/wiki/Server_Name_Indication" class="external-link" target="_blank"><abbr title="Server Name Indication - 서버 이름 표시">SNI</abbr></a>**라는 것이 있습니다.
|
||||
* 이 SNI 확장을 사용하면, 단일 서버(**단일 IP 주소**)에서 **여러 HTTPS 인증서**를 사용하고 **여러 HTTPS 도메인/애플리케이션**을 제공할 수 있습니다.
|
||||
* 이를 위해서는 서버에서 **공개 IP 주소**로 리스닝하는 **하나의** 컴포넌트(프로그램)가 서버에 있는 **모든 HTTPS 인증서**에 접근할 수 있어야 합니다.
|
||||
* 보안 연결을 얻은 **이후에도**, 통신 프로토콜 자체는 **여전히 HTTP**입니다.
|
||||
* **HTTP 프로토콜**로 전송되더라도, 내용은 **암호화**되어 있습니다.
|
||||
|
||||
일반적으로 서버(머신, 호스트 등)에는 **프로그램/HTTP 서버 하나**를 실행해 **HTTPS 관련 부분 전체**를 관리하게 합니다: **암호화된 HTTPS 요청**을 받고, 복호화된 **HTTP 요청**을 같은 서버에서 실행 중인 실제 HTTP 애플리케이션(이 경우 **FastAPI** 애플리케이션)으로 전달하고, 애플리케이션의 **HTTP 응답**을 받아 적절한 **HTTPS 인증서**로 **암호화**한 뒤 **HTTPS**로 클라이언트에 다시 보내는 역할입니다. 이런 서버를 흔히 **<a href="https://en.wikipedia.org/wiki/TLS_termination_proxy" class="external-link" target="_blank">TLS Termination Proxy</a>**라고 부릅니다.
|
||||
|
||||
TLS Termination Proxy로 사용할 수 있는 옵션은 다음과 같습니다:
|
||||
|
||||
* Traefik (인증서 갱신도 처리 가능)
|
||||
* Caddy (인증서 갱신도 처리 가능)
|
||||
* Nginx
|
||||
* HAProxy
|
||||
|
||||
## Let's Encrypt { #lets-encrypt }
|
||||
|
||||
Let's Encrypt 이전에는 이러한 **HTTPS 인증서**가 신뢰할 수 있는 제3자에 의해 판매되었습니다.
|
||||
|
||||
인증서를 획득하는 과정은 번거롭고, 꽤 많은 서류 작업이 필요했으며, 인증서도 상당히 비쌌습니다.
|
||||
|
||||
하지만 그 후 **<a href="https://letsencrypt.org/" class="external-link" target="_blank">Let's Encrypt</a>**가 만들어졌습니다.
|
||||
|
||||
이는 Linux Foundation의 프로젝트입니다. 표준 암호학적 보안을 모두 사용하는 **HTTPS 인증서**를 **무료로**, 자동화된 방식으로 제공합니다. 이 인증서들은 수명이 짧고(약 3개월) 그래서 유효 기간이 짧은 만큼 **실제로 보안이 더 좋아지기도** 합니다.
|
||||
|
||||
도메인은 안전하게 검증되며 인증서는 자동으로 생성됩니다. 또한 이로 인해 인증서 갱신도 자동화할 수 있습니다.
|
||||
|
||||
목표는 인증서의 발급과 갱신을 자동화하여 **무료로, 영구히, 안전한 HTTPS**를 사용할 수 있게 하는 것입니다.
|
||||
|
||||
## 개발자를 위한 HTTPS { #https-for-developers }
|
||||
|
||||
개발자에게 중요한 개념들을 중심으로, HTTPS API가 단계별로 어떻게 보일 수 있는지 예시를 들어 보겠습니다.
|
||||
|
||||
### 도메인 이름 { #domain-name }
|
||||
|
||||
아마도 시작은 **도메인 이름**을 **획득**하는 것일 겁니다. 그 다음 DNS 서버(아마 같은 클라우드 제공업체)에서 이를 설정합니다.
|
||||
|
||||
대개 클라우드 서버(가상 머신) 같은 것을 사용하게 되고, 거기에는 <abbr title="That doesn't change - 변하지 않음">fixed</abbr> **공개 IP 주소**가 있습니다.
|
||||
|
||||
DNS 서버(들)에서 **도메인**이 서버의 **공개 IP 주소**를 가리키도록 레코드(“`A record`”)를 설정합니다.
|
||||
|
||||
보통은 처음 한 번, 모든 것을 설정할 때만 이 작업을 합니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
도메인 이름 부분은 HTTPS보다 훨씬 이전 단계지만, 모든 것이 도메인과 IP 주소에 의존하므로 여기서 언급할 가치가 있습니다.
|
||||
|
||||
///
|
||||
|
||||
### DNS { #dns }
|
||||
|
||||
이제 실제 HTTPS 부분에 집중해 보겠습니다.
|
||||
|
||||
먼저 브라우저는 **DNS 서버**에 질의하여, 여기서는 `someapp.example.com`이라는 **도메인에 대한 IP**가 무엇인지 확인합니다.
|
||||
|
||||
DNS 서버는 브라우저에게 특정 **IP 주소**를 사용하라고 알려줍니다. 이는 DNS 서버에 설정해 둔, 서버가 사용하는 공개 IP 주소입니다.
|
||||
|
||||
<img src="/img/deployment/https/https01.drawio.svg">
|
||||
|
||||
### TLS 핸드셰이크 시작 { #tls-handshake-start }
|
||||
|
||||
그 다음 브라우저는 **포트 443**(HTTPS 포트)에서 해당 IP 주소와 통신합니다.
|
||||
|
||||
통신의 첫 부분은 클라이언트와 서버 사이의 연결을 설정하고, 사용할 암호화 키 등을 결정하는 과정입니다.
|
||||
|
||||
<img src="/img/deployment/https/https02.drawio.svg">
|
||||
|
||||
클라이언트와 서버가 TLS 연결을 설정하기 위해 상호작용하는 이 과정을 **TLS 핸드셰이크**라고 합니다.
|
||||
|
||||
### SNI 확장을 사용하는 TLS { #tls-with-sni-extension }
|
||||
|
||||
서버에서는 특정 **IP 주소**의 특정 **포트**에서 **하나의 프로세스만** 리스닝할 수 있습니다. 같은 IP 주소에서 다른 포트로 리스닝하는 프로세스는 있을 수 있지만, IP 주소와 포트 조합마다 하나만 가능합니다.
|
||||
|
||||
TLS(HTTPS)는 기본적으로 특정 포트 `443`을 사용합니다. 따라서 우리가 필요한 포트는 이것입니다.
|
||||
|
||||
이 포트에서 하나의 프로세스만 리스닝할 수 있으므로, 그 역할을 하는 프로세스는 **TLS Termination Proxy**가 됩니다.
|
||||
|
||||
TLS Termination Proxy는 하나 이상의 **TLS 인증서**(HTTPS 인증서)에 접근할 수 있습니다.
|
||||
|
||||
앞에서 설명한 **SNI 확장**을 사용해, TLS Termination Proxy는 이 연결에 사용할 수 있는 TLS(HTTPS) 인증서들 중에서 클라이언트가 기대하는 도메인과 일치하는 것을 확인해 선택합니다.
|
||||
|
||||
이 경우에는 `someapp.example.com`에 대한 인증서를 사용합니다.
|
||||
|
||||
<img src="/img/deployment/https/https03.drawio.svg">
|
||||
|
||||
클라이언트는 이미 해당 TLS 인증서를 생성한 주체(여기서는 Let's Encrypt이지만, 이는 뒤에서 다시 보겠습니다)를 **신뢰**하므로, 인증서가 유효한지 **검증**할 수 있습니다.
|
||||
|
||||
그 다음 인증서를 사용해 클라이언트와 TLS Termination Proxy는 나머지 **TCP 통신**을 어떻게 **암호화할지 결정**합니다. 이로써 **TLS 핸드셰이크** 단계가 완료됩니다.
|
||||
|
||||
이후 클라이언트와 서버는 TLS가 제공하는 **암호화된 TCP 연결**을 갖게 됩니다. 그리고 그 연결을 사용해 실제 **HTTP 통신**을 시작할 수 있습니다.
|
||||
|
||||
이것이 바로 **HTTPS**입니다. 순수(암호화되지 않은) TCP 연결 대신 **안전한 TLS 연결** 안에서 **HTTP**를 그대로 사용하는 것입니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
통신의 암호화는 HTTP 레벨이 아니라 **TCP 레벨**에서 일어난다는 점에 주의하세요.
|
||||
|
||||
///
|
||||
|
||||
### HTTPS 요청 { #https-request }
|
||||
|
||||
이제 클라이언트와 서버(구체적으로는 브라우저와 TLS Termination Proxy)가 **암호화된 TCP 연결**을 갖게 되었으니 **HTTP 통신**을 시작할 수 있습니다.
|
||||
|
||||
따라서 클라이언트는 **HTTPS 요청**을 보냅니다. 이는 암호화된 TLS 연결을 통해 전달되는 HTTP 요청일 뿐입니다.
|
||||
|
||||
<img src="/img/deployment/https/https04.drawio.svg">
|
||||
|
||||
### 요청 복호화 { #decrypt-the-request }
|
||||
|
||||
TLS Termination Proxy는 합의된 암호화를 사용해 **요청을 복호화**하고, 애플리케이션을 실행 중인 프로세스(예: FastAPI 애플리케이션을 실행하는 Uvicorn 프로세스)에 **일반(복호화된) HTTP 요청**을 전달합니다.
|
||||
|
||||
<img src="/img/deployment/https/https05.drawio.svg">
|
||||
|
||||
### HTTP 응답 { #http-response }
|
||||
|
||||
애플리케이션은 요청을 처리하고 **일반(암호화되지 않은) HTTP 응답**을 TLS Termination Proxy로 보냅니다.
|
||||
|
||||
<img src="/img/deployment/https/https06.drawio.svg">
|
||||
|
||||
### HTTPS 응답 { #https-response }
|
||||
|
||||
그 다음 TLS Termination Proxy는 이전에 합의한 암호화( `someapp.example.com` 인증서로 시작된 것)를 사용해 **응답을 암호화**하고, 브라우저로 다시 보냅니다.
|
||||
|
||||
이후 브라우저는 응답이 유효한지, 올바른 암호화 키로 암호화되었는지 등을 확인합니다. 그런 다음 **응답을 복호화**하고 처리합니다.
|
||||
|
||||
<img src="/img/deployment/https/https07.drawio.svg">
|
||||
|
||||
클라이언트(브라우저)는 앞서 **HTTPS 인증서**로 합의한 암호화를 사용하고 있으므로, 해당 응답이 올바른 서버에서 왔다는 것을 알 수 있습니다.
|
||||
|
||||
### 여러 애플리케이션 { #multiple-applications }
|
||||
|
||||
같은 서버(또는 여러 서버)에는 예를 들어 다른 API 프로그램이나 데이터베이스처럼 **여러 애플리케이션**이 있을 수 있습니다.
|
||||
|
||||
특정 IP와 포트 조합은 하나의 프로세스만 처리할 수 있지만(예시에서는 TLS Termination Proxy), 다른 애플리케이션/프로세스도 **공개 IP와 포트 조합**을 동일하게 쓰려고만 하지 않는다면 서버에서 함께 실행될 수 있습니다.
|
||||
|
||||
<img src="/img/deployment/https/https08.drawio.svg">
|
||||
|
||||
이렇게 하면 TLS Termination Proxy가 **여러 도메인**에 대한 HTTPS와 인증서를 **여러 애플리케이션**에 대해 처리하고, 각 경우에 맞는 애플리케이션으로 요청을 전달할 수 있습니다.
|
||||
|
||||
### 인증서 갱신 { #certificate-renewal }
|
||||
|
||||
미래의 어느 시점에는 각 인증서가 **만료**됩니다(획득 후 약 3개월).
|
||||
|
||||
그 다음에는 또 다른 프로그램(경우에 따라 별도 프로그램일 수도 있고, 경우에 따라 같은 TLS Termination Proxy일 수도 있습니다)이 Let's Encrypt와 통신하여 인증서를 갱신합니다.
|
||||
|
||||
<img src="/img/deployment/https/https.drawio.svg">
|
||||
|
||||
**TLS 인증서**는 IP 주소가 아니라 **도메인 이름**과 **연결**되어 있습니다.
|
||||
|
||||
따라서 인증서를 갱신하려면, 갱신 프로그램이 권한 기관(Let's Encrypt)에게 해당 도메인을 실제로 **“소유”하고 제어하고 있음**을 **증명**해야 합니다.
|
||||
|
||||
이를 위해, 그리고 다양한 애플리케이션 요구를 수용하기 위해 여러 방법이 있습니다. 널리 쓰이는 방법은 다음과 같습니다:
|
||||
|
||||
* **일부 DNS 레코드 수정**.
|
||||
* 이를 위해서는 갱신 프로그램이 DNS 제공업체의 API를 지원해야 하므로, 사용하는 DNS 제공업체에 따라 가능할 수도, 아닐 수도 있습니다.
|
||||
* 도메인과 연결된 공개 IP 주소에서 **서버로 실행**(적어도 인증서 발급 과정 동안).
|
||||
* 앞에서 말했듯 특정 IP와 포트에서는 하나의 프로세스만 리스닝할 수 있습니다.
|
||||
* 이것이 동일한 TLS Termination Proxy가 인증서 갱신 과정까지 처리할 때 매우 유용한 이유 중 하나입니다.
|
||||
* 그렇지 않으면 TLS Termination Proxy를 잠시 중지하고, 갱신 프로그램을 시작해 인증서를 획득한 다음, TLS Termination Proxy에 인증서를 설정하고, 다시 TLS Termination Proxy를 재시작해야 할 수도 있습니다. 이는 TLS Termination Proxy가 꺼져 있는 동안 앱(들)을 사용할 수 없으므로 이상적이지 않습니다.
|
||||
|
||||
앱을 계속 제공하면서 이 갱신 과정을 처리할 수 있는 것은, 애플리케이션 서버(예: Uvicorn)에서 TLS 인증서를 직접 쓰는 대신 TLS Termination Proxy로 HTTPS를 처리하는 **별도의 시스템**을 두고 싶어지는 주요 이유 중 하나입니다.
|
||||
|
||||
## 프록시 전달 헤더 { #proxy-forwarded-headers }
|
||||
|
||||
프록시를 사용해 HTTPS를 처리할 때, **애플리케이션 서버**(예: FastAPI CLI를 통한 Uvicorn)는 HTTPS 과정에 대해 아무것도 알지 못하고 **TLS Termination Proxy**와는 일반 HTTP로 통신합니다.
|
||||
|
||||
이 **프록시**는 보통 요청을 **애플리케이션 서버**에 전달하기 전에, 요청이 프록시에 의해 **전달(forwarded)**되고 있음을 애플리케이션 서버가 알 수 있도록 일부 HTTP 헤더를 즉석에서 설정합니다.
|
||||
|
||||
/// note | 기술 세부사항
|
||||
|
||||
프록시 헤더는 다음과 같습니다:
|
||||
|
||||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For" class="external-link" target="_blank">X-Forwarded-For</a>
|
||||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto" class="external-link" target="_blank">X-Forwarded-Proto</a>
|
||||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Host" class="external-link" target="_blank">X-Forwarded-Host</a>
|
||||
|
||||
///
|
||||
|
||||
그럼에도 불구하고 **애플리케이션 서버**는 자신이 신뢰할 수 있는 **프록시** 뒤에 있다는 것을 모르므로, 기본적으로는 그 헤더들을 신뢰하지 않습니다.
|
||||
|
||||
하지만 **애플리케이션 서버**가 **프록시**가 보낸 *forwarded* 헤더를 신뢰하도록 설정할 수 있습니다. FastAPI CLI를 사용하고 있다면, *CLI Option* `--forwarded-allow-ips`를 사용해 어떤 IP에서 온 *forwarded* 헤더를 신뢰할지 지정할 수 있습니다.
|
||||
|
||||
예를 들어 **애플리케이션 서버**가 신뢰하는 **프록시**로부터만 통신을 받는다면, `--forwarded-allow-ips="*"`로 설정해 들어오는 모든 IP를 신뢰하게 할 수 있습니다. 어차피 **프록시**가 사용하는 IP에서만 요청을 받게 될 것이기 때문입니다.
|
||||
|
||||
이렇게 하면 애플리케이션은 자신이 사용하는 공개 URL이 무엇인지, HTTPS를 사용하는지, 도메인이 무엇인지 등을 알 수 있습니다.
|
||||
|
||||
예를 들어 리다이렉트를 올바르게 처리하는 데 유용합니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
이에 대해서는 [프록시 뒤에서 실행하기 - 프록시 전달 헤더 활성화](../advanced/behind-a-proxy.md#enable-proxy-forwarded-headers){.internal-link target=_blank} 문서에서 더 알아볼 수 있습니다.
|
||||
|
||||
///
|
||||
|
||||
## 요약 { #recap }
|
||||
|
||||
**HTTPS**는 매우 중요하며, 대부분의 경우 상당히 **핵심적**입니다. 개발자가 HTTPS와 관련해 해야 하는 노력의 대부분은 결국 **이 개념들을 이해**하고 그것들이 어떻게 동작하는지 파악하는 것입니다.
|
||||
|
||||
하지만 **개발자를 위한 HTTPS**의 기본 정보를 알고 나면, 여러 도구를 쉽게 조합하고 설정하여 모든 것을 간단하게 관리할 수 있습니다.
|
||||
|
||||
다음 장들에서는 **FastAPI** 애플리케이션을 위한 **HTTPS** 설정 방법을 여러 구체적인 예시로 보여드리겠습니다. 🔒
|
||||
@@ -1,157 +0,0 @@
|
||||
# 서버를 수동으로 실행하기 { #run-a-server-manually }
|
||||
|
||||
## `fastapi run` 명령 사용하기 { #use-the-fastapi-run-command }
|
||||
|
||||
요약하면, `fastapi run`을 사용해 FastAPI 애플리케이션을 서비스하세요:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ <font color="#4E9A06">fastapi</font> run <u style="text-decoration-style:solid">main.py</u>
|
||||
|
||||
<span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting production server 🚀
|
||||
|
||||
Searching for package file structure from directories
|
||||
with <font color="#3465A4">__init__.py</font> files
|
||||
Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font>
|
||||
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> module </font></span> 🐍 main.py
|
||||
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> code </font></span> Importing the FastAPI app object from the module with
|
||||
the following code:
|
||||
|
||||
<u style="text-decoration-style:solid">from </u><u style="text-decoration-style:solid"><b>main</b></u><u style="text-decoration-style:solid"> import </u><u style="text-decoration-style:solid"><b>app</b></u>
|
||||
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> app </font></span> Using import string: <font color="#3465A4">main:app</font>
|
||||
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Server started at <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000</u></font>
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Documentation at <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000/docs</u></font>
|
||||
|
||||
Logs:
|
||||
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>2306215</b></font><b>]</b>
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup.
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete.
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000</u></font> <b>(</b>Press CTRL+C
|
||||
to quit<b>)</b>
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
대부분의 경우에는 이것으로 동작합니다. 😎
|
||||
|
||||
예를 들어 이 명령은 컨테이너나 서버 등에서 **FastAPI** 앱을 시작할 때 사용할 수 있습니다.
|
||||
|
||||
## ASGI 서버 { #asgi-servers }
|
||||
|
||||
이제 조금 더 자세히 살펴보겠습니다.
|
||||
|
||||
FastAPI는 <abbr title="Asynchronous Server Gateway Interface">ASGI</abbr>라고 불리는, Python 웹 프레임워크와 서버를 만들기 위한 표준을 사용합니다. FastAPI는 ASGI 웹 프레임워크입니다.
|
||||
|
||||
원격 서버 머신에서 **FastAPI** 애플리케이션(또는 다른 ASGI 애플리케이션)을 실행하기 위해 필요한 핵심 요소는 **Uvicorn** 같은 ASGI 서버 프로그램입니다. `fastapi` 명령에는 기본으로 이것이 포함되어 있습니다.
|
||||
|
||||
다음을 포함해 여러 대안이 있습니다:
|
||||
|
||||
* <a href="https://www.uvicorn.dev/" class="external-link" target="_blank">Uvicorn</a>: 고성능 ASGI 서버.
|
||||
* <a href="https://hypercorn.readthedocs.io/" class="external-link" target="_blank">Hypercorn</a>: HTTP/2 및 Trio 등 여러 기능과 호환되는 ASGI 서버.
|
||||
* <a href="https://github.com/django/daphne" class="external-link" target="_blank">Daphne</a>: Django Channels를 위해 만들어진 ASGI 서버.
|
||||
* <a href="https://github.com/emmett-framework/granian" class="external-link" target="_blank">Granian</a>: Python 애플리케이션을 위한 Rust HTTP 서버.
|
||||
* <a href="https://unit.nginx.org/howto/fastapi/" class="external-link" target="_blank">NGINX Unit</a>: NGINX Unit은 가볍고 다용도로 사용할 수 있는 웹 애플리케이션 런타임입니다.
|
||||
|
||||
## 서버 머신과 서버 프로그램 { #server-machine-and-server-program }
|
||||
|
||||
이름에 관해 기억해 둘 작은 디테일이 있습니다. 💡
|
||||
|
||||
"**server**"라는 단어는 보통 원격/클라우드 컴퓨터(물리 또는 가상 머신)와, 그 머신에서 실행 중인 프로그램(예: Uvicorn) 둘 다를 가리키는 데 사용됩니다.
|
||||
|
||||
일반적으로 "server"를 읽을 때, 이 두 가지 중 하나를 의미할 수 있다는 점을 기억하세요.
|
||||
|
||||
원격 머신을 가리킬 때는 **server**라고 부르는 것이 일반적이지만, **machine**, **VM**(virtual machine), **node**라고 부르기도 합니다. 이것들은 보통 Linux를 실행하는 원격 머신의 한 형태를 뜻하며, 그곳에서 프로그램을 실행합니다.
|
||||
|
||||
## 서버 프로그램 설치하기 { #install-the-server-program }
|
||||
|
||||
FastAPI를 설치하면 프로덕션 서버인 Uvicorn이 함께 설치되며, `fastapi run` 명령으로 시작할 수 있습니다.
|
||||
|
||||
하지만 ASGI 서버를 수동으로 설치할 수도 있습니다.
|
||||
|
||||
[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 만들고 활성화한 다음, 서버 애플리케이션을 설치하세요.
|
||||
|
||||
예를 들어 Uvicorn을 설치하려면:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ pip install "uvicorn[standard]"
|
||||
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
다른 어떤 ASGI 서버 프로그램도 비슷한 과정이 적용됩니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
`standard`를 추가하면 Uvicorn이 권장되는 추가 의존성 몇 가지를 설치하고 사용합니다.
|
||||
|
||||
여기에는 `asyncio`를 고성능으로 대체할 수 있는 드롭인 대체재인 `uvloop`가 포함되며, 큰 동시성 성능 향상을 제공합니다.
|
||||
|
||||
`pip install "fastapi[standard]"` 같은 방식으로 FastAPI를 설치하면 `uvicorn[standard]`도 함께 설치됩니다.
|
||||
|
||||
///
|
||||
|
||||
## 서버 프로그램 실행하기 { #run-the-server-program }
|
||||
|
||||
ASGI 서버를 수동으로 설치했다면, 보통 FastAPI 애플리케이션을 임포트하기 위해 특별한 형식의 import string을 전달해야 합니다:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ uvicorn main:app --host 0.0.0.0 --port 80
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
/// note | 참고
|
||||
|
||||
`uvicorn main:app` 명령은 다음을 가리킵니다:
|
||||
|
||||
* `main`: 파일 `main.py`(Python "module").
|
||||
* `app`: `main.py` 안에서 `app = FastAPI()` 라인으로 생성된 객체.
|
||||
|
||||
이는 다음과 동일합니다:
|
||||
|
||||
```Python
|
||||
from main import app
|
||||
```
|
||||
|
||||
///
|
||||
|
||||
각 ASGI 서버 프로그램의 대안도 비슷한 명령을 갖고 있으며, 자세한 내용은 각자의 문서를 참고하세요.
|
||||
|
||||
/// warning | 경고
|
||||
|
||||
Uvicorn과 다른 서버는 개발 중에 유용한 `--reload` 옵션을 지원합니다.
|
||||
|
||||
`--reload` 옵션은 훨씬 더 많은 리소스를 소비하고, 더 불안정합니다.
|
||||
|
||||
**개발** 중에는 큰 도움이 되지만, **프로덕션**에서는 사용하지 **말아야** 합니다.
|
||||
|
||||
///
|
||||
|
||||
## 배포 개념 { #deployment-concepts }
|
||||
|
||||
이 예제들은 서버 프로그램(예: Uvicorn)을 실행하여 **단일 프로세스**를 시작하고, 사전에 정한 포트(예: `80`)에서 모든 IP(`0.0.0.0`)로 들어오는 요청을 받도록 합니다.
|
||||
|
||||
이것이 기본 아이디어입니다. 하지만 보통은 다음과 같은 추가 사항들도 처리해야 합니다:
|
||||
|
||||
* 보안 - HTTPS
|
||||
* 시작 시 자동 실행
|
||||
* 재시작
|
||||
* 복제(실행 중인 프로세스 수)
|
||||
* 메모리
|
||||
* 시작 전 선행 단계
|
||||
|
||||
다음 장들에서 이 각각의 개념을 어떻게 생각해야 하는지, 그리고 이를 다루기 위한 전략의 구체적인 예시를 더 알려드리겠습니다. 🚀
|
||||
@@ -1,17 +0,0 @@
|
||||
# 이전 403 인증 오류 상태 코드 사용하기 { #use-old-403-authentication-error-status-codes }
|
||||
|
||||
FastAPI 버전 `0.122.0` 이전에는, 통합 보안 유틸리티가 인증 실패 후 클라이언트에 오류를 반환할 때 HTTP 상태 코드 `403 Forbidden`을 사용했습니다.
|
||||
|
||||
FastAPI 버전 `0.122.0`부터는 더 적절한 HTTP 상태 코드 `401 Unauthorized`를 사용하며, HTTP 명세인 <a href="https://datatracker.ietf.org/doc/html/rfc7235#section-3.1" class="external-link" target="_blank">RFC 7235</a>, <a href="https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized" class="external-link" target="_blank">RFC 9110</a>를 따라 응답에 합리적인 `WWW-Authenticate` 헤더를 반환합니다.
|
||||
|
||||
하지만 어떤 이유로든 클라이언트가 이전 동작에 의존하고 있다면, 보안 클래스에서 `make_not_authenticated_error` 메서드를 오버라이드하여 이전 동작으로 되돌릴 수 있습니다.
|
||||
|
||||
예를 들어, 기본값인 `401 Unauthorized` 오류 대신 `403 Forbidden` 오류를 반환하는 `HTTPBearer`의 서브클래스를 만들 수 있습니다:
|
||||
|
||||
{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *}
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
함수는 예외를 `raise`하는 것이 아니라 예외 인스턴스를 `return`한다는 점에 유의하세요. 예외를 발생시키는(`raise`) 작업은 내부 코드의 나머지 부분에서 수행됩니다.
|
||||
|
||||
///
|
||||
@@ -1,185 +0,0 @@
|
||||
# 커스텀 Docs UI 정적 에셋(자체 호스팅) { #custom-docs-ui-static-assets-self-hosting }
|
||||
|
||||
API 문서는 **Swagger UI**와 **ReDoc**을 사용하며, 각각 JavaScript와 CSS 파일이 필요합니다.
|
||||
|
||||
기본적으로 이러한 파일은 <abbr title="Content Delivery Network - 콘텐츠 전송 네트워크: 일반적으로 여러 서버로 구성되어 JavaScript와 CSS 같은 정적 파일을 제공하는 서비스입니다. 보통 클라이언트에 더 가까운 서버에서 파일을 제공해 성능을 향상시키는 데 사용됩니다.">CDN</abbr>에서 제공됩니다.
|
||||
|
||||
하지만 이를 커스터마이징할 수 있으며, 특정 CDN을 지정하거나 파일을 직접 제공할 수도 있습니다.
|
||||
|
||||
## JavaScript와 CSS용 커스텀 CDN { #custom-cdn-for-javascript-and-css }
|
||||
|
||||
예를 들어 다른 <abbr title="Content Delivery Network">CDN</abbr>을 사용하고 싶다고 해봅시다. 예를 들면 `https://unpkg.com/`을 사용하려는 경우입니다.
|
||||
|
||||
이는 예를 들어 특정 국가에서 일부 URL을 제한하는 경우에 유용할 수 있습니다.
|
||||
|
||||
### 자동 문서 비활성화하기 { #disable-the-automatic-docs }
|
||||
|
||||
첫 번째 단계는 자동 문서를 비활성화하는 것입니다. 기본적으로 자동 문서는 기본 CDN을 사용하기 때문입니다.
|
||||
|
||||
비활성화하려면 `FastAPI` 앱을 생성할 때 해당 URL을 `None`으로 설정하세요:
|
||||
|
||||
{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[8] *}
|
||||
|
||||
### 커스텀 문서 포함하기 { #include-the-custom-docs }
|
||||
|
||||
이제 커스텀 문서를 위한 *경로 처리*를 만들 수 있습니다.
|
||||
|
||||
FastAPI 내부 함수를 재사용해 문서용 HTML 페이지를 생성하고, 필요한 인자를 전달할 수 있습니다:
|
||||
|
||||
* `openapi_url`: 문서 HTML 페이지가 API의 OpenAPI 스키마를 가져올 수 있는 URL입니다. 여기서는 `app.openapi_url` 속성을 사용할 수 있습니다.
|
||||
* `title`: API의 제목입니다.
|
||||
* `oauth2_redirect_url`: 기본값을 사용하려면 여기서 `app.swagger_ui_oauth2_redirect_url`을 사용할 수 있습니다.
|
||||
* `swagger_js_url`: Swagger UI 문서의 HTML이 **JavaScript** 파일을 가져올 수 있는 URL입니다. 커스텀 CDN URL입니다.
|
||||
* `swagger_css_url`: Swagger UI 문서의 HTML이 **CSS** 파일을 가져올 수 있는 URL입니다. 커스텀 CDN URL입니다.
|
||||
|
||||
ReDoc도 마찬가지입니다...
|
||||
|
||||
{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[2:6,11:19,22:24,27:33] *}
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
`swagger_ui_redirect`에 대한 *경로 처리*는 OAuth2를 사용할 때 도움이 되는 헬퍼입니다.
|
||||
|
||||
API를 OAuth2 provider와 통합하면 인증을 수행한 뒤 획득한 자격 증명으로 API 문서로 다시 돌아올 수 있습니다. 그리고 실제 OAuth2 인증을 사용해 API와 상호작용할 수 있습니다.
|
||||
|
||||
Swagger UI가 이 과정을 백그라운드에서 처리해 주지만, 이를 위해 이 "redirect" 헬퍼가 필요합니다.
|
||||
|
||||
///
|
||||
|
||||
### 테스트용 *경로 처리* 만들기 { #create-a-path-operation-to-test-it }
|
||||
|
||||
이제 모든 것이 제대로 동작하는지 테스트할 수 있도록 *경로 처리*를 하나 만드세요:
|
||||
|
||||
{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[36:38] *}
|
||||
|
||||
### 테스트하기 { #test-it }
|
||||
|
||||
이제 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>에서 문서에 접속한 뒤 페이지를 새로고침하면, 새 CDN에서 에셋을 불러오는 것을 확인할 수 있습니다.
|
||||
|
||||
## 문서용 JavaScript와 CSS 자체 호스팅하기 { #self-hosting-javascript-and-css-for-docs }
|
||||
|
||||
JavaScript와 CSS를 자체 호스팅하는 것은 예를 들어, 오프라인 상태이거나 외부 인터넷에 접근할 수 없는 환경, 또는 로컬 네트워크에서도 앱이 계속 동작해야 할 때 유용할 수 있습니다.
|
||||
|
||||
여기서는 동일한 FastAPI 앱에서 해당 파일을 직접 제공하고, 문서가 이를 사용하도록 설정하는 방법을 살펴봅니다.
|
||||
|
||||
### 프로젝트 파일 구조 { #project-file-structure }
|
||||
|
||||
프로젝트 파일 구조가 다음과 같다고 해봅시다:
|
||||
|
||||
```
|
||||
.
|
||||
├── app
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py
|
||||
```
|
||||
|
||||
이제 해당 정적 파일을 저장할 디렉터리를 만드세요.
|
||||
|
||||
새 파일 구조는 다음과 같을 수 있습니다:
|
||||
|
||||
```
|
||||
.
|
||||
├── app
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py
|
||||
└── static/
|
||||
```
|
||||
|
||||
### 파일 다운로드하기 { #download-the-files }
|
||||
|
||||
문서에 필요한 정적 파일을 다운로드해서 `static/` 디렉터리에 넣으세요.
|
||||
|
||||
각 링크를 우클릭한 뒤 "링크를 다른 이름으로 저장..."과 비슷한 옵션을 선택하면 될 것입니다.
|
||||
|
||||
**Swagger UI**는 다음 파일을 사용합니다:
|
||||
|
||||
* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js" class="external-link" target="_blank">`swagger-ui-bundle.js`</a>
|
||||
* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css" class="external-link" target="_blank">`swagger-ui.css`</a>
|
||||
|
||||
**ReDoc**은 다음 파일을 사용합니다:
|
||||
|
||||
* <a href="https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js" class="external-link" target="_blank">`redoc.standalone.js`</a>
|
||||
|
||||
이후 파일 구조는 다음과 같을 수 있습니다:
|
||||
|
||||
```
|
||||
.
|
||||
├── app
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py
|
||||
└── static
|
||||
├── redoc.standalone.js
|
||||
├── swagger-ui-bundle.js
|
||||
└── swagger-ui.css
|
||||
```
|
||||
|
||||
### 정적 파일 제공하기 { #serve-the-static-files }
|
||||
|
||||
* `StaticFiles`를 import합니다.
|
||||
* 특정 경로에 `StaticFiles()` 인스턴스를 "마운트(mount)"합니다.
|
||||
|
||||
{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[7,11] *}
|
||||
|
||||
### 정적 파일 테스트하기 { #test-the-static-files }
|
||||
|
||||
애플리케이션을 시작하고 <a href="http://127.0.0.1:8000/static/redoc.standalone.js" class="external-link" target="_blank">http://127.0.0.1:8000/static/redoc.standalone.js</a>로 이동하세요.
|
||||
|
||||
**ReDoc**용 매우 긴 JavaScript 파일이 보일 것입니다.
|
||||
|
||||
예를 들어 다음과 같이 시작할 수 있습니다:
|
||||
|
||||
```JavaScript
|
||||
/*! For license information please see redoc.standalone.js.LICENSE.txt */
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("null")):
|
||||
...
|
||||
```
|
||||
|
||||
이는 앱에서 정적 파일을 제공할 수 있고, 문서용 정적 파일을 올바른 위치에 배치했다는 것을 확인해 줍니다.
|
||||
|
||||
이제 문서가 이 정적 파일을 사용하도록 앱을 설정할 수 있습니다.
|
||||
|
||||
### 정적 파일을 위한 자동 문서 비활성화하기 { #disable-the-automatic-docs-for-static-files }
|
||||
|
||||
커스텀 CDN을 사용할 때와 마찬가지로, 첫 단계는 자동 문서를 비활성화하는 것입니다. 자동 문서는 기본적으로 CDN을 사용합니다.
|
||||
|
||||
비활성화하려면 `FastAPI` 앱을 생성할 때 해당 URL을 `None`으로 설정하세요:
|
||||
|
||||
{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[9] *}
|
||||
|
||||
### 정적 파일을 위한 커스텀 문서 포함하기 { #include-the-custom-docs-for-static-files }
|
||||
|
||||
그리고 커스텀 CDN을 사용할 때와 동일한 방식으로, 이제 커스텀 문서를 위한 *경로 처리*를 만들 수 있습니다.
|
||||
|
||||
다시 한 번, FastAPI 내부 함수를 재사용해 문서용 HTML 페이지를 생성하고, 필요한 인자를 전달할 수 있습니다:
|
||||
|
||||
* `openapi_url`: 문서 HTML 페이지가 API의 OpenAPI 스키마를 가져올 수 있는 URL입니다. 여기서는 `app.openapi_url` 속성을 사용할 수 있습니다.
|
||||
* `title`: API의 제목입니다.
|
||||
* `oauth2_redirect_url`: 기본값을 사용하려면 여기서 `app.swagger_ui_oauth2_redirect_url`을 사용할 수 있습니다.
|
||||
* `swagger_js_url`: Swagger UI 문서의 HTML이 **JavaScript** 파일을 가져올 수 있는 URL입니다. **이제는 여러분의 앱이 직접 제공하는 파일입니다**.
|
||||
* `swagger_css_url`: Swagger UI 문서의 HTML이 **CSS** 파일을 가져올 수 있는 URL입니다. **이제는 여러분의 앱이 직접 제공하는 파일입니다**.
|
||||
|
||||
ReDoc도 마찬가지입니다...
|
||||
|
||||
{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[2:6,14:22,25:27,30:36] *}
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
`swagger_ui_redirect`에 대한 *경로 처리*는 OAuth2를 사용할 때 도움이 되는 헬퍼입니다.
|
||||
|
||||
API를 OAuth2 provider와 통합하면 인증을 수행한 뒤 획득한 자격 증명으로 API 문서로 다시 돌아올 수 있습니다. 그리고 실제 OAuth2 인증을 사용해 API와 상호작용할 수 있습니다.
|
||||
|
||||
Swagger UI가 이 과정을 백그라운드에서 처리해 주지만, 이를 위해 이 "redirect" 헬퍼가 필요합니다.
|
||||
|
||||
///
|
||||
|
||||
### 정적 파일 테스트용 *경로 처리* 만들기 { #create-a-path-operation-to-test-static-files }
|
||||
|
||||
이제 모든 것이 제대로 동작하는지 테스트할 수 있도록 *경로 처리*를 하나 만드세요:
|
||||
|
||||
{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[39:41] *}
|
||||
|
||||
### 정적 파일 UI 테스트하기 { #test-static-files-ui }
|
||||
|
||||
이제 WiFi 연결을 끊고 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>에서 문서에 접속한 뒤 페이지를 새로고침해 보세요.
|
||||
|
||||
인터넷이 없어도 API 문서를 보고, API와 상호작용할 수 있을 것입니다.
|
||||
@@ -1,109 +0,0 @@
|
||||
# 커스텀 Request 및 APIRoute 클래스 { #custom-request-and-apiroute-class }
|
||||
|
||||
일부 경우에는 `Request`와 `APIRoute` 클래스에서 사용되는 로직을 오버라이드하고 싶을 수 있습니다.
|
||||
|
||||
특히, 이는 middleware에 있는 로직의 좋은 대안이 될 수 있습니다.
|
||||
|
||||
예를 들어, 애플리케이션에서 처리되기 전에 요청 바디를 읽거나 조작하고 싶을 때가 그렇습니다.
|
||||
|
||||
/// danger | 위험
|
||||
|
||||
이 기능은 "고급" 기능입니다.
|
||||
|
||||
**FastAPI**를 이제 막 시작했다면 이 섹션은 건너뛰는 것이 좋습니다.
|
||||
|
||||
///
|
||||
|
||||
## 사용 사례 { #use-cases }
|
||||
|
||||
사용 사례에는 다음이 포함됩니다:
|
||||
|
||||
* JSON이 아닌 요청 바디를 JSON으로 변환하기(예: <a href="https://msgpack.org/index.html" class="external-link" target="_blank">`msgpack`</a>).
|
||||
* gzip으로 압축된 요청 바디 압축 해제하기.
|
||||
* 모든 요청 바디를 자동으로 로깅하기.
|
||||
|
||||
## 커스텀 요청 바디 인코딩 처리하기 { #handling-custom-request-body-encodings }
|
||||
|
||||
커스텀 `Request` 서브클래스를 사용해 gzip 요청의 압축을 해제하는 방법을 살펴보겠습니다.
|
||||
|
||||
그리고 그 커스텀 요청 클래스를 사용하기 위한 `APIRoute` 서브클래스도 함께 보겠습니다.
|
||||
|
||||
### 커스텀 `GzipRequest` 클래스 만들기 { #create-a-custom-gziprequest-class }
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
이 예시는 동작 방식 시연을 위한 장난감 예제입니다. Gzip 지원이 필요하다면 제공되는 [`GzipMiddleware`](../advanced/middleware.md#gzipmiddleware){.internal-link target=_blank}를 사용할 수 있습니다.
|
||||
|
||||
///
|
||||
|
||||
먼저, `GzipRequest` 클래스를 만듭니다. 이 클래스는 `Request.body()` 메서드를 덮어써서, 적절한 헤더가 있는 경우 바디를 압축 해제합니다.
|
||||
|
||||
헤더에 `gzip`이 없으면 바디를 압축 해제하려고 시도하지 않습니다.
|
||||
|
||||
이렇게 하면 동일한 route 클래스가 gzip으로 압축된 요청과 압축되지 않은 요청을 모두 처리할 수 있습니다.
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *}
|
||||
|
||||
### 커스텀 `GzipRoute` 클래스 만들기 { #create-a-custom-gziproute-class }
|
||||
|
||||
다음으로, `GzipRequest`를 활용하는 `fastapi.routing.APIRoute`의 커스텀 서브클래스를 만듭니다.
|
||||
|
||||
이번에는 `APIRoute.get_route_handler()` 메서드를 오버라이드합니다.
|
||||
|
||||
이 메서드는 함수를 반환합니다. 그리고 그 함수가 요청을 받아 응답을 반환합니다.
|
||||
|
||||
여기서는 원본 요청으로부터 `GzipRequest`를 만들기 위해 이를 사용합니다.
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[19:27] *}
|
||||
|
||||
/// note | 기술 세부사항
|
||||
|
||||
`Request`에는 `request.scope` 속성이 있는데, 이는 요청과 관련된 메타데이터를 담고 있는 Python `dict`입니다.
|
||||
|
||||
`Request`에는 또한 `request.receive`가 있는데, 이는 요청의 바디를 "받기(receive)" 위한 함수입니다.
|
||||
|
||||
`scope` `dict`와 `receive` 함수는 모두 ASGI 명세의 일부입니다.
|
||||
|
||||
그리고 이 두 가지, `scope`와 `receive`가 새로운 `Request` 인스턴스를 만드는 데 필요한 것들입니다.
|
||||
|
||||
`Request`에 대해 더 알아보려면 <a href="https://www.starlette.dev/requests/" class="external-link" target="_blank">Starlette의 Requests 문서</a>를 확인하세요.
|
||||
|
||||
///
|
||||
|
||||
`GzipRequest.get_route_handler`가 반환하는 함수가 다르게 하는 유일한 것은 `Request`를 `GzipRequest`로 변환하는 것입니다.
|
||||
|
||||
이렇게 하면, 우리의 `GzipRequest`가 *경로 처리*로 전달하기 전에(필요하다면) 데이터의 압축 해제를 담당하게 됩니다.
|
||||
|
||||
그 이후의 모든 처리 로직은 동일합니다.
|
||||
|
||||
하지만 `GzipRequest.body`에서 변경을 했기 때문에, 필요할 때 **FastAPI**가 로드하는 시점에 요청 바디는 자동으로 압축 해제됩니다.
|
||||
|
||||
## 예외 핸들러에서 요청 바디 접근하기 { #accessing-the-request-body-in-an-exception-handler }
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
같은 문제를 해결하려면 `RequestValidationError`에 대한 커스텀 핸들러에서 `body`를 사용하는 편이 아마 훨씬 더 쉽습니다([오류 처리하기](../tutorial/handling-errors.md#use-the-requestvalidationerror-body){.internal-link target=_blank}).
|
||||
|
||||
하지만 이 예시도 여전히 유효하며, 내부 컴포넌트와 상호작용하는 방법을 보여줍니다.
|
||||
|
||||
///
|
||||
|
||||
같은 접근 방식을 사용해 예외 핸들러에서 요청 바디에 접근할 수도 있습니다.
|
||||
|
||||
필요한 것은 `try`/`except` 블록 안에서 요청을 처리하는 것뿐입니다:
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[14,16] *}
|
||||
|
||||
예외가 발생하더라도 `Request` 인스턴스는 여전히 스코프 안에 남아 있으므로, 오류를 처리할 때 요청 바디를 읽고 활용할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[17:19] *}
|
||||
|
||||
## 라우터에서의 커스텀 `APIRoute` 클래스 { #custom-apiroute-class-in-a-router }
|
||||
|
||||
`APIRouter`의 `route_class` 파라미터를 설정할 수도 있습니다:
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[26] *}
|
||||
|
||||
이 예시에서는 `router` 아래의 *경로 처리*들이 커스텀 `TimedRoute` 클래스를 사용하며, 응답을 생성하는 데 걸린 시간을 담은 추가 `X-Response-Time` 헤더가 응답에 포함됩니다:
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[13:20] *}
|
||||
@@ -1,80 +0,0 @@
|
||||
# OpenAPI 확장하기 { #extending-openapi }
|
||||
|
||||
생성된 OpenAPI 스키마를 수정해야 하는 경우가 있습니다.
|
||||
|
||||
이 섹션에서 그 방법을 살펴보겠습니다.
|
||||
|
||||
## 일반적인 과정 { #the-normal-process }
|
||||
|
||||
일반적인(기본) 과정은 다음과 같습니다.
|
||||
|
||||
`FastAPI` 애플리케이션(인스턴스)에는 OpenAPI 스키마를 반환해야 하는 `.openapi()` 메서드가 있습니다.
|
||||
|
||||
애플리케이션 객체를 생성하는 과정에서 `/openapi.json`(또는 `openapi_url`에 설정한 경로)용 *경로 처리*가 등록됩니다.
|
||||
|
||||
이 경로 처리는 애플리케이션의 `.openapi()` 메서드 결과를 JSON 응답으로 반환할 뿐입니다.
|
||||
|
||||
기본적으로 `.openapi()` 메서드는 프로퍼티 `.openapi_schema`에 내용이 있는지 확인하고, 있으면 그 내용을 반환합니다.
|
||||
|
||||
없으면 `fastapi.openapi.utils.get_openapi`에 있는 유틸리티 함수를 사용해 생성합니다.
|
||||
|
||||
그리고 `get_openapi()` 함수는 다음을 파라미터로 받습니다:
|
||||
|
||||
* `title`: 문서에 표시되는 OpenAPI 제목.
|
||||
* `version`: API 버전. 예: `2.5.0`.
|
||||
* `openapi_version`: 사용되는 OpenAPI 스펙 버전. 기본값은 최신인 `3.1.0`.
|
||||
* `summary`: API에 대한 짧은 요약.
|
||||
* `description`: API 설명. markdown을 포함할 수 있으며 문서에 표시됩니다.
|
||||
* `routes`: 라우트 목록. 각각 등록된 *경로 처리*입니다. `app.routes`에서 가져옵니다.
|
||||
|
||||
/// info | 정보
|
||||
|
||||
`summary` 파라미터는 OpenAPI 3.1.0 이상에서 사용할 수 있으며, FastAPI 0.99.0 이상에서 지원됩니다.
|
||||
|
||||
///
|
||||
|
||||
## 기본값 덮어쓰기 { #overriding-the-defaults }
|
||||
|
||||
위 정보를 바탕으로, 동일한 유틸리티 함수를 사용해 OpenAPI 스키마를 생성하고 필요한 각 부분을 덮어쓸 수 있습니다.
|
||||
|
||||
예를 들어, <a href="https://github.com/Rebilly/ReDoc/blob/master/docs/redoc-vendor-extensions.md#x-logo" class="external-link" target="_blank">커스텀 로고를 포함하기 위한 ReDoc의 OpenAPI 확장</a>을 추가해 보겠습니다.
|
||||
|
||||
### 일반적인 **FastAPI** { #normal-fastapi }
|
||||
|
||||
먼저, 평소처럼 **FastAPI** 애플리케이션을 모두 작성합니다:
|
||||
|
||||
{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[1,4,7:9] *}
|
||||
|
||||
### OpenAPI 스키마 생성하기 { #generate-the-openapi-schema }
|
||||
|
||||
그다음 `custom_openapi()` 함수 안에서, 동일한 유틸리티 함수를 사용해 OpenAPI 스키마를 생성합니다:
|
||||
|
||||
{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[2,15:21] *}
|
||||
|
||||
### OpenAPI 스키마 수정하기 { #modify-the-openapi-schema }
|
||||
|
||||
이제 OpenAPI 스키마의 `info` "object"에 커스텀 `x-logo`를 추가하여 ReDoc 확장을 더할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[22:24] *}
|
||||
|
||||
### OpenAPI 스키마 캐시하기 { #cache-the-openapi-schema }
|
||||
|
||||
생성한 스키마를 저장하기 위한 "cache"로 `.openapi_schema` 프로퍼티를 사용할 수 있습니다.
|
||||
|
||||
이렇게 하면 사용자가 API 문서를 열 때마다 애플리케이션이 스키마를 매번 생성하지 않아도 됩니다.
|
||||
|
||||
스키마는 한 번만 생성되고, 이후 요청에서는 같은 캐시된 스키마가 사용됩니다.
|
||||
|
||||
{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[13:14,25:26] *}
|
||||
|
||||
### 메서드 오버라이드하기 { #override-the-method }
|
||||
|
||||
이제 `.openapi()` 메서드를 새 함수로 교체할 수 있습니다.
|
||||
|
||||
{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[29] *}
|
||||
|
||||
### 확인하기 { #check-it }
|
||||
|
||||
<a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>로 이동하면 커스텀 로고(이 예시에서는 **FastAPI** 로고)를 사용하는 것을 확인할 수 있습니다:
|
||||
|
||||
<img src="/img/tutorial/extending-openapi/image01.png">
|
||||
@@ -1,39 +0,0 @@
|
||||
# 일반 - 사용 방법 - 레시피 { #general-how-to-recipes }
|
||||
|
||||
일반적이거나 자주 나오는 질문에 대해, 문서의 다른 위치로 안내하는 몇 가지 포인터를 소개합니다.
|
||||
|
||||
## 데이터 필터링 - 보안 { #filter-data-security }
|
||||
|
||||
반환하면 안 되는 데이터를 과도하게 반환하지 않도록 하려면, [튜토리얼 - 응답 모델 - 반환 타입](../tutorial/response-model.md){.internal-link target=_blank} 문서를 읽어보세요.
|
||||
|
||||
## 문서화 태그 - OpenAPI { #documentation-tags-openapi }
|
||||
|
||||
*경로 처리*에 태그를 추가하고, 문서 UI에서 이를 그룹화하려면 [튜토리얼 - 경로 처리 구성 - 태그](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank} 문서를 읽어보세요.
|
||||
|
||||
## 문서화 요약 및 설명 - OpenAPI { #documentation-summary-and-description-openapi }
|
||||
|
||||
*경로 처리*에 요약과 설명을 추가하고, 문서 UI에 표시하려면 [튜토리얼 - 경로 처리 구성 - 요약 및 설명](../tutorial/path-operation-configuration.md#summary-and-description){.internal-link target=_blank} 문서를 읽어보세요.
|
||||
|
||||
## 문서화 응답 설명 - OpenAPI { #documentation-response-description-openapi }
|
||||
|
||||
문서 UI에 표시되는 응답의 설명을 정의하려면 [튜토리얼 - 경로 처리 구성 - 응답 설명](../tutorial/path-operation-configuration.md#response-description){.internal-link target=_blank} 문서를 읽어보세요.
|
||||
|
||||
## 문서화 *경로 처리* 지원 중단하기 - OpenAPI { #documentation-deprecate-a-path-operation-openapi }
|
||||
|
||||
*경로 처리*를 지원 중단(deprecate)으로 표시하고, 문서 UI에 보여주려면 [튜토리얼 - 경로 처리 구성 - 지원 중단](../tutorial/path-operation-configuration.md#deprecate-a-path-operation){.internal-link target=_blank} 문서를 읽어보세요.
|
||||
|
||||
## 어떤 데이터든 JSON 호환으로 변환하기 { #convert-any-data-to-json-compatible }
|
||||
|
||||
어떤 데이터든 JSON 호환 형식으로 변환하려면 [튜토리얼 - JSON 호환 인코더](../tutorial/encoder.md){.internal-link target=_blank} 문서를 읽어보세요.
|
||||
|
||||
## OpenAPI 메타데이터 - 문서 { #openapi-metadata-docs }
|
||||
|
||||
라이선스, 버전, 연락처 등의 정보를 포함해 OpenAPI 스키마에 메타데이터를 추가하려면 [튜토리얼 - 메타데이터와 문서 URL](../tutorial/metadata.md){.internal-link target=_blank} 문서를 읽어보세요.
|
||||
|
||||
## OpenAPI 사용자 정의 URL { #openapi-custom-url }
|
||||
|
||||
OpenAPI URL을 커스터마이즈(또는 제거)하려면 [튜토리얼 - 메타데이터와 문서 URL](../tutorial/metadata.md#openapi-url){.internal-link target=_blank} 문서를 읽어보세요.
|
||||
|
||||
## OpenAPI 문서 URL { #openapi-docs-urls }
|
||||
|
||||
자동으로 생성되는 문서 사용자 인터페이스에서 사용하는 URL을 업데이트하려면 [튜토리얼 - 메타데이터와 문서 URL](../tutorial/metadata.md#docs-urls){.internal-link target=_blank} 문서를 읽어보세요.
|
||||
@@ -1,60 +0,0 @@
|
||||
# GraphQL { #graphql }
|
||||
|
||||
**FastAPI**는 **ASGI** 표준을 기반으로 하므로, ASGI와도 호환되는 어떤 **GraphQL** 라이브러리든 매우 쉽게 통합할 수 있습니다.
|
||||
|
||||
같은 애플리케이션에서 일반 FastAPI **경로 처리**와 GraphQL을 함께 조합할 수 있습니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
**GraphQL**은 몇 가지 매우 특정한 사용 사례를 해결합니다.
|
||||
|
||||
일반적인 **web API**와 비교했을 때 **장점**과 **단점**이 있습니다.
|
||||
|
||||
여러분의 사용 사례에서 **이점**이 **단점**을 상쇄하는지 꼭 평가해 보세요. 🤓
|
||||
|
||||
///
|
||||
|
||||
## GraphQL 라이브러리 { #graphql-libraries }
|
||||
|
||||
다음은 **ASGI** 지원이 있는 **GraphQL** 라이브러리들입니다. **FastAPI**와 함께 사용할 수 있습니다:
|
||||
|
||||
* <a href="https://strawberry.rocks/" class="external-link" target="_blank">Strawberry</a> 🍓
|
||||
* <a href="https://strawberry.rocks/docs/integrations/fastapi" class="external-link" target="_blank">FastAPI용 문서</a> 제공
|
||||
* <a href="https://ariadnegraphql.org/" class="external-link" target="_blank">Ariadne</a>
|
||||
* <a href="https://ariadnegraphql.org/docs/fastapi-integration" class="external-link" target="_blank">FastAPI용 문서</a> 제공
|
||||
* <a href="https://tartiflette.io/" class="external-link" target="_blank">Tartiflette</a>
|
||||
* ASGI 통합을 제공하기 위해 <a href="https://tartiflette.github.io/tartiflette-asgi/" class="external-link" target="_blank">Tartiflette ASGI</a> 사용
|
||||
* <a href="https://graphene-python.org/" class="external-link" target="_blank">Graphene</a>
|
||||
* <a href="https://github.com/ciscorn/starlette-graphene3" class="external-link" target="_blank">starlette-graphene3</a> 사용
|
||||
|
||||
## Strawberry로 GraphQL 사용하기 { #graphql-with-strawberry }
|
||||
|
||||
**GraphQL**로 작업해야 하거나 작업하고 싶다면, <a href="https://strawberry.rocks/" class="external-link" target="_blank">**Strawberry**</a>를 **권장**합니다. **FastAPI**의 설계와 가장 가깝고, 모든 것이 **type annotations**에 기반해 있기 때문입니다.
|
||||
|
||||
사용 사례에 따라 다른 라이브러리를 선호할 수도 있지만, 제게 묻는다면 아마 **Strawberry**를 먼저 시도해 보라고 제안할 것입니다.
|
||||
|
||||
다음은 Strawberry를 FastAPI와 통합하는 방법에 대한 간단한 미리보기입니다:
|
||||
|
||||
{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *}
|
||||
|
||||
<a href="https://strawberry.rocks/" class="external-link" target="_blank">Strawberry 문서</a>에서 Strawberry에 대해 더 알아볼 수 있습니다.
|
||||
|
||||
또한 <a href="https://strawberry.rocks/docs/integrations/fastapi" class="external-link" target="_blank">FastAPI에서 Strawberry 사용</a>에 대한 문서도 확인해 보세요.
|
||||
|
||||
## Starlette의 예전 `GraphQLApp` { #older-graphqlapp-from-starlette }
|
||||
|
||||
이전 버전의 Starlette에는 <a href="https://graphene-python.org/" class="external-link" target="_blank">Graphene</a>과 통합하기 위한 `GraphQLApp` 클래스가 포함되어 있었습니다.
|
||||
|
||||
이것은 Starlette에서 deprecated 되었지만, 이를 사용하던 코드가 있다면 같은 사용 사례를 다루고 **거의 동일한 인터페이스**를 가진 <a href="https://github.com/ciscorn/starlette-graphene3" class="external-link" target="_blank">starlette-graphene3</a>로 쉽게 **마이그레이션**할 수 있습니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
GraphQL이 필요하다면, 커스텀 클래스와 타입 대신 type annotations에 기반한 <a href="https://strawberry.rocks/" class="external-link" target="_blank">Strawberry</a>를 여전히 확인해 보시길 권장합니다.
|
||||
|
||||
///
|
||||
|
||||
## 더 알아보기 { #learn-more }
|
||||
|
||||
<a href="https://graphql.org/" class="external-link" target="_blank">공식 GraphQL 문서</a>에서 **GraphQL**에 대해 더 알아볼 수 있습니다.
|
||||
|
||||
또한 위에서 설명한 각 라이브러리에 대해서도 해당 링크에서 더 자세히 읽어볼 수 있습니다.
|
||||
@@ -1,13 +0,0 @@
|
||||
# How To - 레시피 { #how-to-recipes }
|
||||
|
||||
여기에서는 **여러 주제**에 대한 다양한 레시피(“how to” 가이드)를 볼 수 있습니다.
|
||||
|
||||
대부분의 아이디어는 어느 정도 **서로 독립적**이며, 대부분의 경우 **여러분의 프로젝트**에 직접 적용되는 경우에만 학습하면 됩니다.
|
||||
|
||||
프로젝트에 흥미롭고 유용해 보이는 것이 있다면 확인해 보세요. 그렇지 않다면 아마 건너뛰어도 됩니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
**FastAPI를 구조적으로 학습**하고 싶다면(권장), 대신 [튜토리얼 - 사용자 가이드](../tutorial/index.md){.internal-link target=_blank}를 장별로 읽어보세요.
|
||||
|
||||
///
|
||||
@@ -1,135 +0,0 @@
|
||||
# Pydantic v1에서 Pydantic v2로 마이그레이션하기 { #migrate-from-pydantic-v1-to-pydantic-v2 }
|
||||
|
||||
오래된 FastAPI 앱이 있다면 Pydantic 버전 1을 사용하고 있을 수 있습니다.
|
||||
|
||||
FastAPI 0.100.0 버전은 Pydantic v1 또는 v2 중 하나를 지원했습니다. 설치되어 있는 쪽을 사용했습니다.
|
||||
|
||||
FastAPI 0.119.0 버전에서는 v2로의 마이그레이션을 쉽게 하기 위해, Pydantic v2 내부에서 Pydantic v1을(`pydantic.v1`로) 부분적으로 지원하기 시작했습니다.
|
||||
|
||||
FastAPI 0.126.0 버전에서는 Pydantic v1 지원을 중단했지만, `pydantic.v1`은 잠시 동안 계속 지원했습니다.
|
||||
|
||||
/// warning | 경고
|
||||
|
||||
Pydantic 팀은 **Python 3.14**부터 최신 Python 버전에서 Pydantic v1 지원을 중단했습니다.
|
||||
|
||||
여기에는 `pydantic.v1`도 포함되며, Python 3.14 이상에서는 더 이상 지원되지 않습니다.
|
||||
|
||||
Python의 최신 기능을 사용하려면 Pydantic v2를 사용하고 있는지 확인해야 합니다.
|
||||
|
||||
///
|
||||
|
||||
Pydantic v1을 사용하는 오래된 FastAPI 앱이 있다면, 여기서는 이를 Pydantic v2로 마이그레이션하는 방법과 점진적 마이그레이션을 돕는 **FastAPI 0.119.0의 기능**을 소개하겠습니다.
|
||||
|
||||
## 공식 가이드 { #official-guide }
|
||||
|
||||
Pydantic에는 v1에서 v2로의 공식 <a href="https://docs.pydantic.dev/latest/migration/" class="external-link" target="_blank">Migration Guide</a>가 있습니다.
|
||||
|
||||
여기에는 무엇이 바뀌었는지, 검증이 이제 어떻게 더 정확하고 엄격해졌는지, 가능한 주의사항 등도 포함되어 있습니다.
|
||||
|
||||
변경된 내용을 더 잘 이해하기 위해 읽어보면 좋습니다.
|
||||
|
||||
## 테스트 { #tests }
|
||||
|
||||
앱에 대한 [tests](../tutorial/testing.md){.internal-link target=_blank}가 있는지 확인하고, 지속적 통합(CI)에서 테스트를 실행하세요.
|
||||
|
||||
이렇게 하면 업그레이드를 진행하면서도 모든 것이 기대한 대로 계속 동작하는지 확인할 수 있습니다.
|
||||
|
||||
## `bump-pydantic` { #bump-pydantic }
|
||||
|
||||
많은 경우, 커스터마이징 없이 일반적인 Pydantic 모델을 사용하고 있다면 Pydantic v1에서 Pydantic v2로의 마이그레이션 과정 대부분을 자동화할 수 있습니다.
|
||||
|
||||
같은 Pydantic 팀이 제공하는 <a href="https://github.com/pydantic/bump-pydantic" class="external-link" target="_blank">`bump-pydantic`</a>를 사용할 수 있습니다.
|
||||
|
||||
이 도구는 변경해야 하는 코드의 대부분을 자동으로 바꾸는 데 도움을 줍니다.
|
||||
|
||||
그 다음 테스트를 실행해서 모든 것이 동작하는지 확인하면 됩니다. 잘 된다면 끝입니다. 😎
|
||||
|
||||
## v2 안의 Pydantic v1 { #pydantic-v1-in-v2 }
|
||||
|
||||
Pydantic v2는 Pydantic v1의 모든 것을 서브모듈 `pydantic.v1`로 포함합니다. 하지만 이는 Python 3.13보다 높은 버전에서는 더 이상 지원되지 않습니다.
|
||||
|
||||
즉, Pydantic v2의 최신 버전을 설치한 뒤, 이 서브모듈에서 예전 Pydantic v1 구성 요소를 import하여 예전 Pydantic v1을 설치한 것처럼 사용할 수 있습니다.
|
||||
|
||||
{* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *}
|
||||
|
||||
### v2 안의 Pydantic v1에 대한 FastAPI 지원 { #fastapi-support-for-pydantic-v1-in-v2 }
|
||||
|
||||
FastAPI 0.119.0부터는 v2로의 마이그레이션을 쉽게 하기 위해, Pydantic v2 내부의 Pydantic v1에 대해서도 부분적인 지원이 있습니다.
|
||||
|
||||
따라서 Pydantic을 최신 v2로 업그레이드하고, import를 `pydantic.v1` 서브모듈을 사용하도록 바꾸면, 많은 경우 그대로 동작합니다.
|
||||
|
||||
{* ../../docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py hl[2,5,15] *}
|
||||
|
||||
/// warning | 경고
|
||||
|
||||
Pydantic 팀이 Python 3.14부터 최신 Python 버전에서 Pydantic v1을 더 이상 지원하지 않으므로, `pydantic.v1`을 사용하는 것 역시 Python 3.14 이상에서는 지원되지 않는다는 점을 염두에 두세요.
|
||||
|
||||
///
|
||||
|
||||
### 같은 앱에서 Pydantic v1과 v2 함께 사용하기 { #pydantic-v1-and-v2-on-the-same-app }
|
||||
|
||||
Pydantic에서는 Pydantic v2 모델의 필드를 Pydantic v1 모델로 정의하거나 그 반대로 하는 것을 **지원하지 않습니다**.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "❌ Not Supported"
|
||||
direction TB
|
||||
subgraph V2["Pydantic v2 Model"]
|
||||
V1Field["Pydantic v1 Model"]
|
||||
end
|
||||
subgraph V1["Pydantic v1 Model"]
|
||||
V2Field["Pydantic v2 Model"]
|
||||
end
|
||||
end
|
||||
|
||||
style V2 fill:#f9fff3
|
||||
style V1 fill:#fff6f0
|
||||
style V1Field fill:#fff6f0
|
||||
style V2Field fill:#f9fff3
|
||||
```
|
||||
|
||||
...하지만 같은 앱에서 Pydantic v1과 v2를 사용하되, 모델을 분리해서 둘 수는 있습니다.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "✅ Supported"
|
||||
direction TB
|
||||
subgraph V2["Pydantic v2 Model"]
|
||||
V2Field["Pydantic v2 Model"]
|
||||
end
|
||||
subgraph V1["Pydantic v1 Model"]
|
||||
V1Field["Pydantic v1 Model"]
|
||||
end
|
||||
end
|
||||
|
||||
style V2 fill:#f9fff3
|
||||
style V1 fill:#fff6f0
|
||||
style V1Field fill:#fff6f0
|
||||
style V2Field fill:#f9fff3
|
||||
```
|
||||
|
||||
어떤 경우에는 FastAPI 앱의 같은 **경로 처리**에서 Pydantic v1과 v2 모델을 함께 사용하는 것도 가능합니다:
|
||||
|
||||
{* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *}
|
||||
|
||||
위 예제에서 입력 모델은 Pydantic v1 모델이고, 출력 모델(`response_model=ItemV2`로 정의됨)은 Pydantic v2 모델입니다.
|
||||
|
||||
### Pydantic v1 파라미터 { #pydantic-v1-parameters }
|
||||
|
||||
Pydantic v1 모델과 함께 `Body`, `Query`, `Form` 등 파라미터용 FastAPI 전용 도구 일부를 사용해야 한다면, Pydantic v2로의 마이그레이션을 마칠 때까지 `fastapi.temp_pydantic_v1_params`에서 import할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py hl[4,18] *}
|
||||
|
||||
### 단계적으로 마이그레이션하기 { #migrate-in-steps }
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
먼저 `bump-pydantic`로 시도해 보세요. 테스트가 통과하고 잘 동작한다면, 한 번의 명령으로 끝입니다. ✨
|
||||
|
||||
///
|
||||
|
||||
`bump-pydantic`가 여러분의 사용 사례에 맞지 않는다면, 같은 앱에서 Pydantic v1과 v2 모델을 모두 지원하는 기능을 이용해 Pydantic v2로 점진적으로 마이그레이션할 수 있습니다.
|
||||
|
||||
먼저 Pydantic을 최신 v2로 업그레이드하고, 모든 모델의 import를 `pydantic.v1`을 사용하도록 바꿀 수 있습니다.
|
||||
|
||||
그 다음 Pydantic v1에서 v2로 모델을 그룹 단위로, 점진적인 단계로 마이그레이션을 시작하면 됩니다. 🚶
|
||||
@@ -1,102 +0,0 @@
|
||||
# 입력과 출력에 대해 OpenAPI 스키마를 분리할지 여부 { #separate-openapi-schemas-for-input-and-output-or-not }
|
||||
|
||||
**Pydantic v2**가 릴리스된 이후, 생성되는 OpenAPI는 이전보다 조금 더 정확하고 **올바르게** 만들어집니다. 😎
|
||||
|
||||
실제로 어떤 경우에는, 같은 Pydantic 모델에 대해 OpenAPI 안에 **두 개의 JSON Schema**가 생기기도 합니다. **기본값(default value)**이 있는지 여부에 따라, 입력용과 출력용으로 나뉩니다.
|
||||
|
||||
이것이 어떻게 동작하는지, 그리고 필요하다면 어떻게 변경할 수 있는지 살펴보겠습니다.
|
||||
|
||||
## 입력과 출력을 위한 Pydantic 모델 { #pydantic-models-for-input-and-output }
|
||||
|
||||
예를 들어, 다음처럼 기본값이 있는 Pydantic 모델이 있다고 해보겠습니다:
|
||||
|
||||
{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py ln[1:7] hl[7] *}
|
||||
|
||||
### 입력용 모델 { #model-for-input }
|
||||
|
||||
이 모델을 다음처럼 입력으로 사용하면:
|
||||
|
||||
{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py ln[1:15] hl[14] *}
|
||||
|
||||
...`description` 필드는 **필수가 아닙니다**. `None`이라는 기본값이 있기 때문입니다.
|
||||
|
||||
### 문서에서의 입력 모델 { #input-model-in-docs }
|
||||
|
||||
문서에서 `description` 필드에 **빨간 별표**가 없고, 필수로 표시되지 않는 것을 확인할 수 있습니다:
|
||||
|
||||
<div class="screenshot">
|
||||
<img src="/img/tutorial/separate-openapi-schemas/image01.png">
|
||||
</div>
|
||||
|
||||
### 출력용 모델 { #model-for-output }
|
||||
|
||||
하지만 같은 모델을 다음처럼 출력으로 사용하면:
|
||||
|
||||
{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py hl[19] *}
|
||||
|
||||
...`description`에 기본값이 있기 때문에, 그 필드에 대해 **아무것도 반환하지 않더라도** 여전히 그 **기본값**이 들어가게 됩니다.
|
||||
|
||||
### 출력 응답 데이터용 모델 { #model-for-output-response-data }
|
||||
|
||||
문서에서 직접 동작시켜 응답을 확인해 보면, 코드가 `description` 필드 중 하나에 아무것도 추가하지 않았더라도 JSON 응답에는 기본값(`null`)이 포함되어 있습니다:
|
||||
|
||||
<div class="screenshot">
|
||||
<img src="/img/tutorial/separate-openapi-schemas/image02.png">
|
||||
</div>
|
||||
|
||||
이는 해당 필드가 **항상 값을 가진다는 것**을 의미합니다. 다만 그 값이 때로는 `None`(JSON에서는 `null`)일 수 있습니다.
|
||||
|
||||
즉, API를 사용하는 클라이언트는 값이 존재하는지 여부를 확인할 필요가 없고, **필드가 항상 존재한다고 가정**할 수 있습니다. 다만 어떤 경우에는 기본값 `None`이 들어갑니다.
|
||||
|
||||
이를 OpenAPI에서 표현하는 방법은, 그 필드를 **required**로 표시하는 것입니다. 항상 존재하기 때문입니다.
|
||||
|
||||
이 때문에, 하나의 모델이라도 **입력용인지 출력용인지**에 따라 JSON Schema가 달라질 수 있습니다:
|
||||
|
||||
* **입력**에서는 `description`이 **필수가 아님**
|
||||
* **출력**에서는 **필수임** (그리고 값은 `None`일 수도 있으며, JSON 용어로는 `null`)
|
||||
|
||||
### 문서에서의 출력용 모델 { #model-for-output-in-docs }
|
||||
|
||||
문서에서 출력 모델을 확인해 보면, `name`과 `description` **둘 다** **빨간 별표**로 **필수**로 표시되어 있습니다:
|
||||
|
||||
<div class="screenshot">
|
||||
<img src="/img/tutorial/separate-openapi-schemas/image03.png">
|
||||
</div>
|
||||
|
||||
### 문서에서의 입력과 출력 모델 { #model-for-input-and-output-in-docs }
|
||||
|
||||
또 OpenAPI에서 사용 가능한 모든 Schemas(JSON Schemas)를 확인해 보면, `Item-Input` 하나와 `Item-Output` 하나, 이렇게 두 개가 있는 것을 볼 수 있습니다.
|
||||
|
||||
`Item-Input`에서는 `description`이 **필수가 아니며**, 빨간 별표가 없습니다.
|
||||
|
||||
하지만 `Item-Output`에서는 `description`이 **필수이며**, 빨간 별표가 있습니다.
|
||||
|
||||
<div class="screenshot">
|
||||
<img src="/img/tutorial/separate-openapi-schemas/image04.png">
|
||||
</div>
|
||||
|
||||
**Pydantic v2**의 이 기능 덕분에 API 문서는 더 **정밀**해지고, 자동 생성된 클라이언트와 SDK가 있다면 그것들도 더 정밀해져서 더 나은 **developer experience**와 일관성을 제공할 수 있습니다. 🎉
|
||||
|
||||
## 스키마를 분리하지 않기 { #do-not-separate-schemas }
|
||||
|
||||
이제 어떤 경우에는 **입력과 출력에 대해 같은 스키마를 사용**하고 싶을 수도 있습니다.
|
||||
|
||||
가장 대표적인 경우는, 이미 자동 생성된 클라이언트 코드/SDK가 있고, 아직은 그 자동 생성된 클라이언트 코드/SDK들을 전부 업데이트하고 싶지 않은 경우입니다. 언젠가는 업데이트해야 할 가능성이 높지만, 지금 당장은 아닐 수도 있습니다.
|
||||
|
||||
그런 경우에는, **FastAPI**에서 `separate_input_output_schemas=False` 파라미터로 이 기능을 비활성화할 수 있습니다.
|
||||
|
||||
/// info | 정보
|
||||
|
||||
`separate_input_output_schemas` 지원은 FastAPI `0.102.0`에 추가되었습니다. 🤓
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/separate_openapi_schemas/tutorial002_py310.py hl[10] *}
|
||||
|
||||
### 문서에서 입력과 출력 모델에 같은 스키마 사용 { #same-schema-for-input-and-output-models-in-docs }
|
||||
|
||||
이제 모델에 대해 입력과 출력 모두에 사용되는 단일 스키마(오직 `Item`만)가 생성되며, `description`은 **필수가 아닌 것**으로 표시됩니다:
|
||||
|
||||
<div class="screenshot">
|
||||
<img src="/img/tutorial/separate-openapi-schemas/image05.png">
|
||||
</div>
|
||||
@@ -1,7 +0,0 @@
|
||||
# 데이터베이스 테스트하기 { #testing-a-database }
|
||||
|
||||
데이터베이스, SQL, SQLModel에 대해서는 <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel 문서</a>에서 학습할 수 있습니다. 🤓
|
||||
|
||||
<a href="https://sqlmodel.tiangolo.com/tutorial/fastapi/" class="external-link" target="_blank">FastAPI에서 SQLModel을 사용하는 방법에 대한 미니 튜토리얼</a>도 있습니다. ✨
|
||||
|
||||
해당 튜토리얼에는 <a href="https://sqlmodel.tiangolo.com/tutorial/fastapi/tests/" class="external-link" target="_blank">SQL 데이터베이스 테스트</a>에 대한 섹션도 포함되어 있습니다. 😎
|
||||
@@ -1,504 +0,0 @@
|
||||
# 더 큰 애플리케이션 - 여러 파일 { #bigger-applications-multiple-files }
|
||||
|
||||
애플리케이션이나 웹 API를 만들 때, 모든 것을 하나의 파일에 담을 수 있는 경우는 드뭅니다.
|
||||
|
||||
**FastAPI**는 모든 유연성을 유지하면서도 애플리케이션을 구조화할 수 있게 해주는 편리한 도구를 제공합니다.
|
||||
|
||||
/// info | 정보
|
||||
|
||||
Flask를 사용해 보셨다면, 이는 Flask의 Blueprints에 해당하는 개념입니다.
|
||||
|
||||
///
|
||||
|
||||
## 예시 파일 구조 { #an-example-file-structure }
|
||||
|
||||
다음과 같은 파일 구조가 있다고 해봅시다:
|
||||
|
||||
```
|
||||
.
|
||||
├── app
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py
|
||||
│ ├── dependencies.py
|
||||
│ └── routers
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── items.py
|
||||
│ │ └── users.py
|
||||
│ └── internal
|
||||
│ ├── __init__.py
|
||||
│ └── admin.py
|
||||
```
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
`__init__.py` 파일이 여러 개 있습니다: 각 디렉터리 또는 하위 디렉터리에 하나씩 있습니다.
|
||||
|
||||
이 파일들이 한 파일의 코드를 다른 파일로 import할 수 있게 해줍니다.
|
||||
|
||||
예를 들어 `app/main.py`에는 다음과 같은 줄이 있을 수 있습니다:
|
||||
|
||||
```
|
||||
from app.routers import items
|
||||
```
|
||||
|
||||
///
|
||||
|
||||
* `app` 디렉터리에는 모든 것이 들어 있습니다. 그리고 비어 있는 파일 `app/__init__.py`가 있어 "Python package"(“Python modules”의 모음)인 `app`이 됩니다.
|
||||
* `app/main.py` 파일이 있습니다. Python package(`__init__.py` 파일이 있는 디렉터리) 안에 있으므로, 이 package의 "module"입니다: `app.main`.
|
||||
* `app/dependencies.py` 파일도 있습니다. `app/main.py`와 마찬가지로 "module"입니다: `app.dependencies`.
|
||||
* `app/routers/` 하위 디렉터리가 있고, 여기에 또 `__init__.py` 파일이 있으므로 "Python subpackage"입니다: `app.routers`.
|
||||
* `app/routers/items.py` 파일은 `app/routers/` package 안에 있으므로, submodule입니다: `app.routers.items`.
|
||||
* `app/routers/users.py`도 동일하게 또 다른 submodule입니다: `app.routers.users`.
|
||||
* `app/internal/` 하위 디렉터리도 있고 여기에 `__init__.py`가 있으므로 또 다른 "Python subpackage"입니다: `app.internal`.
|
||||
* 그리고 `app/internal/admin.py` 파일은 또 다른 submodule입니다: `app.internal.admin`.
|
||||
|
||||
<img src="/img/tutorial/bigger-applications/package.drawio.svg">
|
||||
|
||||
같은 파일 구조에 주석을 추가하면 다음과 같습니다:
|
||||
|
||||
```bash
|
||||
.
|
||||
├── app # "app" is a Python package
|
||||
│ ├── __init__.py # this file makes "app" a "Python package"
|
||||
│ ├── main.py # "main" module, e.g. import app.main
|
||||
│ ├── dependencies.py # "dependencies" module, e.g. import app.dependencies
|
||||
│ └── routers # "routers" is a "Python subpackage"
|
||||
│ │ ├── __init__.py # makes "routers" a "Python subpackage"
|
||||
│ │ ├── items.py # "items" submodule, e.g. import app.routers.items
|
||||
│ │ └── users.py # "users" submodule, e.g. import app.routers.users
|
||||
│ └── internal # "internal" is a "Python subpackage"
|
||||
│ ├── __init__.py # makes "internal" a "Python subpackage"
|
||||
│ └── admin.py # "admin" submodule, e.g. import app.internal.admin
|
||||
```
|
||||
|
||||
## `APIRouter` { #apirouter }
|
||||
|
||||
사용자만 처리하는 전용 파일이 `/app/routers/users.py`의 submodule이라고 해봅시다.
|
||||
|
||||
코드를 정리하기 위해 사용자와 관련된 *path operations*를 나머지 코드와 분리해 두고 싶을 것입니다.
|
||||
|
||||
하지만 이것은 여전히 같은 **FastAPI** 애플리케이션/웹 API의 일부입니다(같은 "Python Package"의 일부입니다).
|
||||
|
||||
`APIRouter`를 사용해 해당 모듈의 *path operations*를 만들 수 있습니다.
|
||||
|
||||
### `APIRouter` import하기 { #import-apirouter }
|
||||
|
||||
`FastAPI` 클래스와 동일한 방식으로 import하고 "instance"를 생성합니다:
|
||||
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *}
|
||||
|
||||
### `APIRouter`로 *path operations* 만들기 { #path-operations-with-apirouter }
|
||||
|
||||
그 다음 이를 사용해 *path operations*를 선언합니다.
|
||||
|
||||
`FastAPI` 클래스를 사용할 때와 동일한 방식으로 사용합니다:
|
||||
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *}
|
||||
|
||||
`APIRouter`는 "미니 `FastAPI`" 클래스라고 생각할 수 있습니다.
|
||||
|
||||
동일한 옵션들이 모두 지원됩니다.
|
||||
|
||||
동일한 `parameters`, `responses`, `dependencies`, `tags` 등등.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
이 예시에서는 변수 이름이 `router`이지만, 원하는 이름으로 지어도 됩니다.
|
||||
|
||||
///
|
||||
|
||||
이제 이 `APIRouter`를 메인 `FastAPI` 앱에 포함(include)할 것이지만, 먼저 dependencies와 다른 `APIRouter` 하나를 확인해 보겠습니다.
|
||||
|
||||
## Dependencies { #dependencies }
|
||||
|
||||
애플리케이션의 여러 위치에서 사용되는 dependencies가 일부 필요하다는 것을 알 수 있습니다.
|
||||
|
||||
그래서 이를 별도의 `dependencies` 모듈(`app/dependencies.py`)에 둡니다.
|
||||
|
||||
이제 간단한 dependency를 사용해 커스텀 `X-Token` 헤더를 읽어 보겠습니다:
|
||||
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *}
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
이 예시를 단순화하기 위해 임의로 만든 헤더를 사용하고 있습니다.
|
||||
|
||||
하지만 실제 상황에서는 통합된 [Security 유틸리티](security/index.md){.internal-link target=_blank}를 사용하는 것이 더 좋은 결과를 얻을 수 있습니다.
|
||||
|
||||
///
|
||||
|
||||
## `APIRouter`가 있는 또 다른 모듈 { #another-module-with-apirouter }
|
||||
|
||||
애플리케이션의 "items"를 처리하는 전용 endpoint들도 `app/routers/items.py` 모듈에 있다고 해봅시다.
|
||||
|
||||
여기에는 다음에 대한 *path operations*가 있습니다:
|
||||
|
||||
* `/items/`
|
||||
* `/items/{item_id}`
|
||||
|
||||
구조는 `app/routers/users.py`와 완전히 동일합니다.
|
||||
|
||||
하지만 우리는 조금 더 똑똑하게, 코드를 약간 단순화하고 싶습니다.
|
||||
|
||||
이 모듈의 모든 *path operations*에는 다음이 동일하게 적용됩니다:
|
||||
|
||||
* 경로 `prefix`: `/items`.
|
||||
* `tags`: (태그 하나: `items`).
|
||||
* 추가 `responses`.
|
||||
* `dependencies`: 모두 우리가 만든 `X-Token` dependency가 필요합니다.
|
||||
|
||||
따라서 각 *path operation*마다 매번 모두 추가하는 대신, `APIRouter`에 한 번에 추가할 수 있습니다.
|
||||
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *}
|
||||
|
||||
각 *path operation*의 경로는 다음처럼 `/`로 시작해야 하므로:
|
||||
|
||||
```Python hl_lines="1"
|
||||
@router.get("/{item_id}")
|
||||
async def read_item(item_id: str):
|
||||
...
|
||||
```
|
||||
|
||||
...prefix에는 마지막 `/`가 포함되면 안 됩니다.
|
||||
|
||||
따라서 이 경우 prefix는 `/items`입니다.
|
||||
|
||||
또한 이 router에 포함된 모든 *path operations*에 적용될 `tags` 목록과 추가 `responses`도 넣을 수 있습니다.
|
||||
|
||||
그리고 router의 모든 *path operations*에 추가될 `dependencies` 목록도 추가할 수 있으며, 해당 경로들로 들어오는 각 요청마다 실행/해결됩니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
[*path operation decorator의 dependencies*](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}와 마찬가지로, *path operation function*에 어떤 값도 전달되지 않습니다.
|
||||
|
||||
///
|
||||
|
||||
최종적으로 item 경로는 다음과 같습니다:
|
||||
|
||||
* `/items/`
|
||||
* `/items/{item_id}`
|
||||
|
||||
...의도한 그대로입니다.
|
||||
|
||||
* 단일 문자열 `"items"`를 포함하는 태그 목록으로 표시됩니다.
|
||||
* 이 "tags"는 자동 대화형 문서 시스템(OpenAPI 사용)에 특히 유용합니다.
|
||||
* 모두 미리 정의된 `responses`를 포함합니다.
|
||||
* 이 모든 *path operations*는 실행되기 전에 `dependencies` 목록이 평가/실행됩니다.
|
||||
* 특정 *path operation*에 dependencies를 추가로 선언하면 **그것들도 실행됩니다**.
|
||||
* router dependencies가 먼저 실행되고, 그 다음에 [decorator의 `dependencies`](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, 그리고 일반 파라미터 dependencies가 실행됩니다.
|
||||
* [`scopes`가 있는 `Security` dependencies](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}도 추가할 수 있습니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
`APIRouter`에 `dependencies`를 두는 것은 예를 들어 전체 *path operations* 그룹에 인증을 요구할 때 사용할 수 있습니다. 각 경로 처리에 개별적으로 dependencies를 추가하지 않아도 됩니다.
|
||||
|
||||
///
|
||||
|
||||
/// check | 확인
|
||||
|
||||
`prefix`, `tags`, `responses`, `dependencies` 파라미터는 (다른 많은 경우와 마찬가지로) 코드 중복을 피하도록 도와주는 **FastAPI**의 기능입니다.
|
||||
|
||||
///
|
||||
|
||||
### dependencies import하기 { #import-the-dependencies }
|
||||
|
||||
이 코드는 모듈 `app.routers.items`, 파일 `app/routers/items.py`에 있습니다.
|
||||
|
||||
그리고 dependency 함수는 모듈 `app.dependencies`, 파일 `app/dependencies.py`에서 가져와야 합니다.
|
||||
|
||||
그래서 dependencies에 대해 `..`를 사용하는 상대 import를 사용합니다:
|
||||
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *}
|
||||
|
||||
#### 상대 import가 동작하는 방식 { #how-relative-imports-work }
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
import가 동작하는 방식을 완벽히 알고 있다면, 아래 다음 섹션으로 넘어가세요.
|
||||
|
||||
///
|
||||
|
||||
다음과 같이 점 하나 `.`를 쓰면:
|
||||
|
||||
```Python
|
||||
from .dependencies import get_token_header
|
||||
```
|
||||
|
||||
의미는 다음과 같습니다:
|
||||
|
||||
* 이 모듈(파일 `app/routers/items.py`)이 속한 같은 package(디렉터리 `app/routers/`)에서 시작해서...
|
||||
* `dependencies` 모듈(가상의 파일 `app/routers/dependencies.py`)을 찾고...
|
||||
* 그 안에서 함수 `get_token_header`를 import합니다.
|
||||
|
||||
하지만 그 파일은 존재하지 않습니다. dependencies는 `app/dependencies.py` 파일에 있습니다.
|
||||
|
||||
우리 앱/파일 구조를 다시 떠올려 보세요:
|
||||
|
||||
<img src="/img/tutorial/bigger-applications/package.drawio.svg">
|
||||
|
||||
---
|
||||
|
||||
다음처럼 점 두 개 `..`를 쓰면:
|
||||
|
||||
```Python
|
||||
from ..dependencies import get_token_header
|
||||
```
|
||||
|
||||
의미는 다음과 같습니다:
|
||||
|
||||
* 이 모듈(파일 `app/routers/items.py`)이 속한 같은 package(디렉터리 `app/routers/`)에서 시작해서...
|
||||
* 상위 package(디렉터리 `app/`)로 올라가고...
|
||||
* 그 안에서 `dependencies` 모듈(파일 `app/dependencies.py`)을 찾고...
|
||||
* 그 안에서 함수 `get_token_header`를 import합니다.
|
||||
|
||||
이렇게 하면 제대로 동작합니다! 🎉
|
||||
|
||||
---
|
||||
|
||||
같은 방식으로 점 세 개 `...`를 사용했다면:
|
||||
|
||||
```Python
|
||||
from ...dependencies import get_token_header
|
||||
```
|
||||
|
||||
의미는 다음과 같습니다:
|
||||
|
||||
* 이 모듈(파일 `app/routers/items.py`)이 속한 같은 package(디렉터리 `app/routers/`)에서 시작해서...
|
||||
* 상위 package(디렉터리 `app/`)로 올라가고...
|
||||
* 그 package의 상위로 또 올라가는데(상위 package가 없습니다, `app`이 최상위입니다 😱)...
|
||||
* 그 안에서 `dependencies` 모듈(파일 `app/dependencies.py`)을 찾고...
|
||||
* 그 안에서 함수 `get_token_header`를 import합니다.
|
||||
|
||||
이는 `app/` 위쪽의 어떤 package(자신의 `__init__.py` 파일 등을 가진)에 대한 참조가 됩니다. 하지만 우리는 그런 것이 없습니다. 그래서 이 예시에서는 에러가 발생합니다. 🚨
|
||||
|
||||
이제 어떻게 동작하는지 알았으니, 앱이 얼마나 복잡하든 상대 import를 사용할 수 있습니다. 🤓
|
||||
|
||||
### 커스텀 `tags`, `responses`, `dependencies` 추가하기 { #add-some-custom-tags-responses-and-dependencies }
|
||||
|
||||
`APIRouter`에 이미 prefix `/items`와 `tags=["items"]`를 추가했기 때문에 각 *path operation*에 이를 추가하지 않습니다.
|
||||
|
||||
하지만 특정 *path operation*에만 적용될 _추가_ `tags`를 더할 수도 있고, 그 *path operation* 전용의 추가 `responses`도 넣을 수 있습니다:
|
||||
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *}
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
이 마지막 경로 처리는 `["items", "custom"]` 태그 조합을 갖게 됩니다.
|
||||
|
||||
그리고 문서에는 `404`용 응답과 `403`용 응답, 두 가지 모두가 표시됩니다.
|
||||
|
||||
///
|
||||
|
||||
## 메인 `FastAPI` { #the-main-fastapi }
|
||||
|
||||
이제 `app/main.py` 모듈을 봅시다.
|
||||
|
||||
여기에서 `FastAPI` 클래스를 import하고 사용합니다.
|
||||
|
||||
이 파일은 모든 것을 하나로 엮는 애플리케이션의 메인 파일이 될 것입니다.
|
||||
|
||||
그리고 대부분의 로직이 각자의 특정 모듈로 분리되어 있으므로, 메인 파일은 꽤 단순해집니다.
|
||||
|
||||
### `FastAPI` import하기 { #import-fastapi }
|
||||
|
||||
평소처럼 `FastAPI` 클래스를 import하고 생성합니다.
|
||||
|
||||
또한 각 `APIRouter`의 dependencies와 결합될 [global dependencies](dependencies/global-dependencies.md){.internal-link target=_blank}도 선언할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *}
|
||||
|
||||
### `APIRouter` import하기 { #import-the-apirouter }
|
||||
|
||||
이제 `APIRouter`가 있는 다른 submodule들을 import합니다:
|
||||
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *}
|
||||
|
||||
`app/routers/users.py`와 `app/routers/items.py` 파일은 같은 Python package `app`에 속한 submodule들이므로, 점 하나 `.`를 사용해 "상대 import"로 가져올 수 있습니다.
|
||||
|
||||
### import가 동작하는 방식 { #how-the-importing-works }
|
||||
|
||||
다음 구문은:
|
||||
|
||||
```Python
|
||||
from .routers import items, users
|
||||
```
|
||||
|
||||
의미는 다음과 같습니다:
|
||||
|
||||
* 이 모듈(파일 `app/main.py`)이 속한 같은 package(디렉터리 `app/`)에서 시작해서...
|
||||
* subpackage `routers`(디렉터리 `app/routers/`)를 찾고...
|
||||
* 그 안에서 submodule `items`(파일 `app/routers/items.py`)와 `users`(파일 `app/routers/users.py`)를 import합니다...
|
||||
|
||||
`items` 모듈에는 `router` 변수(`items.router`)가 있습니다. 이는 `app/routers/items.py` 파일에서 만든 것과 동일하며 `APIRouter` 객체입니다.
|
||||
|
||||
그리고 `users` 모듈도 같은 방식입니다.
|
||||
|
||||
다음처럼 import할 수도 있습니다:
|
||||
|
||||
```Python
|
||||
from app.routers import items, users
|
||||
```
|
||||
|
||||
/// info | 정보
|
||||
|
||||
첫 번째 버전은 "상대 import"입니다:
|
||||
|
||||
```Python
|
||||
from .routers import items, users
|
||||
```
|
||||
|
||||
두 번째 버전은 "절대 import"입니다:
|
||||
|
||||
```Python
|
||||
from app.routers import items, users
|
||||
```
|
||||
|
||||
Python Packages와 Modules에 대해 더 알아보려면 <a href="https://docs.python.org/3/tutorial/modules.html" class="external-link" target="_blank">Modules에 대한 Python 공식 문서</a>를 읽어보세요.
|
||||
|
||||
///
|
||||
|
||||
### 이름 충돌 피하기 { #avoid-name-collisions }
|
||||
|
||||
submodule `items`를 직접 import하고, 그 안의 `router` 변수만 import하지는 않습니다.
|
||||
|
||||
이는 submodule `users`에도 `router`라는 이름의 변수가 있기 때문입니다.
|
||||
|
||||
만약 다음처럼 순서대로 import했다면:
|
||||
|
||||
```Python
|
||||
from .routers.items import router
|
||||
from .routers.users import router
|
||||
```
|
||||
|
||||
`users`의 `router`가 `items`의 `router`를 덮어써서 동시에 사용할 수 없게 됩니다.
|
||||
|
||||
따라서 같은 파일에서 둘 다 사용할 수 있도록 submodule들을 직접 import합니다:
|
||||
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *}
|
||||
|
||||
### `users`와 `items`용 `APIRouter` 포함하기 { #include-the-apirouters-for-users-and-items }
|
||||
|
||||
이제 submodule `users`와 `items`의 `router`를 포함해 봅시다:
|
||||
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *}
|
||||
|
||||
/// info | 정보
|
||||
|
||||
`users.router`는 `app/routers/users.py` 파일 안의 `APIRouter`를 담고 있습니다.
|
||||
|
||||
`items.router`는 `app/routers/items.py` 파일 안의 `APIRouter`를 담고 있습니다.
|
||||
|
||||
///
|
||||
|
||||
`app.include_router()`로 각 `APIRouter`를 메인 `FastAPI` 애플리케이션에 추가할 수 있습니다.
|
||||
|
||||
그 router의 모든 route가 애플리케이션의 일부로 포함됩니다.
|
||||
|
||||
/// note Technical Details | 기술 세부사항
|
||||
|
||||
내부적으로는 `APIRouter`에 선언된 각 *path operation*마다 *path operation*을 실제로 생성합니다.
|
||||
|
||||
즉, 내부적으로는 모든 것이 동일한 하나의 앱인 것처럼 동작합니다.
|
||||
|
||||
///
|
||||
|
||||
/// check | 확인
|
||||
|
||||
router를 포함(include)할 때 성능을 걱정할 필요는 없습니다.
|
||||
|
||||
이 작업은 마이크로초 단위이며 시작 시에만 발생합니다.
|
||||
|
||||
따라서 성능에 영향을 주지 않습니다. ⚡
|
||||
|
||||
///
|
||||
|
||||
### 커스텀 `prefix`, `tags`, `responses`, `dependencies`로 `APIRouter` 포함하기 { #include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies }
|
||||
|
||||
이제 조직에서 `app/internal/admin.py` 파일을 받았다고 가정해 봅시다.
|
||||
|
||||
여기에는 조직에서 여러 프로젝트 간에 공유하는 관리자용 *path operations*가 있는 `APIRouter`가 들어 있습니다.
|
||||
|
||||
이 예시에서는 매우 단순하게 만들겠습니다. 하지만 조직 내 다른 프로젝트와 공유되기 때문에, 이를 수정할 수 없어 `prefix`, `dependencies`, `tags` 등을 `APIRouter`에 직접 추가할 수 없다고 해봅시다:
|
||||
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *}
|
||||
|
||||
하지만 `APIRouter`를 포함할 때 커스텀 `prefix`를 지정해 모든 *path operations*가 `/admin`으로 시작하게 하고, 이 프로젝트에서 이미 가진 `dependencies`로 보호하고, `tags`와 `responses`도 포함하고 싶습니다.
|
||||
|
||||
원래 `APIRouter`를 수정하지 않고도 `app.include_router()`에 파라미터를 전달해서 이를 선언할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *}
|
||||
|
||||
이렇게 하면 원래 `APIRouter`는 수정되지 않으므로, 조직 내 다른 프로젝트에서도 동일한 `app/internal/admin.py` 파일을 계속 공유할 수 있습니다.
|
||||
|
||||
결과적으로 우리 앱에서 `admin` 모듈의 각 *path operations*는 다음을 갖게 됩니다:
|
||||
|
||||
* prefix `/admin`.
|
||||
* tag `admin`.
|
||||
* dependency `get_token_header`.
|
||||
* 응답 `418`. 🍵
|
||||
|
||||
하지만 이는 우리 앱에서 그 `APIRouter`에만 영향을 주며, 이를 사용하는 다른 코드에는 영향을 주지 않습니다.
|
||||
|
||||
따라서 다른 프로젝트들은 같은 `APIRouter`를 다른 인증 방식으로 사용할 수도 있습니다.
|
||||
|
||||
### *path operation* 포함하기 { #include-a-path-operation }
|
||||
|
||||
*path operations*를 `FastAPI` 앱에 직접 추가할 수도 있습니다.
|
||||
|
||||
여기서는 가능하다는 것을 보여주기 위해... 그냥 해봅니다 🤷:
|
||||
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[21:23] title["app/main.py"] *}
|
||||
|
||||
그리고 `app.include_router()`로 추가한 다른 모든 *path operations*와 함께 올바르게 동작합니다.
|
||||
|
||||
/// info | 정보
|
||||
|
||||
**참고**: 이는 매우 기술적인 세부사항이라 아마 **그냥 건너뛰어도 됩니다**.
|
||||
|
||||
---
|
||||
|
||||
`APIRouter`는 "mount"되는 것이 아니며, 애플리케이션의 나머지 부분과 격리되어 있지 않습니다.
|
||||
|
||||
이는 OpenAPI 스키마와 사용자 인터페이스에 그들의 *path operations*를 포함시키고 싶기 때문입니다.
|
||||
|
||||
나머지와 독립적으로 격리해 "mount"할 수 없으므로, *path operations*는 직접 포함되는 것이 아니라 "clone"(재생성)됩니다.
|
||||
|
||||
///
|
||||
|
||||
## 자동 API 문서 확인하기 { #check-the-automatic-api-docs }
|
||||
|
||||
이제 앱을 실행하세요:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi dev app/main.py
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
그리고 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>에서 문서를 여세요.
|
||||
|
||||
올바른 경로(및 prefix)와 올바른 태그를 사용해, 모든 submodule의 경로를 포함한 자동 API 문서를 볼 수 있습니다:
|
||||
|
||||
<img src="/img/tutorial/bigger-applications/image01.png">
|
||||
|
||||
## 같은 router를 다른 `prefix`로 여러 번 포함하기 { #include-the-same-router-multiple-times-with-different-prefix }
|
||||
|
||||
`.include_router()`를 사용해 *같은* router를 서로 다른 prefix로 여러 번 포함할 수도 있습니다.
|
||||
|
||||
예를 들어 `/api/v1`과 `/api/latest`처럼 서로 다른 prefix로 동일한 API를 노출할 때 유용할 수 있습니다.
|
||||
|
||||
이는 고급 사용 방식이라 실제로 필요하지 않을 수도 있지만, 필요할 때를 위해 제공됩니다.
|
||||
|
||||
## `APIRouter`에 다른 `APIRouter` 포함하기 { #include-an-apirouter-in-another }
|
||||
|
||||
`APIRouter`를 `FastAPI` 애플리케이션에 포함할 수 있는 것과 같은 방식으로, 다음을 사용해 `APIRouter`를 다른 `APIRouter`에 포함할 수 있습니다:
|
||||
|
||||
```Python
|
||||
router.include_router(other_router)
|
||||
```
|
||||
|
||||
`FastAPI` 앱에 `router`를 포함하기 전에 수행해야 하며, 그래야 `other_router`의 *path operations*도 함께 포함됩니다.
|
||||
@@ -1,100 +0,0 @@
|
||||
# Body - 업데이트 { #body-updates }
|
||||
|
||||
## `PUT`으로 교체 업데이트하기 { #update-replacing-with-put }
|
||||
|
||||
항목을 업데이트하려면 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT" class="external-link" target="_blank">HTTP `PUT`</a> 작업을 사용할 수 있습니다.
|
||||
|
||||
`jsonable_encoder`를 사용해 입력 데이터를 JSON으로 저장할 수 있는 데이터로 변환할 수 있습니다(예: NoSQL 데이터베이스 사용 시). 예를 들어 `datetime`을 `str`로 변환하는 경우입니다.
|
||||
|
||||
{* ../../docs_src/body_updates/tutorial001_py310.py hl[28:33] *}
|
||||
|
||||
`PUT`은 기존 데이터를 **대체**해야 하는 데이터를 받는 데 사용합니다.
|
||||
|
||||
### 대체 시 주의사항 { #warning-about-replacing }
|
||||
|
||||
즉, `PUT`으로 항목 `bar`를 업데이트하면서 다음과 같은 body를 보낸다면:
|
||||
|
||||
```Python
|
||||
{
|
||||
"name": "Barz",
|
||||
"price": 3,
|
||||
"description": None,
|
||||
}
|
||||
```
|
||||
|
||||
이미 저장된 속성 `"tax": 20.2`가 포함되어 있지 않기 때문에, 입력 모델은 `"tax": 10.5`라는 기본값을 사용하게 됩니다.
|
||||
|
||||
그리고 데이터는 그 “새로운” `tax` 값 `10.5`로 저장됩니다.
|
||||
|
||||
## `PATCH`로 부분 업데이트하기 { #partial-updates-with-patch }
|
||||
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH" class="external-link" target="_blank">HTTP `PATCH`</a> 작업을 사용해 데이터를 *부분적으로* 업데이트할 수도 있습니다.
|
||||
|
||||
이는 업데이트하려는 데이터만 보내고, 나머지는 그대로 두는 것을 의미합니다.
|
||||
|
||||
/// note | 참고
|
||||
|
||||
`PATCH`는 `PUT`보다 덜 일반적으로 사용되고 덜 알려져 있습니다.
|
||||
|
||||
그리고 많은 팀이 부분 업데이트에도 `PUT`만 사용합니다.
|
||||
|
||||
여러분은 원하는 방식으로 **자유롭게** 사용할 수 있으며, **FastAPI**는 어떤 제한도 강제하지 않습니다.
|
||||
|
||||
다만 이 가이드는 의도된 사용 방식이 대략 어떻게 되는지를 보여줍니다.
|
||||
|
||||
///
|
||||
|
||||
### Pydantic의 `exclude_unset` 파라미터 사용하기 { #using-pydantics-exclude-unset-parameter }
|
||||
|
||||
부분 업데이트를 받으려면 Pydantic 모델의 `.model_dump()`에서 `exclude_unset` 파라미터를 사용하는 것이 매우 유용합니다.
|
||||
|
||||
예: `item.model_dump(exclude_unset=True)`.
|
||||
|
||||
이는 `item` 모델을 만들 때 실제로 설정된 데이터만 포함하는 `dict`를 생성하고, 기본값은 제외합니다.
|
||||
|
||||
그 다음 이를 사용해 (요청에서 전송되어) 설정된 데이터만 포함하고 기본값은 생략한 `dict`를 만들 수 있습니다:
|
||||
|
||||
{* ../../docs_src/body_updates/tutorial002_py310.py hl[32] *}
|
||||
|
||||
### Pydantic의 `update` 파라미터 사용하기 { #using-pydantics-update-parameter }
|
||||
|
||||
이제 `.model_copy()`를 사용해 기존 모델의 복사본을 만들고, 업데이트할 데이터가 들어있는 `dict`를 `update` 파라미터로 전달할 수 있습니다.
|
||||
|
||||
예: `stored_item_model.model_copy(update=update_data)`:
|
||||
|
||||
{* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *}
|
||||
|
||||
### 부분 업데이트 요약 { #partial-updates-recap }
|
||||
|
||||
정리하면, 부분 업데이트를 적용하려면 다음을 수행합니다:
|
||||
|
||||
* (선택 사항) `PUT` 대신 `PATCH`를 사용합니다.
|
||||
* 저장된 데이터를 조회합니다.
|
||||
* 그 데이터를 Pydantic 모델에 넣습니다.
|
||||
* 입력 모델에서 기본값이 제외된 `dict`를 생성합니다(`exclude_unset` 사용).
|
||||
* 이렇게 하면 모델의 기본값으로 이미 저장된 값을 덮어쓰지 않고, 사용자가 실제로 설정한 값만 업데이트할 수 있습니다.
|
||||
* 저장된 모델의 복사본을 만들고, 받은 부분 업데이트로 해당 속성들을 갱신합니다(`update` 파라미터 사용).
|
||||
* 복사한 모델을 DB에 저장할 수 있는 형태로 변환합니다(예: `jsonable_encoder` 사용).
|
||||
* 이는 모델의 `.model_dump()` 메서드를 다시 사용하는 것과 비슷하지만, JSON으로 변환 가능한 데이터 타입으로 값이 확실히 변환되도록 보장합니다(예: `datetime` → `str`).
|
||||
* 데이터를 DB에 저장합니다.
|
||||
* 업데이트된 모델을 반환합니다.
|
||||
|
||||
{* ../../docs_src/body_updates/tutorial002_py310.py hl[28:35] *}
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
동일한 기법을 HTTP `PUT` 작업에서도 실제로 사용할 수 있습니다.
|
||||
|
||||
하지만 여기의 예시는 이런 사용 사례를 위해 만들어진 `PATCH`를 사용합니다.
|
||||
|
||||
///
|
||||
|
||||
/// note | 참고
|
||||
|
||||
입력 모델은 여전히 검증된다는 점에 유의하세요.
|
||||
|
||||
따라서 모든 속성을 생략할 수 있는 부분 업데이트를 받으려면, 모든 속성이 optional로 표시된(기본값을 가지거나 `None`을 기본값으로 가지는) 모델이 필요합니다.
|
||||
|
||||
**업데이트**를 위한 “모든 값이 optional인” 모델과, **생성**을 위한 “필수 값이 있는” 모델을 구분하려면 [추가 모델](extra-models.md){.internal-link target=_blank}에 설명된 아이디어를 사용할 수 있습니다.
|
||||
|
||||
///
|
||||
@@ -1,105 +0,0 @@
|
||||
# 하위 의존성 { #sub-dependencies }
|
||||
|
||||
**하위 의존성**을 가지는 의존성을 만들 수 있습니다.
|
||||
|
||||
필요한 만큼 **깊게** 중첩할 수도 있습니다.
|
||||
|
||||
이것을 해결하는 일은 **FastAPI**가 알아서 처리합니다.
|
||||
|
||||
## 첫 번째 의존성 "dependable" { #first-dependency-dependable }
|
||||
|
||||
다음과 같이 첫 번째 의존성("dependable")을 만들 수 있습니다:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[8:9] *}
|
||||
|
||||
이 의존성은 선택적 쿼리 파라미터 `q`를 `str`로 선언하고, 그대로 반환합니다.
|
||||
|
||||
매우 단순한 예시(그다지 유용하진 않음)이지만, 하위 의존성이 어떻게 동작하는지에 집중하는 데 도움이 됩니다.
|
||||
|
||||
## 두 번째 의존성 "dependable"과 "dependant" { #second-dependency-dependable-and-dependant }
|
||||
|
||||
그다음, 또 다른 의존성 함수("dependable")를 만들 수 있는데, 이 함수는 동시에 자기 자신의 의존성도 선언합니다(그래서 "dependant"이기도 합니다):
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[13] *}
|
||||
|
||||
선언된 파라미터를 살펴보겠습니다:
|
||||
|
||||
* 이 함수 자체가 의존성("dependable")이지만, 다른 의존성도 하나 선언합니다(즉, 다른 무언가에 "의존"합니다).
|
||||
* `query_extractor`에 의존하며, 그 반환값을 파라미터 `q`에 할당합니다.
|
||||
* 또한 선택적 `last_query` 쿠키를 `str`로 선언합니다.
|
||||
* 사용자가 쿼리 `q`를 제공하지 않았다면, 이전에 쿠키에 저장해 둔 마지막 쿼리를 사용합니다.
|
||||
|
||||
## 의존성 사용하기 { #use-the-dependency }
|
||||
|
||||
그다음 다음과 같이 의존성을 사용할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[23] *}
|
||||
|
||||
/// info | 정보
|
||||
|
||||
*경로 처리 함수*에서는 `query_or_cookie_extractor`라는 의존성 하나만 선언하고 있다는 점에 주목하세요.
|
||||
|
||||
하지만 **FastAPI**는 `query_or_cookie_extractor`를 호출하는 동안 그 결과를 전달하기 위해, 먼저 `query_extractor`를 해결해야 한다는 것을 알고 있습니다.
|
||||
|
||||
///
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
|
||||
query_extractor(["query_extractor"])
|
||||
query_or_cookie_extractor(["query_or_cookie_extractor"])
|
||||
|
||||
read_query["/items/"]
|
||||
|
||||
query_extractor --> query_or_cookie_extractor --> read_query
|
||||
```
|
||||
|
||||
## 같은 의존성을 여러 번 사용하기 { #using-the-same-dependency-multiple-times }
|
||||
|
||||
같은 *경로 처리*에 대해 의존성 중 하나가 여러 번 선언되는 경우(예: 여러 의존성이 공통 하위 의존성을 갖는 경우), **FastAPI**는 그 하위 의존성을 요청당 한 번만 호출해야 한다는 것을 알고 있습니다.
|
||||
|
||||
그리고 같은 요청에 대해 동일한 의존성을 여러 번 호출하는 대신, 반환값을 <abbr title="계산/생성된 값을 저장해 두었다가, 다시 계산하지 않고 재사용하기 위한 유틸리티/시스템.">"cache"</abbr>에 저장하고, 그 요청에서 해당 값이 필요한 모든 "dependants"에 전달합니다.
|
||||
|
||||
고급 시나리오로, 같은 요청에서 "cached" 값을 쓰는 대신 매 단계마다(아마도 여러 번) 의존성이 호출되어야 한다는 것을 알고 있다면, `Depends`를 사용할 때 `use_cache=False` 파라미터를 설정할 수 있습니다:
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
```Python hl_lines="1"
|
||||
async def needy_dependency(fresh_value: Annotated[str, Depends(get_value, use_cache=False)]):
|
||||
return {"fresh_value": fresh_value}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.9+ 비 Annotated
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
가능하다면 `Annotated` 버전을 사용하는 것을 권장합니다.
|
||||
|
||||
///
|
||||
|
||||
```Python hl_lines="1"
|
||||
async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)):
|
||||
return {"fresh_value": fresh_value}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
## 정리 { #recap }
|
||||
|
||||
여기서 사용한 그럴듯한 용어들을 제외하면, **Dependency Injection** 시스템은 꽤 단순합니다.
|
||||
|
||||
*경로 처리 함수*와 같은 형태의 함수들일 뿐입니다.
|
||||
|
||||
하지만 여전히 매우 강력하며, 임의로 깊게 중첩된 의존성 "그래프"(트리)를 선언할 수 있습니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
이 단순한 예시만 보면 그다지 유용해 보이지 않을 수도 있습니다.
|
||||
|
||||
하지만 **보안**에 관한 챕터에서 이것이 얼마나 유용한지 보게 될 것입니다.
|
||||
|
||||
또한 얼마나 많은 코드를 아껴주는지도 보게 될 것입니다.
|
||||
|
||||
///
|
||||
@@ -1,244 +0,0 @@
|
||||
# 오류 처리 { #handling-errors }
|
||||
|
||||
API를 사용하는 클라이언트에 오류를 알려야 하는 상황은 많이 있습니다.
|
||||
|
||||
이 클라이언트는 프론트엔드가 있는 브라우저일 수도 있고, 다른 사람이 작성한 코드일 수도 있고, IoT 장치일 수도 있습니다.
|
||||
|
||||
클라이언트에 다음과 같은 내용을 알려야 할 수도 있습니다:
|
||||
|
||||
* 클라이언트가 해당 작업을 수행할 충분한 권한이 없습니다.
|
||||
* 클라이언트가 해당 리소스에 접근할 수 없습니다.
|
||||
* 클라이언트가 접근하려고 한 항목이 존재하지 않습니다.
|
||||
* 등등.
|
||||
|
||||
이런 경우 보통 **400**번대(400에서 499) 범위의 **HTTP 상태 코드**를 반환합니다.
|
||||
|
||||
이는 200번대 HTTP 상태 코드(200에서 299)와 비슷합니다. "200" 상태 코드는 어떤 형태로든 요청이 "성공"했음을 의미합니다.
|
||||
|
||||
400번대 상태 코드는 클라이언트 측에서 오류가 발생했음을 의미합니다.
|
||||
|
||||
**"404 Not Found"** 오류(그리고 농담들)도 다들 기억하시죠?
|
||||
|
||||
## `HTTPException` 사용하기 { #use-httpexception }
|
||||
|
||||
클라이언트에 오류가 포함된 HTTP 응답을 반환하려면 `HTTPException`을 사용합니다.
|
||||
|
||||
### `HTTPException` 가져오기 { #import-httpexception }
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial001_py39.py hl[1] *}
|
||||
|
||||
### 코드에서 `HTTPException` 발생시키기 { #raise-an-httpexception-in-your-code }
|
||||
|
||||
`HTTPException`은 API와 관련된 추가 데이터를 가진 일반적인 Python 예외입니다.
|
||||
|
||||
Python 예외이므로 `return` 하는 것이 아니라 `raise` 합니다.
|
||||
|
||||
이는 또한, *경로 처리 함수* 내부에서 호출하는 유틸리티 함수 안에서 `HTTPException`을 `raise`하면, *경로 처리 함수*의 나머지 코드는 실행되지 않고 즉시 해당 요청이 종료되며 `HTTPException`의 HTTP 오류가 클라이언트로 전송된다는 뜻입니다.
|
||||
|
||||
값을 반환하는 것보다 예외를 발생시키는 것의 이점은 의존성과 보안에 대한 섹션에서 더 분명해집니다.
|
||||
|
||||
이 예시에서는, 클라이언트가 존재하지 않는 ID로 항목을 요청하면 상태 코드 `404`로 예외를 발생시킵니다:
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial001_py39.py hl[11] *}
|
||||
|
||||
### 결과 응답 { #the-resulting-response }
|
||||
|
||||
클라이언트가 `http://example.com/items/foo`( `item_id` `"foo"`)를 요청하면, HTTP 상태 코드 200과 다음 JSON 응답을 받습니다:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"item": "The Foo Wrestlers"
|
||||
}
|
||||
```
|
||||
|
||||
하지만 클라이언트가 `http://example.com/items/bar`(존재하지 않는 `item_id` `"bar"`)를 요청하면, HTTP 상태 코드 404("not found" 오류)와 다음 JSON 응답을 받습니다:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"detail": "Item not found"
|
||||
}
|
||||
```
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
`HTTPException`을 발생시킬 때 `detail` 파라미터로 `str`만 전달할 수 있는 것이 아니라, JSON으로 변환할 수 있는 어떤 값이든 전달할 수 있습니다.
|
||||
|
||||
`dict`, `list` 등을 전달할 수 있습니다.
|
||||
|
||||
이들은 **FastAPI**가 자동으로 처리해 JSON으로 변환합니다.
|
||||
|
||||
///
|
||||
|
||||
## 커스텀 헤더 추가하기 { #add-custom-headers }
|
||||
|
||||
HTTP 오류에 커스텀 헤더를 추가할 수 있으면 유용한 상황이 있습니다. 예를 들어 특정 보안 유형에서 그렇습니다.
|
||||
|
||||
아마 코드에서 직접 사용할 일은 거의 없을 것입니다.
|
||||
|
||||
하지만 고급 시나리오에서 필요하다면 커스텀 헤더를 추가할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial002_py39.py hl[14] *}
|
||||
|
||||
## 커스텀 예외 핸들러 설치하기 { #install-custom-exception-handlers }
|
||||
|
||||
<a href="https://www.starlette.dev/exceptions/" class="external-link" target="_blank">Starlette의 동일한 예외 유틸리티</a>를 사용해 커스텀 예외 핸들러를 추가할 수 있습니다.
|
||||
|
||||
여러분(또는 사용하는 라이브러리)이 `raise`할 수 있는 커스텀 예외 `UnicornException`이 있다고 가정해 봅시다.
|
||||
|
||||
그리고 이 예외를 FastAPI에서 전역적으로 처리하고 싶다고 해봅시다.
|
||||
|
||||
`@app.exception_handler()`로 커스텀 예외 핸들러를 추가할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial003_py39.py hl[5:7,13:18,24] *}
|
||||
|
||||
여기서 `/unicorns/yolo`를 요청하면, *경로 처리*가 `UnicornException`을 `raise`합니다.
|
||||
|
||||
하지만 `unicorn_exception_handler`가 이를 처리합니다.
|
||||
|
||||
따라서 HTTP 상태 코드 `418`과 다음 JSON 내용을 가진 깔끔한 오류를 받게 됩니다:
|
||||
|
||||
```JSON
|
||||
{"message": "Oops! yolo did something. There goes a rainbow..."}
|
||||
```
|
||||
|
||||
/// note | 기술 세부사항
|
||||
|
||||
`from starlette.requests import Request`와 `from starlette.responses import JSONResponse`를 사용할 수도 있습니다.
|
||||
|
||||
**FastAPI**는 개발자의 편의를 위해 `starlette.responses`를 `fastapi.responses`로도 동일하게 제공합니다. 하지만 사용 가능한 대부분의 응답은 Starlette에서 직접 옵니다. `Request`도 마찬가지입니다.
|
||||
|
||||
///
|
||||
|
||||
## 기본 예외 핸들러 오버라이드하기 { #override-the-default-exception-handlers }
|
||||
|
||||
**FastAPI**에는 몇 가지 기본 예외 핸들러가 있습니다.
|
||||
|
||||
이 핸들러들은 `HTTPException`을 `raise`했을 때, 그리고 요청에 유효하지 않은 데이터가 있을 때 기본 JSON 응답을 반환하는 역할을 합니다.
|
||||
|
||||
이 예외 핸들러들을 여러분의 것으로 오버라이드할 수 있습니다.
|
||||
|
||||
### 요청 검증 예외 오버라이드하기 { #override-request-validation-exceptions }
|
||||
|
||||
요청에 유효하지 않은 데이터가 포함되면, **FastAPI**는 내부적으로 `RequestValidationError`를 `raise`합니다.
|
||||
|
||||
그리고 이에 대한 기본 예외 핸들러도 포함되어 있습니다.
|
||||
|
||||
이를 오버라이드하려면 `RequestValidationError`를 가져오고, `@app.exception_handler(RequestValidationError)`로 예외 핸들러를 데코레이트해 사용하세요.
|
||||
|
||||
예외 핸들러는 `Request`와 예외를 받습니다.
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial004_py39.py hl[2,14:19] *}
|
||||
|
||||
이제 `/items/foo`로 이동하면, 다음과 같은 기본 JSON 오류 대신:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": [
|
||||
"path",
|
||||
"item_id"
|
||||
],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
다음과 같은 텍스트 버전을 받게 됩니다:
|
||||
|
||||
```
|
||||
Validation errors:
|
||||
Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer
|
||||
```
|
||||
|
||||
### `HTTPException` 오류 핸들러 오버라이드하기 { #override-the-httpexception-error-handler }
|
||||
|
||||
같은 방식으로 `HTTPException` 핸들러도 오버라이드할 수 있습니다.
|
||||
|
||||
예를 들어, 이런 오류들에 대해 JSON 대신 일반 텍스트 응답을 반환하고 싶을 수 있습니다:
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial004_py39.py hl[3:4,9:11,25] *}
|
||||
|
||||
/// note | 기술 세부사항
|
||||
|
||||
`from starlette.responses import PlainTextResponse`를 사용할 수도 있습니다.
|
||||
|
||||
**FastAPI**는 개발자의 편의를 위해 `starlette.responses`를 `fastapi.responses`로도 동일하게 제공합니다. 하지만 사용 가능한 대부분의 응답은 Starlette에서 직접 옵니다.
|
||||
|
||||
///
|
||||
|
||||
/// warning | 경고
|
||||
|
||||
`RequestValidationError`에는 검증 오류가 발생한 파일 이름과 줄 정보가 포함되어 있어, 원한다면 관련 정보와 함께 로그에 표시할 수 있다는 점을 유념하세요.
|
||||
|
||||
하지만 이는 단순히 문자열로 변환해 그 정보를 그대로 반환하면 시스템에 대한 일부 정보를 누설할 수 있다는 뜻이기도 합니다. 그래서 여기의 코드는 각 오류를 독립적으로 추출해 보여줍니다.
|
||||
|
||||
///
|
||||
|
||||
### `RequestValidationError`의 body 사용하기 { #use-the-requestvalidationerror-body }
|
||||
|
||||
`RequestValidationError`에는 유효하지 않은 데이터와 함께 받은 `body`가 포함됩니다.
|
||||
|
||||
앱을 개발하는 동안 body를 로그로 남기고 디버그하거나, 사용자에게 반환하는 등으로 사용할 수 있습니다.
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial005_py39.py hl[14] *}
|
||||
|
||||
이제 다음처럼 유효하지 않은 item을 보내보세요:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"title": "towel",
|
||||
"size": "XL"
|
||||
}
|
||||
```
|
||||
|
||||
받은 body를 포함해 데이터가 유효하지 않다고 알려주는 응답을 받게 됩니다:
|
||||
|
||||
```JSON hl_lines="12-15"
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": [
|
||||
"body",
|
||||
"size"
|
||||
],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"title": "towel",
|
||||
"size": "XL"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### FastAPI의 `HTTPException` vs Starlette의 `HTTPException` { #fastapis-httpexception-vs-starlettes-httpexception }
|
||||
|
||||
**FastAPI**에는 자체 `HTTPException`이 있습니다.
|
||||
|
||||
그리고 **FastAPI**의 `HTTPException` 오류 클래스는 Starlette의 `HTTPException` 오류 클래스를 상속합니다.
|
||||
|
||||
유일한 차이는 **FastAPI**의 `HTTPException`은 `detail` 필드에 JSON으로 변환 가능한 어떤 데이터든 받을 수 있는 반면, Starlette의 `HTTPException`은 문자열만 받을 수 있다는 점입니다.
|
||||
|
||||
따라서 코드에서는 평소처럼 **FastAPI**의 `HTTPException`을 계속 `raise`하면 됩니다.
|
||||
|
||||
하지만 예외 핸들러를 등록할 때는 Starlette의 `HTTPException`에 대해 등록해야 합니다.
|
||||
|
||||
이렇게 하면 Starlette 내부 코드의 어떤 부분, 또는 Starlette 확장/플러그인이 Starlette `HTTPException`을 `raise`하더라도, 여러분의 핸들러가 이를 잡아서 처리할 수 있습니다.
|
||||
|
||||
이 예시에서는 동일한 코드에서 두 `HTTPException`을 모두 사용할 수 있도록, Starlette의 예외를 `StarletteHTTPException`으로 이름을 바꿉니다:
|
||||
|
||||
```Python
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
```
|
||||
|
||||
### **FastAPI**의 예외 핸들러 재사용하기 { #reuse-fastapis-exception-handlers }
|
||||
|
||||
예외를 사용하면서 **FastAPI**의 동일한 기본 예외 핸들러도 함께 사용하고 싶다면, `fastapi.exception_handlers`에서 기본 예외 핸들러를 가져와 재사용할 수 있습니다:
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial006_py39.py hl[2:5,15,21] *}
|
||||
|
||||
이 예시에서는 매우 표현력 있는 메시지로 오류를 출력만 하고 있지만, 요지는 이해하셨을 겁니다. 예외를 사용한 뒤 기본 예외 핸들러를 그대로 재사용할 수 있습니다.
|
||||
@@ -1,203 +0,0 @@
|
||||
# 보안 - 첫 단계 { #security-first-steps }
|
||||
|
||||
어떤 도메인에 **backend** API가 있다고 가정해 보겠습니다.
|
||||
|
||||
그리고 다른 도메인에 **frontend**가 있거나, 같은 도메인의 다른 경로에 있거나(또는 모바일 애플리케이션에 있을 수도 있습니다).
|
||||
|
||||
그리고 frontend가 **username**과 **password**를 사용해 backend에 인증할 수 있는 방법이 필요하다고 해봅시다.
|
||||
|
||||
**FastAPI**와 함께 **OAuth2**를 사용해서 이를 구현할 수 있습니다.
|
||||
|
||||
하지만 필요한 작은 정보 조각들을 찾기 위해 길고 긴 전체 스펙을 읽느라 시간을 쓰지 않도록 하겠습니다.
|
||||
|
||||
보안을 처리하기 위해 **FastAPI**가 제공하는 도구들을 사용해 봅시다.
|
||||
|
||||
## 어떻게 보이는지 { #how-it-looks }
|
||||
|
||||
먼저 코드를 그냥 사용해서 어떻게 동작하는지 보고, 그다음에 무슨 일이 일어나는지 이해하러 다시 돌아오겠습니다.
|
||||
|
||||
## `main.py` 만들기 { #create-main-py }
|
||||
|
||||
예제를 파일 `main.py`에 복사하세요:
|
||||
|
||||
{* ../../docs_src/security/tutorial001_an_py39.py *}
|
||||
|
||||
## 실행하기 { #run-it }
|
||||
|
||||
/// info | 정보
|
||||
|
||||
<a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a> 패키지는 `pip install "fastapi[standard]"` 명령을 실행하면 **FastAPI**와 함께 자동으로 설치됩니다.
|
||||
|
||||
하지만 `pip install fastapi` 명령을 사용하면 `python-multipart` 패키지가 기본으로 포함되지 않습니다.
|
||||
|
||||
수동으로 설치하려면, [가상 환경](../../virtual-environments.md){.internal-link target=_blank}을 만들고 활성화한 다음, 아래로 설치하세요:
|
||||
|
||||
```console
|
||||
$ pip install python-multipart
|
||||
```
|
||||
|
||||
이는 **OAuth2**가 `username`과 `password`를 보내기 위해 "form data"를 사용하기 때문입니다.
|
||||
|
||||
///
|
||||
|
||||
다음으로 예제를 실행하세요:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi dev main.py
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## 확인하기 { #check-it }
|
||||
|
||||
대화형 문서로 이동하세요: <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||
|
||||
다음과 비슷한 화면이 보일 것입니다:
|
||||
|
||||
<img src="/img/tutorial/security/image01.png">
|
||||
|
||||
/// check | Authorize 버튼!
|
||||
|
||||
반짝이는 새 "Authorize" 버튼이 이미 있습니다.
|
||||
|
||||
그리고 *경로 처리*에는 오른쪽 상단에 클릭할 수 있는 작은 자물쇠가 있습니다.
|
||||
|
||||
///
|
||||
|
||||
그리고 이를 클릭하면 `username`과 `password`(그리고 다른 선택적 필드들)를 입력할 수 있는 작은 인증 폼이 나타납니다:
|
||||
|
||||
<img src="/img/tutorial/security/image02.png">
|
||||
|
||||
/// note | 참고
|
||||
|
||||
폼에 무엇을 입력하든 아직은 동작하지 않습니다. 하지만 곧 여기까지 구현할 것입니다.
|
||||
|
||||
///
|
||||
|
||||
물론 이것은 최종 사용자를 위한 frontend는 아니지만, 모든 API를 대화형으로 문서화하는 훌륭한 자동 도구입니다.
|
||||
|
||||
frontend 팀(그게 본인일 수도 있습니다)이 사용할 수 있습니다.
|
||||
|
||||
서드파티 애플리케이션과 시스템에서도 사용할 수 있습니다.
|
||||
|
||||
그리고 동일한 애플리케이션을 디버그하고, 확인하고, 테스트하기 위해 본인이 사용할 수도 있습니다.
|
||||
|
||||
## `password` 플로우 { #the-password-flow }
|
||||
|
||||
이제 조금 돌아가서 이것들이 무엇인지 이해해 봅시다.
|
||||
|
||||
`password` "flow"는 보안과 인증을 처리하기 위해 OAuth2에서 정의한 여러 방식("flows") 중 하나입니다.
|
||||
|
||||
OAuth2는 backend 또는 API가 사용자를 인증하는 서버와 독립적일 수 있도록 설계되었습니다.
|
||||
|
||||
하지만 이 경우에는 같은 **FastAPI** 애플리케이션이 API와 인증을 모두 처리합니다.
|
||||
|
||||
따라서, 단순화된 관점에서 다시 정리해보면:
|
||||
|
||||
* 사용자가 frontend에서 `username`과 `password`를 입력하고 `Enter`를 누릅니다.
|
||||
* frontend(사용자의 브라우저에서 실행됨)는 해당 `username`과 `password`를 우리 API의 특정 URL로 보냅니다(`tokenUrl="token"`로 선언됨).
|
||||
* API는 `username`과 `password`를 확인하고 "token"으로 응답합니다(아직 아무것도 구현하지 않았습니다).
|
||||
* "token"은 나중에 이 사용자를 검증하는 데 사용할 수 있는 어떤 내용이 담긴 문자열일 뿐입니다.
|
||||
* 보통 token은 일정 시간이 지나면 만료되도록 설정합니다.
|
||||
* 그래서 사용자는 나중에 어느 시점엔 다시 로그인해야 합니다.
|
||||
* 그리고 token이 도난당하더라도 위험이 더 낮습니다. 대부분의 경우 영구적으로 항상 동작하는 키와는 다릅니다.
|
||||
* frontend는 그 token을 임시로 어딘가에 저장합니다.
|
||||
* 사용자가 frontend에서 클릭해서 frontend 웹 앱의 다른 섹션으로 이동합니다.
|
||||
* frontend는 API에서 더 많은 데이터를 가져와야 합니다.
|
||||
* 하지만 그 특정 endpoint에는 인증이 필요합니다.
|
||||
* 그래서 우리 API에 인증하기 위해 `Authorization` 헤더를, 값은 `Bearer `에 token을 더한 형태로 보냅니다.
|
||||
* token에 `foobar`가 들어 있다면 `Authorization` 헤더의 내용은 `Bearer foobar`가 됩니다.
|
||||
|
||||
## **FastAPI**의 `OAuth2PasswordBearer` { #fastapis-oauth2passwordbearer }
|
||||
|
||||
**FastAPI**는 이런 보안 기능을 구현하기 위해, 서로 다른 추상화 수준에서 여러 도구를 제공합니다.
|
||||
|
||||
이 예제에서는 **OAuth2**의 **Password** 플로우와 **Bearer** token을 사용합니다. 이를 위해 `OAuth2PasswordBearer` 클래스를 사용합니다.
|
||||
|
||||
/// info | 정보
|
||||
|
||||
"bearer" token만이 유일한 선택지는 아닙니다.
|
||||
|
||||
하지만 이 사용 사례에는 가장 적합한 선택입니다.
|
||||
|
||||
또한 OAuth2 전문가로서 왜 다른 옵션이 더 적합한지 정확히 아는 경우가 아니라면, 대부분의 사용 사례에도 가장 적합할 가능성이 큽니다.
|
||||
|
||||
그런 경우를 위해서도 **FastAPI**는 이를 구성할 수 있는 도구를 제공합니다.
|
||||
|
||||
///
|
||||
|
||||
`OAuth2PasswordBearer` 클래스의 인스턴스를 만들 때 `tokenUrl` 파라미터를 전달합니다. 이 파라미터에는 클라이언트(사용자의 브라우저에서 실행되는 frontend)가 token을 받기 위해 `username`과 `password`를 보낼 URL이 들어 있습니다.
|
||||
|
||||
{* ../../docs_src/security/tutorial001_an_py39.py hl[8] *}
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
여기서 `tokenUrl="token"`은 아직 만들지 않은 상대 URL `token`을 가리킵니다. 상대 URL이므로 `./token`과 동일합니다.
|
||||
|
||||
상대 URL을 사용하므로, 예를 들어 API가 `https://example.com/`에 있다면 `https://example.com/token`을 가리킵니다. 하지만 API가 `https://example.com/api/v1/`에 있다면 `https://example.com/api/v1/token`을 가리킵니다.
|
||||
|
||||
상대 URL을 사용하는 것은 [프록시 뒤에서](../../advanced/behind-a-proxy.md){.internal-link target=_blank} 같은 고급 사용 사례에서도 애플리케이션이 계속 동작하도록 보장하는 데 중요합니다.
|
||||
|
||||
///
|
||||
|
||||
이 파라미터는 그 endpoint / *경로 처리*를 만들지는 않지만, URL `/token`이 클라이언트가 token을 얻기 위해 사용해야 할 URL이라고 선언합니다. 이 정보는 OpenAPI에 사용되고, 이어서 대화형 API 문서 시스템에서도 사용됩니다.
|
||||
|
||||
곧 실제 경로 처리를 만들 것입니다.
|
||||
|
||||
/// info | 정보
|
||||
|
||||
엄격한 "Pythonista"라면 `token_url` 대신 `tokenUrl` 같은 파라미터 이름 스타일이 마음에 들지 않을 수도 있습니다.
|
||||
|
||||
이는 OpenAPI 스펙에서 사용하는 이름과 동일하게 맞춘 것이기 때문입니다. 그래서 이런 보안 스킴에 대해 더 조사해야 할 때, 그대로 복사해서 붙여 넣어 더 많은 정보를 찾을 수 있습니다.
|
||||
|
||||
///
|
||||
|
||||
`oauth2_scheme` 변수는 `OAuth2PasswordBearer`의 인스턴스이지만, "callable"이기도 합니다.
|
||||
|
||||
다음처럼 호출될 수 있습니다:
|
||||
|
||||
```Python
|
||||
oauth2_scheme(some, parameters)
|
||||
```
|
||||
|
||||
따라서 `Depends`와 함께 사용할 수 있습니다.
|
||||
|
||||
### 사용하기 { #use-it }
|
||||
|
||||
이제 `Depends`로 `oauth2_scheme`를 의존성에 전달할 수 있습니다.
|
||||
|
||||
{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *}
|
||||
|
||||
이 의존성은 `str`을 제공하고, 그 값은 *경로 처리 함수*의 파라미터 `token`에 할당됩니다.
|
||||
|
||||
**FastAPI**는 이 의존성을 사용해 OpenAPI 스키마(및 자동 API 문서)에 "security scheme"를 정의할 수 있다는 것을 알게 됩니다.
|
||||
|
||||
/// info | 기술 세부사항
|
||||
|
||||
**FastAPI**는 (의존성에 선언된) `OAuth2PasswordBearer` 클래스를 사용해 OpenAPI에서 보안 스킴을 정의할 수 있다는 것을 알고 있습니다. 이는 `OAuth2PasswordBearer`가 `fastapi.security.oauth2.OAuth2`를 상속하고, 이것이 다시 `fastapi.security.base.SecurityBase`를 상속하기 때문입니다.
|
||||
|
||||
OpenAPI(및 자동 API 문서)와 통합되는 모든 보안 유틸리티는 `SecurityBase`를 상속하며, 그래서 **FastAPI**가 이를 OpenAPI에 어떻게 통합할지 알 수 있습니다.
|
||||
|
||||
///
|
||||
|
||||
## 무엇을 하는지 { #what-it-does }
|
||||
|
||||
요청에서 `Authorization` 헤더를 찾아, 값이 `Bearer `에 어떤 token이 붙은 형태인지 확인한 뒤, 그 token을 `str`로 반환합니다.
|
||||
|
||||
`Authorization` 헤더가 없거나, 값에 `Bearer ` token이 없다면, 곧바로 401 상태 코드 오류(`UNAUTHORIZED`)로 응답합니다.
|
||||
|
||||
오류를 반환하기 위해 token이 존재하는지 직접 확인할 필요조차 없습니다. 함수가 실행되었다면 그 token에는 `str`이 들어 있다고 확신할 수 있습니다.
|
||||
|
||||
대화형 문서에서 이미 시도해 볼 수 있습니다:
|
||||
|
||||
<img src="/img/tutorial/security/image03.png">
|
||||
|
||||
아직 token의 유효성을 검증하진 않지만, 이것만으로도 시작은 된 셈입니다.
|
||||
|
||||
## 요약 { #recap }
|
||||
|
||||
즉, 추가로 3~4줄만으로도 이미 원시적인 형태의 보안을 갖추게 됩니다.
|
||||
@@ -1,106 +0,0 @@
|
||||
# 보안 { #security }
|
||||
|
||||
보안, 인증(authentication), 인가(authorization)를 처리하는 방법은 매우 다양합니다.
|
||||
|
||||
그리고 보통 복잡하고 "어려운" 주제이기도 합니다.
|
||||
|
||||
많은 프레임워크와 시스템에서 보안과 인증만 처리하는 데도 큰 노력과 코드가 필요합니다(많은 경우 작성된 전체 코드의 50% 이상이 될 수도 있습니다).
|
||||
|
||||
**FastAPI**는 모든 보안 명세를 전부 공부하고 배울 필요 없이, 표준적인 방식으로 쉽고 빠르게 **보안(Security)** 을 다룰 수 있도록 여러 도구를 제공합니다.
|
||||
|
||||
하지만 먼저, 몇 가지 작은 개념을 확인해 보겠습니다.
|
||||
|
||||
## 급하신가요? { #in-a-hurry }
|
||||
|
||||
이 용어들에 관심이 없고 사용자명과 비밀번호 기반 인증을 사용한 보안을 *지금 당장* 추가하기만 하면 된다면, 다음 장들로 넘어가세요.
|
||||
|
||||
## OAuth2 { #oauth2 }
|
||||
|
||||
OAuth2는 인증과 인가를 처리하는 여러 방법을 정의하는 명세입니다.
|
||||
|
||||
상당히 방대한 명세이며 여러 복잡한 사용 사례를 다룹니다.
|
||||
|
||||
"제3자"를 사용해 인증하는 방법도 포함합니다.
|
||||
|
||||
바로 `"Facebook, Google, X (Twitter), GitHub로 로그인"` 같은 시스템들이 내부적으로 사용하는 방식입니다.
|
||||
|
||||
### OAuth 1 { #oauth-1 }
|
||||
|
||||
OAuth 1도 있었는데, 이는 OAuth2와 매우 다르고 통신을 암호화하는 방법까지 직접 명세에 포함했기 때문에 더 복잡했습니다.
|
||||
|
||||
요즘에는 그다지 인기 있거나 사용되지는 않습니다.
|
||||
|
||||
OAuth2는 통신을 어떻게 암호화할지는 명세하지 않고, 애플리케이션이 HTTPS로 제공될 것을 기대합니다.
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
**배포**에 대한 섹션에서 Traefik과 Let's Encrypt를 사용해 무료로 HTTPS를 설정하는 방법을 볼 수 있습니다.
|
||||
|
||||
///
|
||||
|
||||
## OpenID Connect { #openid-connect }
|
||||
|
||||
OpenID Connect는 **OAuth2**를 기반으로 한 또 다른 명세입니다.
|
||||
|
||||
OAuth2에서 비교적 모호한 부분을 일부 구체화하여 상호 운용성을 높이려는 확장입니다.
|
||||
|
||||
예를 들어, Google 로그인은 OpenID Connect를 사용합니다(내부적으로는 OAuth2를 사용).
|
||||
|
||||
하지만 Facebook 로그인은 OpenID Connect를 지원하지 않습니다. 자체적인 변형의 OAuth2를 사용합니다.
|
||||
|
||||
### OpenID("OpenID Connect"가 아님) { #openid-not-openid-connect }
|
||||
|
||||
"OpenID"라는 명세도 있었습니다. 이는 **OpenID Connect**와 같은 문제를 해결하려고 했지만, OAuth2를 기반으로 하지 않았습니다.
|
||||
|
||||
따라서 완전히 별도의 추가 시스템이었습니다.
|
||||
|
||||
요즘에는 그다지 인기 있거나 사용되지는 않습니다.
|
||||
|
||||
## OpenAPI { #openapi }
|
||||
|
||||
OpenAPI(이전에는 Swagger로 알려짐)는 API를 구축하기 위한 공개 명세입니다(현재 Linux Foundation의 일부).
|
||||
|
||||
**FastAPI**는 **OpenAPI**를 기반으로 합니다.
|
||||
|
||||
이 덕분에 여러 자동 대화형 문서 인터페이스, 코드 생성 등과 같은 기능을 사용할 수 있습니다.
|
||||
|
||||
OpenAPI에는 여러 보안 "scheme"을 정의하는 방법이 있습니다.
|
||||
|
||||
이를 사용하면 이러한 대화형 문서 시스템을 포함해, 표준 기반 도구들을 모두 활용할 수 있습니다.
|
||||
|
||||
OpenAPI는 다음 보안 scheme들을 정의합니다:
|
||||
|
||||
* `apiKey`: 다음에서 전달될 수 있는 애플리케이션 전용 키:
|
||||
* 쿼리 파라미터
|
||||
* 헤더
|
||||
* 쿠키
|
||||
* `http`: 표준 HTTP 인증 시스템, 예:
|
||||
* `bearer`: `Authorization` 헤더에 `Bearer ` + 토큰 값을 넣는 방식. OAuth2에서 유래했습니다.
|
||||
* HTTP Basic 인증
|
||||
* HTTP Digest 등
|
||||
* `oauth2`: 보안을 처리하는 모든 OAuth2 방식(이를 "flow"라고 부릅니다).
|
||||
* 이 flow들 중 여러 개는 OAuth 2.0 인증 제공자(예: Google, Facebook, X (Twitter), GitHub 등)를 구축하는 데 적합합니다:
|
||||
* `implicit`
|
||||
* `clientCredentials`
|
||||
* `authorizationCode`
|
||||
* 하지만 같은 애플리케이션에서 직접 인증을 처리하는 데 완벽하게 사용할 수 있는 특정 "flow"도 하나 있습니다:
|
||||
* `password`: 다음 장들에서 이에 대한 예시를 다룹니다.
|
||||
* `openIdConnect`: OAuth2 인증 데이터를 자동으로 탐색(discover)하는 방법을 정의합니다.
|
||||
* 이 자동 탐색은 OpenID Connect 명세에서 정의됩니다.
|
||||
|
||||
|
||||
/// tip | 팁
|
||||
|
||||
Google, Facebook, X (Twitter), GitHub 등 다른 인증/인가 제공자를 통합하는 것도 가능하며 비교적 쉽습니다.
|
||||
|
||||
가장 복잡한 문제는 그런 인증/인가 제공자 자체를 구축하는 것이지만, **FastAPI**는 어려운 작업을 대신 처리해 주면서 이를 쉽게 할 수 있는 도구를 제공합니다.
|
||||
|
||||
///
|
||||
|
||||
## **FastAPI** 유틸리티 { #fastapi-utilities }
|
||||
|
||||
FastAPI는 `fastapi.security` 모듈에서 각 보안 scheme에 대한 여러 도구를 제공하며, 이러한 보안 메커니즘을 더 쉽게 사용할 수 있게 해줍니다.
|
||||
|
||||
다음 장들에서는 **FastAPI**가 제공하는 도구를 사용해 API에 보안을 추가하는 방법을 보게 될 것입니다.
|
||||
|
||||
또한 대화형 문서 시스템에 어떻게 자동으로 통합되는지도 확인하게 됩니다.
|
||||
@@ -10,26 +10,6 @@ Keep existing translations as they are if the term is already translated.
|
||||
|
||||
When translating documentation into Portuguese, use neutral and widely understandable language. Although Portuguese originated in Portugal and has its largest number of speakers in Brazil, it is also an official language in several countries and regions, such as Equatorial Guinea, Mozambique, Angola, Cape Verde, and São Tomé and Príncipe. Avoid words or expressions that are specific to a single country or region.
|
||||
|
||||
Only keep parentheses if they exist in the source text. Do not add parentheses to terms that do not have them.
|
||||
|
||||
### Avoiding Repetition in Translation
|
||||
|
||||
When translating sentences, avoid unnecessary repetition of words or phrases that are implied in context.
|
||||
- Merge repeated words naturally while keeping the meaning.
|
||||
- Do **not** introduce extra words to replace repeated phrases unnecessarily.
|
||||
- Keep translations fluent and concise, but maintain the original meaning.
|
||||
|
||||
**Example:**
|
||||
|
||||
Source:
|
||||
Let's see how that works and how to change it if you need to do that.
|
||||
|
||||
Avoid translating literally as:
|
||||
Vamos ver como isso funciona e como alterar isso se você precisar fazer isso.
|
||||
|
||||
Better translation:
|
||||
Vamos ver como isso funciona e como alterar se você precisar.
|
||||
|
||||
---
|
||||
|
||||
For the next terms, use the following translations:
|
||||
@@ -42,11 +22,10 @@ For the next terms, use the following translations:
|
||||
* /// note: /// note | Nota
|
||||
* /// tip: /// tip | Dica
|
||||
* /// warning: /// warning | Atenção
|
||||
* you should: você deveria
|
||||
* (you should): (você deveria)
|
||||
* async context manager: gerenciador de contexto assíncrono
|
||||
* autocomplete: autocompletar
|
||||
* autocompletion: preenchimento automático
|
||||
* auto-completion: preenchimento automático
|
||||
* bug: bug
|
||||
* context manager: gerenciador de contexto
|
||||
* cross domain: cross domain (do not translate to "domínio cruzado")
|
||||
|
||||
503
docs/uk/docs/_llm-test.md
Normal file
503
docs/uk/docs/_llm-test.md
Normal file
@@ -0,0 +1,503 @@
|
||||
# Тестовий файл LLM { #llm-test-file }
|
||||
|
||||
Цей документ перевіряє, чи <abbr title="Large Language Model - Велика мовна модель">LLM</abbr>, який перекладає документацію, розуміє `general_prompt` у `scripts/translate.py` та мовно-специфічний prompt у `docs/{language code}/llm-prompt.md`. Мовно-специфічний prompt додається до `general_prompt`.
|
||||
|
||||
Тести, додані тут, побачать усі розробники мовно-специфічних prompt.
|
||||
|
||||
Використовуйте так:
|
||||
|
||||
* Майте мовно-специфічний prompt — `docs/{language code}/llm-prompt.md`.
|
||||
* Зробіть «свіжий» переклад цього документа на бажану цільову мову (див., напр., команду `translate-page` у `translate.py`). Це створить переклад у `docs/{language code}/docs/_llm-test.md`.
|
||||
* Перевірте, чи все гаразд у перекладі.
|
||||
* За потреби поліпште мовно-специфічний prompt, загальний prompt або англомовний документ.
|
||||
* Потім вручну виправте решту проблем у перекладі, щоб це був якісний переклад.
|
||||
* Перекладіть знову, маючи якісний переклад на місці. Ідеальний результат — LLM більше не вносить жодних змін у переклад. Це означає, що загальний prompt і ваш мовно-специфічний prompt — настільки добрі, наскільки це можливо (інколи він усе ж робитиме кілька на вигляд випадкових змін, причина в тому, що <a href="https://doublespeak.chat/#/handbook#deterministic-output" class="external-link" target="_blank">LLM не є детермінованими алгоритмами</a>).
|
||||
|
||||
Тести:
|
||||
|
||||
## Фрагменти коду { #code-snippets }
|
||||
|
||||
//// tab | Тест
|
||||
|
||||
Ось фрагмент коду: `foo`. А ось ще один фрагмент коду: `bar`. І ще один: `baz quux`.
|
||||
|
||||
////
|
||||
|
||||
//// tab | Інформація
|
||||
|
||||
Вміст фрагментів коду слід залишати без змін.
|
||||
|
||||
Див. розділ `### Content of code snippets` у загальному prompt у `scripts/translate.py`.
|
||||
|
||||
////
|
||||
|
||||
## Лапки { #quotes }
|
||||
|
||||
//// tab | Тест
|
||||
|
||||
Учора мій друг написав: "If you spell incorrectly correctly, you have spelled it incorrectly". На що я відповів: "Correct, but 'incorrectly' is incorrectly not '"incorrectly"'" .
|
||||
|
||||
/// note | Примітка
|
||||
|
||||
LLM, імовірно, перекладе це неправильно. Цікаво лише, чи збереже він виправлений переклад під час повторного перекладання.
|
||||
|
||||
///
|
||||
|
||||
////
|
||||
|
||||
//// tab | Інформація
|
||||
|
||||
Автор prompt може вирішити, чи перетворювати нейтральні лапки на типографські. Можна залишити як є.
|
||||
|
||||
Див., наприклад, розділ `### Quotes` у `docs/de/llm-prompt.md`.
|
||||
|
||||
////
|
||||
|
||||
## Лапки у фрагментах коду { #quotes-in-code-snippets }
|
||||
|
||||
//// tab | Тест
|
||||
|
||||
`pip install "foo[bar]"`
|
||||
|
||||
Приклади рядкових літералів у фрагментах коду: `"this"`, `'that'`.
|
||||
|
||||
Складний приклад рядкових літералів у фрагментах коду: `f"I like {'oranges' if orange else "apples"}"`
|
||||
|
||||
Хардкор: `Yesterday, my friend wrote: "If you spell incorrectly correctly, you have spelled it incorrectly". To which I answered: "Correct, but 'incorrectly' is incorrectly not '"incorrectly"'"`
|
||||
|
||||
////
|
||||
|
||||
//// tab | Інформація
|
||||
|
||||
... Однак лапки всередині фрагментів коду мають залишатися як є.
|
||||
|
||||
////
|
||||
|
||||
## Блоки коду { #code-blocks }
|
||||
|
||||
//// tab | Тест
|
||||
|
||||
Приклад коду Bash...
|
||||
|
||||
```bash
|
||||
# Print a greeting to the universe
|
||||
echo "Hello universe"
|
||||
```
|
||||
|
||||
...і приклад коду консолі...
|
||||
|
||||
```console
|
||||
$ <font color="#4E9A06">fastapi</font> run <u style="text-decoration-style:solid">main.py</u>
|
||||
<span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting server
|
||||
Searching for package file structure
|
||||
```
|
||||
|
||||
...і ще один приклад коду консолі...
|
||||
|
||||
```console
|
||||
// Create a directory "Code"
|
||||
$ mkdir code
|
||||
// Switch into that directory
|
||||
$ cd code
|
||||
```
|
||||
|
||||
...і приклад коду Python...
|
||||
|
||||
```Python
|
||||
wont_work() # This won't work 😱
|
||||
works(foo="bar") # This works 🎉
|
||||
```
|
||||
|
||||
...і на цьому все.
|
||||
|
||||
////
|
||||
|
||||
//// tab | Інформація
|
||||
|
||||
Код у блоках коду не слід змінювати, за винятком коментарів.
|
||||
|
||||
Див. розділ `### Content of code blocks` у загальному prompt у `scripts/translate.py`.
|
||||
|
||||
////
|
||||
|
||||
## Вкладки та кольорові блоки { #tabs-and-colored-boxes }
|
||||
|
||||
//// tab | Тест
|
||||
|
||||
/// info | Інформація
|
||||
Деякий текст
|
||||
///
|
||||
|
||||
/// note | Примітка
|
||||
Деякий текст
|
||||
///
|
||||
|
||||
/// note | Технічні деталі
|
||||
Деякий текст
|
||||
///
|
||||
|
||||
/// check
|
||||
Деякий текст
|
||||
///
|
||||
|
||||
/// tip | Порада
|
||||
Деякий текст
|
||||
///
|
||||
|
||||
/// warning | Попередження
|
||||
Деякий текст
|
||||
///
|
||||
|
||||
/// danger | Обережно
|
||||
Деякий текст
|
||||
///
|
||||
|
||||
////
|
||||
|
||||
//// tab | Інформація
|
||||
|
||||
Для вкладок і блоків `Info`/`Note`/`Warning`/тощо слід додавати переклад їхньої назви після вертикальної риски (`|`).
|
||||
|
||||
Див. розділи `### Special blocks` і `### Tab blocks` у загальному prompt у `scripts/translate.py`.
|
||||
|
||||
////
|
||||
|
||||
## Вебпосилання та внутрішні посилання { #web-and-internal-links }
|
||||
|
||||
//// tab | Тест
|
||||
|
||||
Текст посилання слід перекладати, а адресу посилання — залишати без змін:
|
||||
|
||||
* [Посилання на заголовок вище](#code-snippets)
|
||||
* [Внутрішнє посилання](index.md#installation){.internal-link target=_blank}
|
||||
* <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">Зовнішнє посилання</a>
|
||||
* <a href="https://fastapi.tiangolo.com/css/styles.css" class="external-link" target="_blank">Посилання на стилі</a>
|
||||
* <a href="https://fastapi.tiangolo.com/js/logic.js" class="external-link" target="_blank">Посилання на скрипт</a>
|
||||
* <a href="https://fastapi.tiangolo.com/img/foo.jpg" class="external-link" target="_blank">Посилання на зображення</a>
|
||||
|
||||
Текст посилання слід перекладати, а адреса посилання має вказувати на переклад:
|
||||
|
||||
* <a href="https://fastapi.tiangolo.com/uk/" class="external-link" target="_blank">Посилання на FastAPI</a>
|
||||
|
||||
////
|
||||
|
||||
//// tab | Інформація
|
||||
|
||||
Посилання слід перекладати, але їхня адреса має залишатися без змін. Виняток — абсолютні посилання на сторінки документації FastAPI. У такому разі посилання має вести на переклад.
|
||||
|
||||
Див. розділ `### Links` у загальному prompt у `scripts/translate.py`.
|
||||
|
||||
////
|
||||
|
||||
## HTML-елементи «abbr» { #html-abbr-elements }
|
||||
|
||||
//// tab | Тест
|
||||
|
||||
Тут наведено кілька речей, обгорнутих в HTML-елементи «abbr» (деякі вигадані):
|
||||
|
||||
### abbr задає повну фразу { #the-abbr-gives-a-full-phrase }
|
||||
|
||||
* <abbr title="Getting Things Done - Доведення справ до кінця">GTD</abbr>
|
||||
* <abbr title="less than - менше ніж"><code>lt</code></abbr>
|
||||
* <abbr title="XML Web Token - XML вебтокен">XWT</abbr>
|
||||
* <abbr title="Parallel Server Gateway Interface - Паралельний Server Gateway Interface">PSGI</abbr>
|
||||
|
||||
### abbr задає пояснення { #the-abbr-gives-an-explanation }
|
||||
|
||||
* <abbr title="Група машин, які налаштовані бути з’єднаними та працювати разом певним чином.">cluster</abbr>
|
||||
* <abbr title="Метод машинного навчання, що використовує штучні нейронні мережі з численними прихованими шарами між вхідним і вихідним шарами, формуючи комплексну внутрішню структуру">Deep Learning</abbr>
|
||||
|
||||
### abbr задає повну фразу та пояснення { #the-abbr-gives-a-full-phrase-and-an-explanation }
|
||||
|
||||
* <abbr title="Mozilla Developer Network - Mozilla Developer Network: документація для розробників, написана людьми з Firefox">MDN</abbr>
|
||||
* <abbr title="Input/Output - Ввід/вивід: читання або запис на диск, мережеві комунікації.">I/O</abbr>.
|
||||
|
||||
////
|
||||
|
||||
//// tab | Інформація
|
||||
|
||||
Атрибути `title` в елементах `abbr` перекладаються за певними інструкціями.
|
||||
|
||||
Переклади можуть додавати власні елементи `abbr`, які LLM не повинен вилучати. Напр., щоб пояснювати англійські слова.
|
||||
|
||||
Див. розділ `### HTML abbr elements` у загальному prompt у `scripts/translate.py`.
|
||||
|
||||
////
|
||||
|
||||
## Заголовки { #headings }
|
||||
|
||||
//// tab | Тест
|
||||
|
||||
### Розробка вебзастосунку — навчальний посібник { #develop-a-webapp-a-tutorial }
|
||||
|
||||
Привіт.
|
||||
|
||||
### Підказки типів і -анотації { #type-hints-and-annotations }
|
||||
|
||||
Привіт ще раз.
|
||||
|
||||
### Супер- і підкласи { #super-and-subclasses }
|
||||
|
||||
Привіт ще раз.
|
||||
|
||||
////
|
||||
|
||||
//// tab | Інформація
|
||||
|
||||
Єдине жорстке правило для заголовків — LLM має залишати частину хешу в фігурних дужках без змін, що гарантує, що посилання не зламаються.
|
||||
|
||||
Див. розділ `### Headings` у загальному prompt у `scripts/translate.py`.
|
||||
|
||||
Для деяких мовно-специфічних інструкцій див., напр., розділ `### Headings` у `docs/de/llm-prompt.md`.
|
||||
|
||||
////
|
||||
|
||||
## Терміни, що використовуються в документації { #terms-used-in-the-docs }
|
||||
|
||||
//// tab | Тест
|
||||
|
||||
* ви
|
||||
* ваш
|
||||
|
||||
* напр.
|
||||
* тощо
|
||||
|
||||
* `foo` як `int`
|
||||
* `bar` як `str`
|
||||
* `baz` як `list`
|
||||
|
||||
* Tutorial — Посібник користувача
|
||||
* Advanced User Guide
|
||||
* документація SQLModel
|
||||
* документація API
|
||||
* автоматична документація
|
||||
|
||||
* Data Science
|
||||
* Deep Learning
|
||||
* Machine Learning
|
||||
* Dependency Injection
|
||||
* автентифікація HTTP Basic
|
||||
* HTTP Digest
|
||||
* формат ISO
|
||||
* стандарт JSON Schema
|
||||
* JSON schema
|
||||
* визначення schema
|
||||
* Password Flow
|
||||
* Mobile
|
||||
|
||||
* застарілий
|
||||
* спроєктований
|
||||
* недійсний
|
||||
* на льоту
|
||||
* стандартний
|
||||
* за замовчуванням
|
||||
* чутливий до регістру
|
||||
* нечутливий до регістру
|
||||
|
||||
* обслуговувати застосунок
|
||||
* обслуговувати сторінку
|
||||
|
||||
* застосунок
|
||||
* застосунок
|
||||
|
||||
* запит
|
||||
* відповідь
|
||||
* відповідь про помилку
|
||||
|
||||
* операція шляху
|
||||
* декоратор операції шляху
|
||||
* функція операції шляху
|
||||
|
||||
* body
|
||||
* тіло запиту
|
||||
* тіло відповіді
|
||||
* JSON body
|
||||
* form body
|
||||
* file body
|
||||
* тіло функції
|
||||
|
||||
* параметр
|
||||
* body-параметр
|
||||
* path-параметр
|
||||
* query-параметр
|
||||
* cookie-параметр
|
||||
* header-параметр
|
||||
* form-параметр
|
||||
* параметр функції
|
||||
|
||||
* подія
|
||||
* подія startup
|
||||
* запуск сервера
|
||||
* подія shutdown
|
||||
* подія lifespan
|
||||
|
||||
* обробник
|
||||
* обробник подій
|
||||
* обробник винятків
|
||||
* обробляти
|
||||
|
||||
* модель
|
||||
* модель Pydantic
|
||||
* модель даних
|
||||
* модель бази даних
|
||||
* модель форми
|
||||
* об’єкт моделі
|
||||
|
||||
* клас
|
||||
* базовий клас
|
||||
* батьківський клас
|
||||
* підклас
|
||||
* дочірній клас
|
||||
* споріднений клас
|
||||
* метод класу
|
||||
|
||||
* заголовок
|
||||
* заголовки
|
||||
* заголовок authorization
|
||||
* заголовок `Authorization`
|
||||
* forwarded header
|
||||
|
||||
* система dependency injection
|
||||
* залежність
|
||||
* dependable
|
||||
* dependant
|
||||
|
||||
* I/O bound
|
||||
* CPU bound
|
||||
* concurrency
|
||||
* parallelism
|
||||
* multiprocessing
|
||||
|
||||
* env var
|
||||
* змінна оточення
|
||||
* `PATH`
|
||||
* змінна `PATH`
|
||||
|
||||
* автентифікація
|
||||
* провайдер автентифікації
|
||||
* авторизація
|
||||
* форма авторизації
|
||||
* провайдер авторизації
|
||||
* користувач автентифікується
|
||||
* система автентифікує користувача
|
||||
|
||||
* CLI
|
||||
* інтерфейс командного рядка
|
||||
|
||||
* сервер
|
||||
* клієнт
|
||||
|
||||
* хмарний провайдер
|
||||
* хмарний сервіс
|
||||
|
||||
* розробка
|
||||
* етапи розробки
|
||||
|
||||
* dict
|
||||
* словник
|
||||
* перелічення
|
||||
* enum
|
||||
* елемент enum
|
||||
|
||||
* енкодер
|
||||
* декодер
|
||||
* кодувати
|
||||
* декодувати
|
||||
|
||||
* виняток
|
||||
* підняти
|
||||
|
||||
* вираз
|
||||
* інструкція
|
||||
|
||||
* frontend
|
||||
* backend
|
||||
|
||||
* обговорення GitHub
|
||||
* issue GitHub
|
||||
|
||||
* продуктивність
|
||||
* оптимізація продуктивності
|
||||
|
||||
* тип повернення
|
||||
* значення повернення
|
||||
|
||||
* безпека
|
||||
* схема безпеки
|
||||
|
||||
* завдання
|
||||
* фонове завдання
|
||||
* функція завдання
|
||||
|
||||
* шаблон
|
||||
* рушій шаблонів
|
||||
|
||||
* анотація типів
|
||||
* підказка типів
|
||||
|
||||
* server worker
|
||||
* worker Uvicorn
|
||||
* Worker Gunicorn
|
||||
* процес worker
|
||||
* клас worker
|
||||
* робоче навантаження
|
||||
|
||||
* розгортання
|
||||
* розгортати
|
||||
|
||||
* SDK
|
||||
* software development kit
|
||||
|
||||
* `APIRouter`
|
||||
* `requirements.txt`
|
||||
* Bearer Token
|
||||
* breaking change
|
||||
* bug
|
||||
* button
|
||||
* callable
|
||||
* code
|
||||
* commit
|
||||
* context manager
|
||||
* coroutine
|
||||
* сесія бази даних
|
||||
* диск
|
||||
* домен
|
||||
* engine
|
||||
* fake X
|
||||
* метод HTTP GET
|
||||
* item
|
||||
* бібліотека
|
||||
* lifespan
|
||||
* lock
|
||||
* middleware
|
||||
* мобільний застосунок
|
||||
* модуль
|
||||
* mounting
|
||||
* мережа
|
||||
* origin
|
||||
* override
|
||||
* payload
|
||||
* processor
|
||||
* property
|
||||
* proxy
|
||||
* pull request
|
||||
* query
|
||||
* RAM
|
||||
* віддалена машина
|
||||
* status code
|
||||
* string
|
||||
* tag
|
||||
* вебфреймворк
|
||||
* wildcard
|
||||
* повертати
|
||||
* валідувати
|
||||
|
||||
////
|
||||
|
||||
//// tab | Інформація
|
||||
|
||||
Це неповний і не нормативний список (переважно) технічних термінів, які зустрічаються в документації. Він може бути корисним автору prompt, щоб зрозуміти, для яких термінів LLM потрібна підказка. Наприклад, коли він постійно відкотить якісний переклад до гіршого варіанта. Або коли він має проблеми з відмінюванням/узгодженням терміна вашою мовою.
|
||||
|
||||
Див., напр., розділ `### List of English terms and their preferred German translations` у `docs/de/llm-prompt.md`.
|
||||
|
||||
////
|
||||
3
docs/uk/docs/about/index.md
Normal file
3
docs/uk/docs/about/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Про { #about }
|
||||
|
||||
Про FastAPI, його дизайн, натхнення та інше. 🤓
|
||||
247
docs/uk/docs/advanced/additional-responses.md
Normal file
247
docs/uk/docs/advanced/additional-responses.md
Normal file
@@ -0,0 +1,247 @@
|
||||
# Додаткові відповіді в OpenAPI { #additional-responses-in-openapi }
|
||||
|
||||
/// warning | Попередження
|
||||
|
||||
Це доволі просунута тема.
|
||||
|
||||
Якщо ви тільки починаєте з **FastAPI**, вам це може бути не потрібно.
|
||||
|
||||
///
|
||||
|
||||
Ви можете оголошувати додаткові відповіді з додатковими кодами статусу, media types, описами тощо.
|
||||
|
||||
Ці додаткові відповіді буде включено до схеми OpenAPI, тож вони також з’являться в документації API.
|
||||
|
||||
Але для цих додаткових відповідей вам потрібно переконатися, що ви повертаєте `Response`, як-от `JSONResponse`, безпосередньо, із вашим кодом статусу та вмістом.
|
||||
|
||||
## Додаткова відповідь із `model` { #additional-response-with-model }
|
||||
|
||||
Ви можете передати в *декоратори операції шляху* параметр `responses`.
|
||||
|
||||
Він приймає `dict`: ключі — це коди статусу для кожної відповіді (наприклад, `200`), а значення — інші `dict` з інформацією для кожної з них.
|
||||
|
||||
Кожен із цих `dict` відповіді може мати ключ `model`, що містить Pydantic model, так само як `response_model`.
|
||||
|
||||
**FastAPI** візьме цю модель, згенерує її JSON Schema та включить у правильне місце в OpenAPI.
|
||||
|
||||
Наприклад, щоб оголосити ще одну відповідь із кодом статусу `404` і Pydantic model `Message`, ви можете написати:
|
||||
|
||||
{* ../../docs_src/additional_responses/tutorial001_py39.py hl[18,22] *}
|
||||
|
||||
/// note | Примітка
|
||||
|
||||
Пам’ятайте, що потрібно повертати `JSONResponse` безпосередньо.
|
||||
|
||||
///
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Ключ `model` не є частиною OpenAPI.
|
||||
|
||||
**FastAPI** візьме Pydantic model звідти, згенерує JSON Schema і помістить його в правильне місце.
|
||||
|
||||
Правильне місце:
|
||||
|
||||
* У ключі `content`, значенням якого є інший JSON-об’єкт (`dict`), що містить:
|
||||
* Ключ із media type, наприклад `application/json`, значенням якого є інший JSON-об’єкт, що містить:
|
||||
* Ключ `schema`, значенням якого є JSON Schema з моделі — ось правильне місце.
|
||||
* **FastAPI** додає тут посилання на глобальні JSON Schemas в іншому місці вашого OpenAPI, замість того щоб включати схему безпосередньо. Так інші застосунки та клієнти можуть використовувати ці JSON Schemas напряму, надавати кращі інструменти генерації коду тощо.
|
||||
|
||||
///
|
||||
|
||||
Згенеровані відповіді в OpenAPI для цієї *операції шляху* будуть:
|
||||
|
||||
```JSON hl_lines="3-12"
|
||||
{
|
||||
"responses": {
|
||||
"404": {
|
||||
"description": "Additional Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Item"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Схеми мають посилання на інше місце всередині схеми OpenAPI:
|
||||
|
||||
```JSON hl_lines="4-16"
|
||||
{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Message": {
|
||||
"title": "Message",
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"title": "Message",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Item": {
|
||||
"title": "Item",
|
||||
"required": [
|
||||
"id",
|
||||
"value"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"title": "Id",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"title": "Value",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": [
|
||||
"loc",
|
||||
"msg",
|
||||
"type"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"msg": {
|
||||
"title": "Message",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"title": "Error Type",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Додаткові media types для основної відповіді { #additional-media-types-for-the-main-response }
|
||||
|
||||
Ви можете використати цей самий параметр `responses`, щоб додати різні media types для тієї самої основної відповіді.
|
||||
|
||||
Наприклад, ви можете додати додатковий media type `image/png`, оголосивши, що ваша *операція шляху* може повертати JSON-об’єкт (із media type `application/json`) або PNG-зображення:
|
||||
|
||||
{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *}
|
||||
|
||||
/// note | Примітка
|
||||
|
||||
Зверніть увагу, що потрібно повертати зображення безпосередньо через `FileResponse`.
|
||||
|
||||
///
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Якщо ви явно не вкажете інший media type у параметрі `responses`, FastAPI вважатиме, що відповідь має той самий media type, що й основний клас відповіді (типово `application/json`).
|
||||
|
||||
Але якщо ви вказали власний клас відповіді з `None` як media type, FastAPI використає `application/json` для будь-якої додаткової відповіді, що має пов’язану модель.
|
||||
|
||||
///
|
||||
|
||||
## Об’єднання інформації { #combining-information }
|
||||
|
||||
Ви також можете об’єднувати інформацію про відповіді з кількох місць, зокрема з параметрів `response_model`, `status_code` і `responses`.
|
||||
|
||||
Ви можете оголосити `response_model`, використовуючи типовий код статусу `200` (або власний, якщо потрібно), а потім оголосити додаткову інформацію для цієї ж відповіді в `responses` — безпосередньо в схемі OpenAPI.
|
||||
|
||||
**FastAPI** збереже додаткову інформацію з `responses` і об’єднає її з JSON Schema вашої моделі.
|
||||
|
||||
Наприклад, ви можете оголосити відповідь із кодом статусу `404`, яка використовує Pydantic model і має власний `description`.
|
||||
|
||||
А також відповідь із кодом статусу `200`, яка використовує ваш `response_model`, але містить власний `example`:
|
||||
|
||||
{* ../../docs_src/additional_responses/tutorial003_py39.py hl[20:31] *}
|
||||
|
||||
Усе це буде об’єднано й включено до вашого OpenAPI та показано в документації API:
|
||||
|
||||
<img src="/img/tutorial/additional-responses/image01.png">
|
||||
|
||||
## Поєднання попередньо визначених відповідей і власних { #combine-predefined-responses-and-custom-ones }
|
||||
|
||||
Можливо, ви хочете мати деякі попередньо визначені відповіді, що застосовуються до багатьох *операцій шляху*, але при цьому поєднати їх із власними відповідями, потрібними для кожної *операції шляху*.
|
||||
|
||||
Для таких випадків можна використати Python-техніку «розпакування» `dict` за допомогою `**dict_to_unpack`:
|
||||
|
||||
```Python
|
||||
old_dict = {
|
||||
"old key": "old value",
|
||||
"second old key": "second old value",
|
||||
}
|
||||
new_dict = {**old_dict, "new key": "new value"}
|
||||
```
|
||||
|
||||
Тут `new_dict` міститиме всі пари ключ-значення з `old_dict`, плюс нову пару ключ-значення:
|
||||
|
||||
```Python
|
||||
{
|
||||
"old key": "old value",
|
||||
"second old key": "second old value",
|
||||
"new key": "new value",
|
||||
}
|
||||
```
|
||||
|
||||
Ви можете застосувати цю техніку, щоб повторно використати деякі попередньо визначені відповіді у ваших *операціях шляху* та поєднати їх із додатковими власними.
|
||||
|
||||
Наприклад:
|
||||
|
||||
{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *}
|
||||
|
||||
## Докладніше про відповіді OpenAPI { #more-information-about-openapi-responses }
|
||||
|
||||
Щоб побачити, що саме можна включати до відповідей, перегляньте ці розділи специфікації OpenAPI:
|
||||
|
||||
* <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#responses-object" class="external-link" target="_blank">OpenAPI Responses Object</a> — містить `Response Object`.
|
||||
* <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#response-object" class="external-link" target="_blank">OpenAPI Response Object</a> — ви можете включити будь-що з цього безпосередньо в кожну відповідь у параметрі `responses`, зокрема `description`, `headers`, `content` (усередині нього ви оголошуєте різні media types і JSON Schemas) та `links`.
|
||||
41
docs/uk/docs/advanced/additional-status-codes.md
Normal file
41
docs/uk/docs/advanced/additional-status-codes.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Додаткові коди статусу { #additional-status-codes }
|
||||
|
||||
За замовчуванням **FastAPI** повертатиме відповіді, використовуючи `JSONResponse`, поміщаючи вміст, який ви повертаєте з вашої *операції шляху*, всередину цього `JSONResponse`.
|
||||
|
||||
Він використає код статусу за замовчуванням або той, який ви встановили у вашій *операції шляху*.
|
||||
|
||||
## Додаткові коди статусу { #additional-status-codes_1 }
|
||||
|
||||
Якщо ви хочете повертати додаткові коди статусу, окрім основного, ви можете зробити це, повертаючи `Response` безпосередньо, наприклад `JSONResponse`, і встановити додатковий код статусу напряму.
|
||||
|
||||
Наприклад, припустімо, що ви хочете мати *операцію шляху*, яка дозволяє оновлювати елементи, і повертає HTTP-код статусу 200 «OK» у разі успіху.
|
||||
|
||||
Але ви також хочете, щоб вона приймала нові елементи. І коли елементів раніше не існувало, вона створює їх і повертає HTTP-код статусу 201 «Created».
|
||||
|
||||
Щоб цього досягти, імпортуйте `JSONResponse` і повертайте ваш вміст безпосередньо там, встановивши `status_code`, який вам потрібен:
|
||||
|
||||
{* ../../docs_src/additional_status_codes/tutorial001_an_py310.py hl[4,25] *}
|
||||
|
||||
/// warning | Попередження
|
||||
|
||||
Коли ви повертаєте `Response` безпосередньо, як у прикладі вище, його буде повернуто напряму.
|
||||
|
||||
Його не буде серіалізовано моделлю тощо.
|
||||
|
||||
Переконайтеся, що він містить дані, які ви хочете повернути, і що значення є валідним JSON (якщо ви використовуєте `JSONResponse`).
|
||||
|
||||
///
|
||||
|
||||
/// note | Технічні деталі
|
||||
|
||||
Ви також можете використати `from starlette.responses import JSONResponse`.
|
||||
|
||||
**FastAPI** надає ті самі `starlette.responses` як `fastapi.responses` просто для вашої зручності як розробника. Але більшість доступних відповідей надходять безпосередньо зі Starlette. Те саме стосується `status`.
|
||||
|
||||
///
|
||||
|
||||
## OpenAPI і документація API { #openapi-and-api-docs }
|
||||
|
||||
Якщо ви повертаєте додаткові коди статусу та відповіді безпосередньо, їх не буде включено до схеми OpenAPI (документації API), адже FastAPI не має способу заздалегідь знати, що саме ви збираєтеся повертати.
|
||||
|
||||
Але ви можете задокументувати це у вашому коді, використовуючи: [Додаткові відповіді](additional-responses.md){.internal-link target=_blank}.
|
||||
163
docs/uk/docs/advanced/advanced-dependencies.md
Normal file
163
docs/uk/docs/advanced/advanced-dependencies.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# Розширені залежності { #advanced-dependencies }
|
||||
|
||||
## Параметризовані залежності { #parameterized-dependencies }
|
||||
|
||||
Усі залежності, які ми бачили, — це фіксована функція або клас.
|
||||
|
||||
Але можуть бути випадки, коли ви хочете мати змогу задавати параметри для залежності, не оголошуючи багато різних функцій або класів.
|
||||
|
||||
Уявімо, що ми хочемо мати залежність, яка перевіряє, чи містить параметр запиту `q` певний фіксований вміст.
|
||||
|
||||
Але ми хочемо мати змогу параметризувати цей фіксований вміст.
|
||||
|
||||
## «Callable» екземпляр { #a-callable-instance }
|
||||
|
||||
У Python є спосіб зробити екземпляр класу «callable».
|
||||
|
||||
Не сам клас (який уже є callable), а екземпляр цього класу.
|
||||
|
||||
Для цього ми оголошуємо метод `__call__`:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[12] *}
|
||||
|
||||
У цьому випадку саме `__call__` **FastAPI** використовуватиме, щоб перевіряти додаткові параметри та підзалежності, і саме його буде викликано, щоб передати значення параметру у вашій *функції операції шляху* пізніше.
|
||||
|
||||
## Параметризуйте екземпляр { #parameterize-the-instance }
|
||||
|
||||
А тепер ми можемо використати `__init__`, щоб оголосити параметри екземпляра, які ми зможемо використати для «параметризації» залежності:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[9] *}
|
||||
|
||||
У цьому випадку **FastAPI** ніколи не чіпатиме й не «цікавитиметься» `__init__` — ми використаємо його безпосередньо у своєму коді.
|
||||
|
||||
## Створіть екземпляр { #create-an-instance }
|
||||
|
||||
Ми можемо створити екземпляр цього класу так:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[18] *}
|
||||
|
||||
Таким чином ми можемо «параметризувати» нашу залежність, яка тепер містить `"bar"` як атрибут `checker.fixed_content`.
|
||||
|
||||
## Використайте екземпляр як залежність { #use-the-instance-as-a-dependency }
|
||||
|
||||
Далі ми можемо використати цей `checker` у `Depends(checker)` замість `Depends(FixedContentQueryChecker)`, адже залежністю є екземпляр `checker`, а не сам клас.
|
||||
|
||||
І під час розв’язання залежності **FastAPI** викличе цей `checker` так:
|
||||
|
||||
```Python
|
||||
checker(q="somequery")
|
||||
```
|
||||
|
||||
...і передасть те, що він поверне, як значення залежності у нашій *функції операції шляху* в параметр `fixed_content_included`:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[22] *}
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Усе це може здаватися надуманим. І поки що може бути не дуже зрозуміло, у чому користь.
|
||||
|
||||
Ці приклади навмисно прості, але показують, як це все працює.
|
||||
|
||||
У розділах про безпеку є допоміжні функції, які реалізовані так само.
|
||||
|
||||
Якщо ви все це зрозуміли, то вже знаєте, як «під капотом» працюють ті утиліти для безпеки.
|
||||
|
||||
///
|
||||
|
||||
## Залежності з `yield`, `HTTPException`, `except` і Background Tasks { #dependencies-with-yield-httpexception-except-and-background-tasks }
|
||||
|
||||
/// warning | Попередження
|
||||
|
||||
Швидше за все, вам не потрібні ці технічні деталі.
|
||||
|
||||
Вони корисні переважно, якщо у вас був застосунок FastAPI старіший за 0.121.0 і ви стикаєтесь із проблемами в залежностях з `yield`.
|
||||
|
||||
///
|
||||
|
||||
Залежності з `yield` еволюціонували з часом, щоб врахувати різні сценарії використання та виправити деякі проблеми. Нижче наведено підсумок змін.
|
||||
|
||||
### Залежності з `yield` і `scope` { #dependencies-with-yield-and-scope }
|
||||
|
||||
У версії 0.121.0 FastAPI додав підтримку `Depends(scope="function")` для залежностей з `yield`.
|
||||
|
||||
Використовуючи `Depends(scope="function")`, вихідний код після `yield` виконується одразу після завершення *функції операції шляху*, до того як відповідь буде надіслана клієнту.
|
||||
|
||||
А при використанні `Depends(scope="request")` (за замовчуванням) вихідний код після `yield` виконується після того, як відповідь надіслано.
|
||||
|
||||
Докладніше про це читайте в документації: [Залежності з `yield` — Ранній вихід і `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope).
|
||||
|
||||
### Залежності з `yield` і `StreamingResponse`, технічні деталі { #dependencies-with-yield-and-streamingresponse-technical-details }
|
||||
|
||||
До FastAPI 0.118.0, якщо ви використовували залежність з `yield`, вихідний код виконувався після того, як *функція операції шляху* повертала результат, але прямо перед надсиланням відповіді.
|
||||
|
||||
Мета була — не утримувати ресурси довше, ніж потрібно, очікуючи, поки відповідь пройде мережею.
|
||||
|
||||
Ця зміна також означала, що якщо ви повертали `StreamingResponse`, вихідний код залежності з `yield` уже був би виконаний.
|
||||
|
||||
Наприклад, якщо у вас була сесія бази даних у залежності з `yield`, то `StreamingResponse` не зміг би використовувати цю сесію під час стримінгу даних, бо сесію вже було б закрито у вихідному коді після `yield`.
|
||||
|
||||
Цю поведінку було повернуто у 0.118.0, щоб вихідний код після `yield` виконувався після надсилання відповіді.
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Як ви побачите нижче, це дуже схоже на поведінку до версії 0.106.0, але з кількома покращеннями та виправленнями помилок для крайніх випадків.
|
||||
|
||||
///
|
||||
|
||||
#### Сценарії з раннім виконанням вихідного коду { #use-cases-with-early-exit-code }
|
||||
|
||||
Є деякі сценарії з особливими умовами, у яких могла б бути корисною стара поведінка — виконувати вихідний код залежностей з `yield` перед надсиланням відповіді.
|
||||
|
||||
Наприклад, уявімо, що у вас є код, який використовує сесію бази даних у залежності з `yield` лише для перевірки користувача, але сесія більше ніколи не використовується у *функції операції шляху* — лише в залежності — **і** відповідь надсилається довго, як `StreamingResponse`, що повільно передає дані, але з певної причини не використовує базу даних.
|
||||
|
||||
У такому випадку сесія бази даних утримуватиметься до завершення надсилання відповіді, але якщо ви її не використовуєте, то це не потрібно.
|
||||
|
||||
Ось як це могло б виглядати:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial013_an_py310.py *}
|
||||
|
||||
Вихідний код, автоматичне закриття `Session` у:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *}
|
||||
|
||||
...було б виконано після того, як відповідь завершить надсилати повільні дані:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *}
|
||||
|
||||
Але оскільки `generate_stream()` не використовує сесію бази даних, насправді немає потреби тримати сесію відкритою під час надсилання відповіді.
|
||||
|
||||
Якщо у вас є саме такий сценарій із SQLModel (або SQLAlchemy), ви можете явно закрити сесію після того, як вона вам більше не потрібна:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial014_an_py310.py ln[24:28] hl[28] *}
|
||||
|
||||
Тоді сесія вивільнить з’єднання з базою даних, і його зможуть використати інші запити.
|
||||
|
||||
Якщо у вас є інший сценарій, що потребує раннього виходу із залежності з `yield`, будь ласка, створіть <a href="https://github.com/fastapi/fastapi/discussions/new?category=questions" class="external-link" target="_blank">питання в GitHub Discussions</a> з вашим конкретним сценарієм і поясненням, чому вам було б корисне раннє закриття для залежностей з `yield`.
|
||||
|
||||
Якщо знайдуться переконливі сценарії для раннього закриття в залежностях з `yield`, я розгляну можливість додати новий спосіб явно вмикати раннє закриття.
|
||||
|
||||
### Залежності з `yield` і `except`, технічні деталі { #dependencies-with-yield-and-except-technical-details }
|
||||
|
||||
До FastAPI 0.110.0, якщо ви використовували залежність з `yield`, а потім перехоплювали виняток через `except` у цій залежності і не піднімали виняток знову, виняток автоматично піднімався/передавався будь-яким обробникам винятків або обробнику внутрішньої помилки сервера.
|
||||
|
||||
У версії 0.110.0 це було змінено, щоб виправити неконтрольоване споживання пам’яті через переспрямовані винятки без обробника (внутрішні помилки сервера), і щоб узгодити поведінку зі звичайним Python-кодом.
|
||||
|
||||
### Background Tasks і залежності з `yield`, технічні деталі { #background-tasks-and-dependencies-with-yield-technical-details }
|
||||
|
||||
До FastAPI 0.106.0 піднімати винятки після `yield` було неможливо: вихідний код у залежностях з `yield` виконувався *після* надсилання відповіді, тож [Обробники винятків](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} уже відпрацьовували.
|
||||
|
||||
Так було спроєктовано переважно для того, щоб дозволити використовувати ті самі об’єкти, «yield»-нуті залежностями, усередині фонових завдань, бо вихідний код виконувався після завершення фонових завдань.
|
||||
|
||||
Це було змінено у FastAPI 0.106.0 з наміром не утримувати ресурси під час очікування, поки відповідь пройде мережею.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Крім того, фонове завдання зазвичай є незалежним набором логіки, який слід обробляти окремо, з власними ресурсами (наприклад, власним з’єднанням з базою даних).
|
||||
|
||||
Тож у такий спосіб ваш код, імовірно, буде чистішим.
|
||||
|
||||
///
|
||||
|
||||
Якщо ви раніше покладалися на таку поведінку, тепер вам слід створювати ресурси для фонових завдань усередині самого фонового завдання та використовувати всередині лише дані, які не залежать від ресурсів залежностей з `yield`.
|
||||
|
||||
Наприклад, замість використання тієї самої сесії бази даних, ви створите нову сесію бази даних усередині фонового завдання і отримаєте об’єкти з бази даних, використовуючи цю нову сесію. А потім, замість передачі об’єкта з бази даних як параметра у функцію фонового завдання, ви передасте ID цього об’єкта й повторно отримаєте об’єкт усередині функції фонового завдання.
|
||||
99
docs/uk/docs/advanced/async-tests.md
Normal file
99
docs/uk/docs/advanced/async-tests.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Асинхронні тести { #async-tests }
|
||||
|
||||
Ви вже бачили, як тестувати ваші застосунки **FastAPI** за допомогою наданого `TestClient`. До цього моменту ви бачили лише те, як писати синхронні тести, без використання `async`-функцій.
|
||||
|
||||
Можливість використовувати асинхронні функції у ваших тестах може бути корисною, наприклад, коли ви асинхронно виконуєте запити до бази даних. Уявіть, що ви хочете протестувати надсилання запитів до вашого застосунку FastAPI, а потім перевірити, що ваш backend успішно записав правильні дані в базу даних, використовуючи async-бібліотеку для роботи з БД.
|
||||
|
||||
Подивімося, як це реалізувати.
|
||||
|
||||
## pytest.mark.anyio { #pytest-mark-anyio }
|
||||
|
||||
Якщо ми хочемо викликати асинхронні функції в наших тестах, наші тестові функції теж мають бути асинхронними. AnyIO надає зручний плагін для цього, який дозволяє нам вказати, що деякі тестові функції слід викликати асинхронно.
|
||||
|
||||
## HTTPX { #httpx }
|
||||
|
||||
Навіть якщо ваш застосунок **FastAPI** використовує звичайні `def`-функції замість `async def`, під капотом це все одно `async`-застосунок.
|
||||
|
||||
`TestClient` виконує певну «магію» всередині, щоб викликати асинхронний застосунок FastAPI у ваших звичайних `def` тестових функціях, використовуючи стандартний pytest. Але ця магія більше не працює, коли ми використовуємо його всередині асинхронних функцій. Запускаючи наші тести асинхронно, ми більше не можемо використовувати `TestClient` усередині тестових функцій.
|
||||
|
||||
`TestClient` базується на <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a>, і, на щастя, ми можемо використовувати його напряму для тестування API.
|
||||
|
||||
## Приклад { #example }
|
||||
|
||||
Для простого прикладу розгляньмо структуру файлів, схожу на описану в [Більші застосунки](../tutorial/bigger-applications.md){.internal-link target=_blank} та [Тестування](../tutorial/testing.md){.internal-link target=_blank}:
|
||||
|
||||
```
|
||||
.
|
||||
├── app
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py
|
||||
│ └── test_main.py
|
||||
```
|
||||
|
||||
Файл `main.py` матиме:
|
||||
|
||||
{* ../../docs_src/async_tests/app_a_py39/main.py *}
|
||||
|
||||
Файл `test_main.py` матиме тести для `main.py`, і тепер він може виглядати так:
|
||||
|
||||
{* ../../docs_src/async_tests/app_a_py39/test_main.py *}
|
||||
|
||||
## Запуск { #run-it }
|
||||
|
||||
Ви можете запускати тести як зазвичай, через:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ pytest
|
||||
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Детальніше { #in-detail }
|
||||
|
||||
Маркер `@pytest.mark.anyio` повідомляє pytest, що цю тестову функцію потрібно викликати асинхронно:
|
||||
|
||||
{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[7] *}
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Зверніть увагу, що тепер тестова функція — це `async def`, а не просто `def`, як раніше при використанні `TestClient`.
|
||||
|
||||
///
|
||||
|
||||
Потім ми можемо створити `AsyncClient` із застосунком і надсилати до нього async-запити, використовуючи `await`.
|
||||
|
||||
{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[9:12] *}
|
||||
|
||||
Це еквівалентно:
|
||||
|
||||
```Python
|
||||
response = client.get('/')
|
||||
```
|
||||
|
||||
...що ми використовували для виконання запитів через `TestClient`.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Зверніть увагу, що з новим `AsyncClient` ми використовуємо async/await — запит є асинхронним.
|
||||
|
||||
///
|
||||
|
||||
/// warning | Попередження
|
||||
|
||||
Якщо ваш застосунок покладається на події життєвого циклу (lifespan events), `AsyncClient` не запускатиме ці події. Щоб гарантувати їх запуск, використовуйте `LifespanManager` з <a href="https://github.com/florimondmanca/asgi-lifespan#usage" class="external-link" target="_blank">florimondmanca/asgi-lifespan</a>.
|
||||
|
||||
///
|
||||
|
||||
## Інші виклики асинхронних функцій { #other-asynchronous-function-calls }
|
||||
|
||||
Оскільки тестова функція тепер асинхронна, ви також можете викликати (і `await`-ити) інші `async`-функції, окрім надсилання запитів до вашого застосунку FastAPI в тестах — так само, як ви викликали б їх будь-де в іншому місці вашого коду.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Якщо під час інтеграції викликів асинхронних функцій у тести ви стикаєтеся з `RuntimeError: Task attached to a different loop` (наприклад, при використанні <a href="https://stackoverflow.com/questions/41584243/runtimeerror-task-attached-to-a-different-loop" class="external-link" target="_blank">MongoDB's MotorClient</a>), пам’ятайте: об’єкти, яким потрібен event loop, слід створювати лише всередині async-функцій, наприклад, у callback `@app.on_event("startup")`.
|
||||
|
||||
///
|
||||
466
docs/uk/docs/advanced/behind-a-proxy.md
Normal file
466
docs/uk/docs/advanced/behind-a-proxy.md
Normal file
@@ -0,0 +1,466 @@
|
||||
# За проксі { #behind-a-proxy }
|
||||
|
||||
У багатьох ситуаціях перед вашим застосунком FastAPI ви використовуватимете **проксі**, як-от Traefik або Nginx.
|
||||
|
||||
Такі проксі можуть обробляти сертифікати HTTPS та інші речі.
|
||||
|
||||
## Forwarded-заголовки проксі { #proxy-forwarded-headers }
|
||||
|
||||
**Проксі** перед вашим застосунком зазвичай «на льоту» встановлює деякі заголовки перед надсиланням запитів на ваш **сервер**, щоб повідомити серверу, що запит був **переспрямований** проксі, і передати початковий (публічний) URL, включно з доменом, що використовується HTTPS тощо.
|
||||
|
||||
Програма **сервера** (наприклад, **Uvicorn** через **FastAPI CLI**) уміє інтерпретувати ці заголовки, а потім передавати цю інформацію вашому застосунку.
|
||||
|
||||
Але з міркувань безпеки, оскільки сервер не знає, що він знаходиться за довіреним проксі, він не інтерпретуватиме ці заголовки.
|
||||
|
||||
/// note | Технічні деталі
|
||||
|
||||
Заголовки проксі:
|
||||
|
||||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For" class="external-link" target="_blank">X-Forwarded-For</a>
|
||||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto" class="external-link" target="_blank">X-Forwarded-Proto</a>
|
||||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Host" class="external-link" target="_blank">X-Forwarded-Host</a>
|
||||
|
||||
///
|
||||
|
||||
### Увімкнення Forwarded-заголовків проксі { #enable-proxy-forwarded-headers }
|
||||
|
||||
Ви можете запустити FastAPI CLI з *CLI Option* `--forwarded-allow-ips` і передати IP-адреси, яким слід довіряти для читання цих forwarded-заголовків.
|
||||
|
||||
Якщо встановити `--forwarded-allow-ips="*"`, довіра буде до всіх вхідних IP.
|
||||
|
||||
Якщо ваш **сервер** знаходиться за довіреним **проксі** і лише проксі звертається до нього, це змусить сервер приймати як «дозволену» ту IP-адресу, з якої приходять запити до сервера — тобто IP цього **проксі**.
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi run --forwarded-allow-ips="*"
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
### Редіректи з HTTPS { #redirects-with-https }
|
||||
|
||||
Наприклад, припустімо, ви визначили *операцію шляху* `/items/`:
|
||||
|
||||
{* ../../docs_src/behind_a_proxy/tutorial001_01_py39.py hl[6] *}
|
||||
|
||||
Якщо клієнт спробує перейти на `/items`, за замовчуванням його буде перенаправлено на `/items/`.
|
||||
|
||||
Але до встановлення *CLI Option* `--forwarded-allow-ips` редірект може вести на `http://localhost:8000/items/`.
|
||||
|
||||
Та, можливо, ваш застосунок розміщено на `https://mysuperapp.com`, і перенаправлення має вести на `https://mysuperapp.com/items/`.
|
||||
|
||||
Після встановлення `--proxy-headers` FastAPI зможе перенаправляти на правильне місце. 😎
|
||||
|
||||
```
|
||||
https://mysuperapp.com/items/
|
||||
```
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Якщо хочете дізнатися більше про HTTPS, перегляньте посібник [Про HTTPS](../deployment/https.md){.internal-link target=_blank}.
|
||||
|
||||
///
|
||||
|
||||
### Як працюють Forwarded-заголовки проксі { #how-proxy-forwarded-headers-work }
|
||||
|
||||
Ось візуальне представлення того, як **проксі** додає forwarded-заголовки між клієнтом і **сервером застосунку**:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Proxy as Proxy/Load Balancer
|
||||
participant Server as FastAPI Server
|
||||
|
||||
Client->>Proxy: HTTPS Request<br/>Host: mysuperapp.com<br/>Path: /items
|
||||
|
||||
Note over Proxy: Proxy adds forwarded headers
|
||||
|
||||
Proxy->>Server: HTTP Request<br/>X-Forwarded-For: [client IP]<br/>X-Forwarded-Proto: https<br/>X-Forwarded-Host: mysuperapp.com<br/>Path: /items
|
||||
|
||||
Note over Server: Server interprets headers<br/>(if --forwarded-allow-ips is set)
|
||||
|
||||
Server->>Proxy: HTTP Response<br/>with correct HTTPS URLs
|
||||
|
||||
Proxy->>Client: HTTPS Response
|
||||
```
|
||||
|
||||
**Проксі** перехоплює початковий запит клієнта й додає спеціальні *forwarded*-заголовки (`X-Forwarded-*`) перед тим, як передати запит на **сервер застосунку**.
|
||||
|
||||
Ці заголовки зберігають інформацію про початковий запит, яка інакше була б втрачена:
|
||||
|
||||
* **X-Forwarded-For**: IP-адреса початкового клієнта
|
||||
* **X-Forwarded-Proto**: початковий протокол (`https`)
|
||||
* **X-Forwarded-Host**: початковий хост (`mysuperapp.com`)
|
||||
|
||||
Коли **FastAPI CLI** налаштовано з `--forwarded-allow-ips`, він довіряє цим заголовкам і використовує їх, наприклад, щоб генерувати правильні URL в редіректах.
|
||||
|
||||
## Проксі зі «зрізаним» префіксом шляху { #proxy-with-a-stripped-path-prefix }
|
||||
|
||||
У вас може бути проксі, який додає префікс шляху до вашого застосунку.
|
||||
|
||||
У таких випадках ви можете використати `root_path` для налаштування застосунку.
|
||||
|
||||
`root_path` — це механізм, наданий специфікацією ASGI (на якій побудований FastAPI через Starlette).
|
||||
|
||||
`root_path` використовується для обробки таких специфічних випадків.
|
||||
|
||||
Також він використовується внутрішньо під час монтування підзастосунків.
|
||||
|
||||
Наявність проксі зі «зрізаним» префіксом шляху в цьому випадку означає, що ви можете оголосити шлях `/app` у своєму коді, але потім додати шар зверху (проксі), який розмістить ваш застосунок **FastAPI** під шляхом на кшталт `/api/v1`.
|
||||
|
||||
У цьому разі початковий шлях `/app` фактично обслуговуватиметься за адресою `/api/v1/app`.
|
||||
|
||||
Попри те, що весь ваш код написано так, ніби існує лише `/app`.
|
||||
|
||||
{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[6] *}
|
||||
|
||||
І проксі буде **«зрізати»** **префікс шляху** на льоту перед передаванням запиту на сервер застосунку (ймовірно, Uvicorn через FastAPI CLI), залишаючи ваш застосунок переконаним, що його обслуговують на `/app`, тож вам не потрібно оновлювати весь код, додаючи префікс `/api/v1`.
|
||||
|
||||
До цього моменту все працюватиме як зазвичай.
|
||||
|
||||
Але потім, коли ви відкриєте вбудований UI документації (frontend), він очікуватиме отримати схему OpenAPI за адресою `/openapi.json`, а не `/api/v1/openapi.json`.
|
||||
|
||||
Тож frontend (який працює в браузері) спробує звернутися до `/openapi.json` і не зможе отримати схему OpenAPI.
|
||||
|
||||
Оскільки для нашого застосунку використовується проксі з префіксом шляху `/api/v1`, frontend має отримувати схему OpenAPI за адресою `/api/v1/openapi.json`.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
|
||||
browser("Browser")
|
||||
proxy["Proxy on http://0.0.0.0:9999/api/v1/app"]
|
||||
server["Server on http://127.0.0.1:8000/app"]
|
||||
|
||||
browser --> proxy
|
||||
proxy --> server
|
||||
```
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
IP `0.0.0.0` зазвичай використовують, щоб означити: програма слухає на всіх IP, доступних на цій машині/сервері.
|
||||
|
||||
///
|
||||
|
||||
UI документації також потрібна схема OpenAPI, щоб задекларувати, що цей API `server` розташований на `/api/v1` (за проксі). Наприклад:
|
||||
|
||||
```JSON hl_lines="4-8"
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
// More stuff here
|
||||
"servers": [
|
||||
{
|
||||
"url": "/api/v1"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
// More stuff here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
У цьому прикладі «Proxy» може бути чимось на кшталт **Traefik**. А сервером може бути FastAPI CLI з **Uvicorn**, який запускає ваш застосунок FastAPI.
|
||||
|
||||
### Надання `root_path` { #providing-the-root-path }
|
||||
|
||||
Щоб цього досягти, ви можете використати опцію командного рядка `--root-path`, наприклад:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Якщо ви використовуєте Hypercorn, у нього також є опція `--root-path`.
|
||||
|
||||
/// note | Технічні деталі
|
||||
|
||||
Специфікація ASGI визначає `root_path` для цього сценарію.
|
||||
|
||||
А опція командного рядка `--root-path` надає цей `root_path`.
|
||||
|
||||
///
|
||||
|
||||
### Перевірка поточного `root_path` { #checking-the-current-root-path }
|
||||
|
||||
Ви можете отримати поточний `root_path`, який використовується вашим застосунком для кожного запиту: він є частиною словника `scope` (це частина специфікації ASGI).
|
||||
|
||||
Тут ми включаємо його в повідомлення лише для демонстрації.
|
||||
|
||||
{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[8] *}
|
||||
|
||||
Далі, якщо ви запустите Uvicorn так:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Відповідь буде приблизно такою:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"message": "Hello World",
|
||||
"root_path": "/api/v1"
|
||||
}
|
||||
```
|
||||
|
||||
### Встановлення `root_path` у застосунку FastAPI { #setting-the-root-path-in-the-fastapi-app }
|
||||
|
||||
Альтернативно, якщо у вас немає способу передати опцію командного рядка на кшталт `--root-path` або еквівалентну, ви можете встановити параметр `root_path` під час створення застосунку FastAPI:
|
||||
|
||||
{* ../../docs_src/behind_a_proxy/tutorial002_py39.py hl[3] *}
|
||||
|
||||
Передавання `root_path` у `FastAPI` еквівалентне передаванню опції командного рядка `--root-path` до Uvicorn або Hypercorn.
|
||||
|
||||
### Про `root_path` { #about-root-path }
|
||||
|
||||
Майте на увазі: сервер (Uvicorn) не використовуватиме цей `root_path` ні для чого іншого, окрім передавання його застосунку.
|
||||
|
||||
Але якщо ви перейдете в браузері на <a href="http://127.0.0.1:8000/app" class="external-link" target="_blank">http://127.0.0.1:8000/app</a>, ви побачите звичайну відповідь:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"message": "Hello World",
|
||||
"root_path": "/api/v1"
|
||||
}
|
||||
```
|
||||
|
||||
Отже, він не очікуватиме доступу за адресою `http://127.0.0.1:8000/api/v1/app`.
|
||||
|
||||
Uvicorn очікуватиме, що проксі звертатиметься до Uvicorn за адресою `http://127.0.0.1:8000/app`, а додавання додаткового префікса `/api/v1` зверху — це вже відповідальність проксі.
|
||||
|
||||
## Про проксі зі «зрізаним» префіксом шляху { #about-proxies-with-a-stripped-path-prefix }
|
||||
|
||||
Майте на увазі: проксі зі «зрізаним» префіксом шляху — лише один зі способів налаштування.
|
||||
|
||||
Ймовірно, у багатьох випадках за замовчуванням проксі не матиме «зрізаного» префікса шляху.
|
||||
|
||||
У такому випадку (без «зрізаного» префікса) проксі слухатиме щось на кшталт `https://myawesomeapp.com`, і тоді, якщо браузер перейде на `https://myawesomeapp.com/api/v1/app`, а ваш сервер (наприклад, Uvicorn) слухає на `http://127.0.0.1:8000`, проксі (без «зрізаного» префікса шляху) звертатиметься до Uvicorn за тим самим шляхом: `http://127.0.0.1:8000/api/v1/app`.
|
||||
|
||||
## Локальне тестування з Traefik { #testing-locally-with-traefik }
|
||||
|
||||
Ви можете легко виконати цей експеримент локально зі «зрізаним» префіксом шляху, використовуючи <a href="https://docs.traefik.io/" class="external-link" target="_blank">Traefik</a>.
|
||||
|
||||
<a href="https://github.com/containous/traefik/releases" class="external-link" target="_blank">Завантажте Traefik</a> — це один бінарний файл; ви можете розпакувати стиснений файл і запустити його безпосередньо з термінала.
|
||||
|
||||
Потім створіть файл `traefik.toml` із таким вмістом:
|
||||
|
||||
```TOML hl_lines="3"
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":9999"
|
||||
|
||||
[providers]
|
||||
[providers.file]
|
||||
filename = "routes.toml"
|
||||
```
|
||||
|
||||
Це вказує Traefik слухати порт 9999 і використовувати інший файл `routes.toml`.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Ми використовуємо порт 9999 замість стандартного HTTP-порту 80, щоб вам не доводилося запускати його з правами адміністратора (`sudo`).
|
||||
|
||||
///
|
||||
|
||||
Тепер створіть той інший файл `routes.toml`:
|
||||
|
||||
```TOML hl_lines="5 12 20"
|
||||
[http]
|
||||
[http.middlewares]
|
||||
|
||||
[http.middlewares.api-stripprefix.stripPrefix]
|
||||
prefixes = ["/api/v1"]
|
||||
|
||||
[http.routers]
|
||||
|
||||
[http.routers.app-http]
|
||||
entryPoints = ["http"]
|
||||
service = "app"
|
||||
rule = "PathPrefix(`/api/v1`)"
|
||||
middlewares = ["api-stripprefix"]
|
||||
|
||||
[http.services]
|
||||
|
||||
[http.services.app]
|
||||
[http.services.app.loadBalancer]
|
||||
[[http.services.app.loadBalancer.servers]]
|
||||
url = "http://127.0.0.1:8000"
|
||||
```
|
||||
|
||||
Цей файл налаштовує Traefik на використання префікса шляху `/api/v1`.
|
||||
|
||||
І тоді Traefik переспрямовуватиме свої запити до вашого Uvicorn, що працює на `http://127.0.0.1:8000`.
|
||||
|
||||
Тепер запустіть Traefik:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ ./traefik --configFile=traefik.toml
|
||||
|
||||
INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
А тепер запустіть ваш застосунок, використовуючи опцію `--root-path`:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
### Перевірка відповідей { #check-the-responses }
|
||||
|
||||
Тепер, якщо ви перейдете на URL з портом Uvicorn: <a href="http://127.0.0.1:8000/app" class="external-link" target="_blank">http://127.0.0.1:8000/app</a>, ви побачите звичайну відповідь:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"message": "Hello World",
|
||||
"root_path": "/api/v1"
|
||||
}
|
||||
```
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Зверніть увагу: хоча ви звертаєтеся за адресою `http://127.0.0.1:8000/app`, показується `root_path` `/api/v1`, узятий з опції `--root-path`.
|
||||
|
||||
///
|
||||
|
||||
А тепер відкрийте URL з портом Traefik, включно з префіксом шляху: <a href="http://127.0.0.1:9999/api/v1/app" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/app</a>.
|
||||
|
||||
Ми отримуємо ту саму відповідь:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"message": "Hello World",
|
||||
"root_path": "/api/v1"
|
||||
}
|
||||
```
|
||||
|
||||
але цього разу за URL із префіксом шляху, наданим проксі: `/api/v1`.
|
||||
|
||||
Звісно, ідея тут у тому, що всі звертатимуться до застосунку через проксі, тож варіант із префіксом шляху `/api/v1` є «правильним».
|
||||
|
||||
А варіант без префікса шляху (`http://127.0.0.1:8000/app`), який надає безпосередньо Uvicorn, буде виключно для доступу _проксі_ (Traefik) до нього.
|
||||
|
||||
Це демонструє, як Proxy (Traefik) використовує префікс шляху і як сервер (Uvicorn) використовує `root_path` з опції `--root-path`.
|
||||
|
||||
### Перевірка UI документації { #check-the-docs-ui }
|
||||
|
||||
Але найцікавіше — далі. ✨
|
||||
|
||||
«Офіційний» спосіб доступу до застосунку — через проксі з префіксом шляху, який ми визначили. Тож, як і слід очікувати, якщо ви спробуєте UI документації, який віддає Uvicorn напряму, без префікса шляху в URL, він не працюватиме, бо очікує доступ через проксі.
|
||||
|
||||
Ви можете перевірити це за адресою <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>:
|
||||
|
||||
<img src="/img/tutorial/behind-a-proxy/image01.png">
|
||||
|
||||
Але якщо ми звернемося до UI документації за «офіційною» URL через проксі на порту `9999`, за адресою `/api/v1/docs`, він працює коректно! 🎉
|
||||
|
||||
Ви можете перевірити це за адресою <a href="http://127.0.0.1:9999/api/v1/docs" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/docs</a>:
|
||||
|
||||
<img src="/img/tutorial/behind-a-proxy/image02.png">
|
||||
|
||||
Саме так, як ми й хотіли. ✔️
|
||||
|
||||
Це тому, що FastAPI використовує цей `root_path`, щоб створити `server` за замовчуванням в OpenAPI з URL, наданим через `root_path`.
|
||||
|
||||
## Додаткові сервери { #additional-servers }
|
||||
|
||||
/// warning | Попередження
|
||||
|
||||
Це більш просунутий сценарій. За бажанням можете його пропустити.
|
||||
|
||||
///
|
||||
|
||||
За замовчуванням **FastAPI** створює `server` у схемі OpenAPI з URL для `root_path`.
|
||||
|
||||
Але ви також можете надати інші альтернативні `servers`, наприклад, якщо хочете, щоб *той самий* UI документації взаємодіяв і зі staging-, і з production-середовищем.
|
||||
|
||||
Якщо ви передаєте власний список `servers` і є `root_path` (бо ваш API знаходиться за проксі), **FastAPI** вставить «server» з цим `root_path` на початок списку.
|
||||
|
||||
Наприклад:
|
||||
|
||||
{* ../../docs_src/behind_a_proxy/tutorial003_py39.py hl[4:7] *}
|
||||
|
||||
Згенерує схему OpenAPI на кшталт:
|
||||
|
||||
```JSON hl_lines="5-7"
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
// More stuff here
|
||||
"servers": [
|
||||
{
|
||||
"url": "/api/v1"
|
||||
},
|
||||
{
|
||||
"url": "https://stag.example.com",
|
||||
"description": "Staging environment"
|
||||
},
|
||||
{
|
||||
"url": "https://prod.example.com",
|
||||
"description": "Production environment"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
// More stuff here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Зверніть увагу на автоматично згенерований server зі значенням `url` `/api/v1`, узятим з `root_path`.
|
||||
|
||||
///
|
||||
|
||||
У UI документації за адресою <a href="http://127.0.0.1:9999/api/v1/docs" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/docs</a> це виглядатиме так:
|
||||
|
||||
<img src="/img/tutorial/behind-a-proxy/image03.png">
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
UI документації взаємодіятиме з тим сервером, який ви виберете.
|
||||
|
||||
///
|
||||
|
||||
/// note | Технічні деталі
|
||||
|
||||
Властивість `servers` у специфікації OpenAPI є необов’язковою.
|
||||
|
||||
Якщо ви не вказуєте параметр `servers` і `root_path` дорівнює `/`, властивість `servers` у згенерованій схемі OpenAPI буде повністю пропущена за замовчуванням, що еквівалентно одному server зі значенням `url` `/`.
|
||||
|
||||
///
|
||||
|
||||
### Вимкнення автоматичного server з `root_path` { #disable-automatic-server-from-root-path }
|
||||
|
||||
Якщо ви не хочете, щоб **FastAPI** включав автоматичний server з `root_path`, можете використати параметр `root_path_in_servers=False`:
|
||||
|
||||
{* ../../docs_src/behind_a_proxy/tutorial004_py39.py hl[9] *}
|
||||
|
||||
і тоді він не включатиметься в схему OpenAPI.
|
||||
|
||||
## Монтування підзастосунку { #mounting-a-sub-application }
|
||||
|
||||
Якщо вам потрібно змонтувати підзастосунок (як описано в [Підзастосунки — монтування](sub-applications.md){.internal-link target=_blank}), водночас використовуючи проксі з `root_path`, ви можете зробити це у звичний спосіб.
|
||||
|
||||
FastAPI внутрішньо «розумно» використовує `root_path`, тож усе просто працюватиме. ✨
|
||||
312
docs/uk/docs/advanced/custom-response.md
Normal file
312
docs/uk/docs/advanced/custom-response.md
Normal file
@@ -0,0 +1,312 @@
|
||||
# Користувацька відповідь — HTML, Stream, File та інше { #custom-response-html-stream-file-others }
|
||||
|
||||
За замовчуванням **FastAPI** повертатиме відповіді, використовуючи `JSONResponse`.
|
||||
|
||||
Ви можете перевизначити це, повернувши `Response` напряму, як показано в розділі [Повернути Response напряму](response-directly.md){.internal-link target=_blank}.
|
||||
|
||||
Але якщо ви повертаєте `Response` напряму (або будь-який підклас, як-от `JSONResponse`), дані не будуть автоматично конвертовані (навіть якщо ви оголосили `response_model`), і документація не буде автоматично згенерована (наприклад, із включенням конкретного «media type» у HTTP-заголовок `Content-Type` як частини згенерованого OpenAPI).
|
||||
|
||||
Втім, ви також можете оголосити `Response`, яку потрібно використовувати (наприклад, будь-який підклас `Response`), у *декораторі операції шляху* через параметр `response_class`.
|
||||
|
||||
Вміст, який ви повертаєте з вашої *функції операції шляху*, буде вкладено всередину цього `Response`.
|
||||
|
||||
І якщо цей `Response` має JSON media type (`application/json`), як у випадку з `JSONResponse` та `UJSONResponse`, дані, які ви повертаєте, будуть автоматично конвертовані (і відфільтровані) за допомогою будь-якого Pydantic `response_model`, який ви оголосили в *декораторі операції шляху*.
|
||||
|
||||
/// note | Примітка
|
||||
|
||||
Якщо ви використовуєте клас відповіді без media type, FastAPI очікуватиме, що ваша відповідь не матиме вмісту, тож не документуватиме формат відповіді у згенерованій OpenAPI-документації.
|
||||
|
||||
///
|
||||
|
||||
## Використання `ORJSONResponse` { #use-orjsonresponse }
|
||||
|
||||
Наприклад, якщо ви «вичавлюєте» продуктивність, можете встановити й використати <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a> та налаштувати відповідь як `ORJSONResponse`.
|
||||
|
||||
Імпортуйте клас `Response` (підклас), який хочете використовувати, та оголосіть його в *декораторі операції шляху*.
|
||||
|
||||
Для великих відповідей повертати `Response` напряму набагато швидше, ніж повертати словник.
|
||||
|
||||
Це тому, що за замовчуванням FastAPI перевірятиме кожен елемент усередині та переконуватиметься, що його можна серіалізувати в JSON, використовуючи той самий [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}, пояснений у підручнику. Саме це дозволяє вам повертати **довільні об’єкти**, наприклад моделі бази даних.
|
||||
|
||||
Але якщо ви впевнені, що вміст, який ви повертаєте, **можна серіалізувати в JSON**, ви можете передати його безпосередньо класу відповіді та уникнути додаткових витрат, які FastAPI матиме, пропускаючи результат через `jsonable_encoder` перед передаванням його класу відповіді.
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial001b_py39.py hl[2,7] *}
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Параметр `response_class` також буде використано, щоб визначити «media type» відповіді.
|
||||
|
||||
У цьому випадку HTTP-заголовок `Content-Type` буде встановлено в `application/json`.
|
||||
|
||||
І це буде задокументовано відповідним чином в OpenAPI.
|
||||
|
||||
///
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
`ORJSONResponse` доступний лише у FastAPI, а не в Starlette.
|
||||
|
||||
///
|
||||
|
||||
## HTML-відповідь { #html-response }
|
||||
|
||||
Щоб повертати відповідь з HTML напряму з **FastAPI**, використовуйте `HTMLResponse`.
|
||||
|
||||
* Імпортуйте `HTMLResponse`.
|
||||
* Передайте `HTMLResponse` як параметр `response_class` вашого *декоратора операції шляху*.
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial002_py39.py hl[2,7] *}
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Параметр `response_class` також буде використано, щоб визначити «media type» відповіді.
|
||||
|
||||
У цьому випадку HTTP-заголовок `Content-Type` буде встановлено в `text/html`.
|
||||
|
||||
І це буде задокументовано відповідним чином в OpenAPI.
|
||||
|
||||
///
|
||||
|
||||
### Повернути `Response` { #return-a-response }
|
||||
|
||||
Як показано в [Повернути Response напряму](response-directly.md){.internal-link target=_blank}, ви також можете перевизначити відповідь напряму у вашій *операції шляху*, повернувши її.
|
||||
|
||||
Той самий приклад з вище, який повертає `HTMLResponse`, може виглядати так:
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial003_py39.py hl[2,7,19] *}
|
||||
|
||||
/// warning | Попередження
|
||||
|
||||
`Response`, повернений напряму вашою *функцією операції шляху*, не буде задокументований в OpenAPI (наприклад, `Content-Type` не буде задокументовано) і не буде видимий в автоматичних інтерактивних документах.
|
||||
|
||||
///
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Звісно, фактичний заголовок `Content-Type`, status code тощо будуть взяті з об’єкта `Response`, який ви повернули.
|
||||
|
||||
///
|
||||
|
||||
### Документувати в OpenAPI і перевизначити `Response` { #document-in-openapi-and-override-response }
|
||||
|
||||
Якщо ви хочете перевизначити відповідь усередині функції, але водночас задокументувати «media type» в OpenAPI, ви можете використати параметр `response_class` І повернути об’єкт `Response`.
|
||||
|
||||
Тоді `response_class` буде використано лише для документування OpenAPI *операції шляху*, а ваш `Response` буде використано як є.
|
||||
|
||||
#### Повернути `HTMLResponse` напряму { #return-an-htmlresponse-directly }
|
||||
|
||||
Наприклад, це може виглядати так:
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial004_py39.py hl[7,21,23] *}
|
||||
|
||||
У цьому прикладі функція `generate_html_response()` вже генерує та повертає `Response` замість повернення HTML як `str`.
|
||||
|
||||
Повертаючи результат виклику `generate_html_response()`, ви вже повертаєте `Response`, який перевизначить стандартну поведінку **FastAPI**.
|
||||
|
||||
Але оскільки ви також передали `HTMLResponse` у `response_class`, **FastAPI** знатиме, як задокументувати це в OpenAPI та інтерактивній документації як HTML з `text/html`:
|
||||
|
||||
<img src="/img/tutorial/custom-response/image01.png">
|
||||
|
||||
## Доступні відповіді { #available-responses }
|
||||
|
||||
Ось деякі з доступних відповідей.
|
||||
|
||||
Майте на увазі, що ви можете використати `Response`, щоб повернути будь-що інше, або навіть створити власний підклас.
|
||||
|
||||
/// note | Технічні деталі
|
||||
|
||||
Ви також можете використати `from starlette.responses import HTMLResponse`.
|
||||
|
||||
**FastAPI** надає ті самі `starlette.responses` як `fastapi.responses` просто для вашої зручності як розробника. Але більшість доступних відповідей надходить безпосередньо зі Starlette.
|
||||
|
||||
///
|
||||
|
||||
### `Response` { #response }
|
||||
|
||||
Основний клас `Response`, від нього наслідуються всі інші відповіді.
|
||||
|
||||
Ви можете повертати його напряму.
|
||||
|
||||
Він приймає такі параметри:
|
||||
|
||||
* `content` — `str` або `bytes`.
|
||||
* `status_code` — HTTP status code типу `int`.
|
||||
* `headers` — `dict` зі строк.
|
||||
* `media_type` — `str`, що задає media type. Напр. `"text/html"`.
|
||||
|
||||
FastAPI (насправді Starlette) автоматично додасть заголовок Content-Length. Також буде додано заголовок Content-Type на основі `media_type` із додаванням charset для текстових типів.
|
||||
|
||||
{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *}
|
||||
|
||||
### `HTMLResponse` { #htmlresponse }
|
||||
|
||||
Приймає текст або байти та повертає HTML-відповідь, як ви читали вище.
|
||||
|
||||
### `PlainTextResponse` { #plaintextresponse }
|
||||
|
||||
Приймає текст або байти та повертає відповідь звичайним текстом.
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial005_py39.py hl[2,7,9] *}
|
||||
|
||||
### `JSONResponse` { #jsonresponse }
|
||||
|
||||
Приймає деякі дані та повертає відповідь, закодовану як `application/json`.
|
||||
|
||||
Це відповідь за замовчуванням, яку використовує **FastAPI**, як ви читали вище.
|
||||
|
||||
### `ORJSONResponse` { #orjsonresponse }
|
||||
|
||||
Швидка альтернативна JSON-відповідь з використанням <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>, як ви читали вище.
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Для цього потрібно встановити `orjson`, наприклад командою `pip install orjson`.
|
||||
|
||||
///
|
||||
|
||||
### `UJSONResponse` { #ujsonresponse }
|
||||
|
||||
Альтернативна JSON-відповідь з використанням <a href="https://github.com/ultrajson/ultrajson" class="external-link" target="_blank">`ujson`</a>.
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Для цього потрібно встановити `ujson`, наприклад командою `pip install ujson`.
|
||||
|
||||
///
|
||||
|
||||
/// warning | Попередження
|
||||
|
||||
`ujson` менш обережний, ніж вбудована реалізація Python, у тому, як він обробляє деякі крайові випадки.
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial001_py39.py hl[2,7] *}
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Можливо, `ORJSONResponse` буде швидшою альтернативою.
|
||||
|
||||
///
|
||||
|
||||
### `RedirectResponse` { #redirectresponse }
|
||||
|
||||
Повертає HTTP-перенаправлення. За замовчуванням використовує status code 307 (Temporary Redirect).
|
||||
|
||||
Ви можете повернути `RedirectResponse` напряму:
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial006_py39.py hl[2,9] *}
|
||||
|
||||
---
|
||||
|
||||
Або ви можете використати його в параметрі `response_class`:
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial006b_py39.py hl[2,7,9] *}
|
||||
|
||||
Якщо ви зробите так, то з вашої *функції операції шляху* можна повертати URL напряму.
|
||||
|
||||
У цьому випадку використаний `status_code` буде стандартним для `RedirectResponse`, тобто `307`.
|
||||
|
||||
---
|
||||
|
||||
Також можна використати параметр `status_code` у поєднанні з параметром `response_class`:
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial006c_py39.py hl[2,7,9] *}
|
||||
|
||||
### `StreamingResponse` { #streamingresponse }
|
||||
|
||||
Приймає async generator або звичайний generator/iterator і стрімить тіло відповіді.
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial007_py39.py hl[2,14] *}
|
||||
|
||||
#### Використання `StreamingResponse` з file-like об’єктами { #using-streamingresponse-with-file-like-objects }
|
||||
|
||||
Якщо у вас є <a href="https://docs.python.org/3/glossary.html#term-file-like-object" class="external-link" target="_blank">file-like</a> об’єкт (наприклад, об’єкт, який повертає `open()`), ви можете створити generator function, щоб ітеруватися по цьому file-like об’єкту.
|
||||
|
||||
Так вам не потрібно спочатку повністю зчитувати його в пам’ять; ви можете передати цю generator function у `StreamingResponse` і повернути її.
|
||||
|
||||
Це охоплює багато бібліотек для роботи з хмарним сховищем, обробки відео та інші.
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial008_py39.py hl[2,10:12,14] *}
|
||||
|
||||
1. Це generator function. Це «generator function», бо всередині містить інструкції `yield`.
|
||||
2. Використовуючи блок `with`, ми гарантуємо, що file-like об’єкт буде закрито після завершення generator function. Тобто після того, як вона завершить надсилання відповіді.
|
||||
3. Цей `yield from` каже функції ітеруватися по об’єкту з назвою `file_like`. А потім для кожної проітерованої частини робити `yield` цієї частини так, ніби вона походить з цієї generator function (`iterfile`).
|
||||
|
||||
Отже, це generator function, яка «делегує» роботу генерації чомусь іншому всередині.
|
||||
|
||||
Роблячи це таким чином, ми можемо помістити все в блок `with` і так гарантувати, що file-like об’єкт буде закрито після завершення.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Зверніть увагу: оскільки тут ми використовуємо стандартний `open()`, який не підтримує `async` та `await`, ми оголошуємо операцію шляху звичайним `def`.
|
||||
|
||||
///
|
||||
|
||||
### `FileResponse` { #fileresponse }
|
||||
|
||||
Асинхронно стрімить файл як відповідь.
|
||||
|
||||
Має інший набір аргументів для створення екземпляра, ніж інші типи відповідей:
|
||||
|
||||
* `path` — шлях до файлу, який потрібно стрімити.
|
||||
* `headers` — будь-які користувацькі заголовки для включення, як словник.
|
||||
* `media_type` — рядок, що задає media type. Якщо не задано, ім’я файлу або шлях буде використано для визначення media type.
|
||||
* `filename` — якщо задано, буде включено у відповідь `Content-Disposition`.
|
||||
|
||||
Файлові відповіді включатимуть відповідні заголовки `Content-Length`, `Last-Modified` та `ETag`.
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial009_py39.py hl[2,10] *}
|
||||
|
||||
Ви також можете використати параметр `response_class`:
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial009b_py39.py hl[2,8,10] *}
|
||||
|
||||
У цьому випадку ви можете повертати шлях до файлу напряму з вашої *функції операції шляху*.
|
||||
|
||||
## Користувацький клас відповіді { #custom-response-class }
|
||||
|
||||
Ви можете створити власний клас відповіді, успадкувавши його від `Response`, і використовувати.
|
||||
|
||||
Наприклад, припустімо, ви хочете використовувати <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>, але з деякими користувацькими налаштуваннями, яких немає у включеному класі `ORJSONResponse`.
|
||||
|
||||
Скажімо, ви хочете, щоб він повертав JSON з відступами та форматуванням, тож хочете використати опцію orjson `orjson.OPT_INDENT_2`.
|
||||
|
||||
Ви можете створити `CustomORJSONResponse`. Головне, що потрібно зробити, — створити метод `Response.render(content)`, який повертає вміст як `bytes`:
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial009c_py39.py hl[9:14,17] *}
|
||||
|
||||
Тепер замість повернення:
|
||||
|
||||
```json
|
||||
{"message": "Hello World"}
|
||||
```
|
||||
|
||||
...ця відповідь повертатиме:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Hello World"
|
||||
}
|
||||
```
|
||||
|
||||
Звісно, імовірно, ви знайдете значно кращі способи скористатися цим, ніж форматування JSON. 😉
|
||||
|
||||
## Клас відповіді за замовчуванням { #default-response-class }
|
||||
|
||||
Під час створення екземпляра класу **FastAPI** або `APIRouter` ви можете вказати, який клас відповіді використовувати за замовчуванням.
|
||||
|
||||
Параметр, який це визначає, — `default_response_class`.
|
||||
|
||||
У прикладі нижче **FastAPI** використовуватиме `ORJSONResponse` за замовчуванням у всіх *операціях шляху* замість `JSONResponse`.
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial010_py39.py hl[2,4] *}
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Ви все одно можете перевизначати `response_class` в *операціях шляху*, як і раніше.
|
||||
|
||||
///
|
||||
|
||||
## Додаткова документація { #additional-documentation }
|
||||
|
||||
Ви також можете оголосити media type та багато інших деталей в OpenAPI через `responses`: [Додаткові відповіді в OpenAPI](additional-responses.md){.internal-link target=_blank}.
|
||||
95
docs/uk/docs/advanced/dataclasses.md
Normal file
95
docs/uk/docs/advanced/dataclasses.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Використання dataclasses { #using-dataclasses }
|
||||
|
||||
FastAPI побудовано поверх **Pydantic**, і я показував вам, як використовувати моделі Pydantic для оголошення запитів і відповідей.
|
||||
|
||||
Але FastAPI також підтримує використання <a href="https://docs.python.org/3/library/dataclasses.html" class="external-link" target="_blank">`dataclasses`</a> так само:
|
||||
|
||||
{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *}
|
||||
|
||||
Це й досі підтримується завдяки **Pydantic**, оскільки він має <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/#use-of-stdlib-dataclasses-with-basemodel" class="external-link" target="_blank">внутрішню підтримку для `dataclasses`</a>.
|
||||
|
||||
Тож навіть із наведеним вище кодом, який явно не використовує Pydantic, FastAPI застосовує Pydantic, щоб перетворити ці стандартні dataclasses на власний різновид dataclasses у Pydantic.
|
||||
|
||||
І, звісно, це підтримує те саме:
|
||||
|
||||
* валідацію даних
|
||||
* серіалізацію даних
|
||||
* документацію даних тощо
|
||||
|
||||
Це працює так само, як і з моделями Pydantic. І фактично під капотом це реалізовано так само — за допомогою Pydantic.
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Майте на увазі, що dataclasses не можуть робити все те, що можуть моделі Pydantic.
|
||||
|
||||
Тож вам усе одно може знадобитися використовувати моделі Pydantic.
|
||||
|
||||
Але якщо у вас є багато dataclasses, це гарний прийом, щоб використати їх для побудови web API за допомогою FastAPI. 🤓
|
||||
|
||||
///
|
||||
|
||||
## Dataclasses у `response_model` { #dataclasses-in-response-model }
|
||||
|
||||
Ви також можете використовувати `dataclasses` у параметрі `response_model`:
|
||||
|
||||
{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *}
|
||||
|
||||
Dataclass буде автоматично перетворено на Pydantic dataclass.
|
||||
|
||||
У такий спосіб його схема відображатиметься в інтерфейсі документації API:
|
||||
|
||||
<img src="/img/tutorial/dataclasses/image01.png">
|
||||
|
||||
## Dataclasses у вкладених структурах даних { #dataclasses-in-nested-data-structures }
|
||||
|
||||
Ви також можете поєднувати `dataclasses` з іншими анотаціями типів, щоб створювати вкладені структури даних.
|
||||
|
||||
У деяких випадках вам усе ж доведеться використовувати версію `dataclasses` від Pydantic. Наприклад, якщо виникають помилки з автоматично згенерованою документацією API.
|
||||
|
||||
У такому разі ви можете просто замінити стандартні `dataclasses` на `pydantic.dataclasses` — це повна (drop-in) заміна:
|
||||
|
||||
{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *}
|
||||
|
||||
1. Ми все ще імпортуємо `field` зі стандартного `dataclasses`.
|
||||
|
||||
2. `pydantic.dataclasses` — це повна (drop-in) заміна для `dataclasses`.
|
||||
|
||||
3. Dataclass `Author` містить список dataclasses `Item`.
|
||||
|
||||
4. Dataclass `Author` використовується як параметр `response_model`.
|
||||
|
||||
5. Ви можете використовувати інші стандартні анотації типів із dataclasses як тіло запиту.
|
||||
|
||||
У цьому випадку це список dataclasses `Item`.
|
||||
|
||||
6. Тут ми повертаємо словник, який містить `items` — список dataclasses.
|
||||
|
||||
FastAPI все ще здатний <abbr title="перетворення даних у формат, який можна передавати">серіалізувати</abbr> дані в JSON.
|
||||
|
||||
7. Тут `response_model` використовує анотацію типу — список dataclasses `Author`.
|
||||
|
||||
Знову ж таки, ви можете поєднувати `dataclasses` зі стандартними анотаціями типів.
|
||||
|
||||
8. Зверніть увагу, що ця *функція операції шляху* використовує звичайний `def` замість `async def`.
|
||||
|
||||
Як завжди, у FastAPI ви можете поєднувати `def` і `async def` за потреби.
|
||||
|
||||
Якщо вам потрібно освіжити в пам’яті, коли що використовувати, перегляньте розділ _«Поспішаєте?»_ у документації про [`async` і `await`](../async.md#in-a-hurry){.internal-link target=_blank}.
|
||||
|
||||
9. Ця *функція операції шляху* не повертає dataclasses (хоча могла б), а список словників із внутрішніми даними.
|
||||
|
||||
FastAPI використає параметр `response_model` (який включає dataclasses), щоб перетворити відповідь.
|
||||
|
||||
Ви можете поєднувати `dataclasses` з іншими анотаціями типів у багатьох різних комбінаціях, щоб формувати складні структури даних.
|
||||
|
||||
Перегляньте підказки в анотаціях у коді вище, щоб побачити конкретніші деталі.
|
||||
|
||||
## Дізнатися більше { #learn-more }
|
||||
|
||||
Ви також можете поєднувати `dataclasses` з іншими моделями Pydantic, успадковуватися від них, включати їх у власні моделі тощо.
|
||||
|
||||
Щоб дізнатися більше, перегляньте <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/" class="external-link" target="_blank">документацію Pydantic про dataclasses</a>.
|
||||
|
||||
## Версія { #version }
|
||||
|
||||
Це доступно починаючи з версії FastAPI `0.67.0`. 🔖
|
||||
165
docs/uk/docs/advanced/events.md
Normal file
165
docs/uk/docs/advanced/events.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# Події життєвого циклу { #lifespan-events }
|
||||
|
||||
Ви можете визначити логіку (код), яку слід виконати перед **запуском** застосунку. Це означає, що цей код буде виконано **один раз**, **до того**, як застосунок **почне приймати запити**.
|
||||
|
||||
Так само ви можете визначити логіку (код), яку слід виконати під час **завершення роботи** застосунку. У цьому разі цей код буде виконано **один раз**, **після** обробки, ймовірно, **багатьох запитів**.
|
||||
|
||||
Оскільки цей код виконується до того, як застосунок **почне** приймати запити, і одразу після того, як він **завершить** їх обробку, він охоплює весь **життєвий цикл** застосунку (слово «lifespan» за мить стане важливим).
|
||||
|
||||
Це може бути дуже корисно для налаштування **ресурсів**, які потрібні вам для всього застосунку, які є **спільними** для запитів, та/або які потрібно потім **очистити**. Наприклад, пул підключень до бази даних або завантаження спільної моделі машинного навчання.
|
||||
|
||||
## Випадок використання { #use-case }
|
||||
|
||||
Почнімо з прикладу **випадку використання**, а потім подивімося, як це розв’язати.
|
||||
|
||||
Уявімо, що у вас є кілька **моделей машинного навчання**, які ви хочете використовувати для обробки запитів.
|
||||
|
||||
Ці самі моделі є спільними для запитів, тобто це не одна модель на запит чи одна на користувача або щось подібне.
|
||||
|
||||
Уявімо, що завантаження моделі може **займати досить багато часу**, бо потрібно читати багато **даних із диска**. Тож ви не хочете робити це для кожного запиту.
|
||||
|
||||
Ви могли б завантажувати її на верхньому рівні модуля/файлу, але це також означало б, що модель **завантажуватиметься** навіть тоді, коли ви просто запускаєте простий автоматизований тест. Тоді тест був би **повільним**, бо йому довелося б чекати на завантаження моделі перед запуском незалежної частини коду.
|
||||
|
||||
Саме це ми й розв’яжемо: завантажимо модель до початку обробки запитів, але лише безпосередньо перед тим, як застосунок почне їх приймати, а не під час завантаження коду.
|
||||
|
||||
## Lifespan { #lifespan }
|
||||
|
||||
Ви можете визначити цю логіку *startup* і *shutdown* за допомогою параметра `lifespan` застосунку `FastAPI` та «context manager» (за мить покажу, що це таке).
|
||||
|
||||
Почнімо з прикладу, а потім розберемо його детально.
|
||||
|
||||
Створімо асинхронну функцію `lifespan()` з `yield` ось так:
|
||||
|
||||
{* ../../docs_src/events/tutorial003_py39.py hl[16,19] *}
|
||||
|
||||
Тут ми імітуємо дорогу операцію *startup* із завантаженням моделі, додаючи (фейкову) функцію моделі в словник із моделями машинного навчання перед `yield`. Цей код буде виконано **до того**, як застосунок **почне приймати запити**, під час *startup*.
|
||||
|
||||
А потім, одразу після `yield`, ми вивантажуємо модель. Цей код буде виконано **після** того, як застосунок **завершить обробку запитів**, безпосередньо перед *shutdown*. Це може, наприклад, звільнити ресурси на кшталт пам’яті або GPU.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
`shutdown` відбудеться, коли ви **зупиняєте** застосунок.
|
||||
|
||||
Можливо, вам потрібно запустити нову версію, або ви просто втомилися його запускати.
|
||||
|
||||
///
|
||||
|
||||
### Функція lifespan { #lifespan-function }
|
||||
|
||||
Перше, на що слід звернути увагу: ми визначаємо асинхронну функцію з `yield`. Це дуже схоже на залежності з `yield`.
|
||||
|
||||
{* ../../docs_src/events/tutorial003_py39.py hl[14:19] *}
|
||||
|
||||
Перша частина функції, до `yield`, буде виконана **до** запуску застосунку.
|
||||
|
||||
А частина після `yield` буде виконана **після** того, як застосунок завершить роботу.
|
||||
|
||||
### Async Context Manager { #async-context-manager }
|
||||
|
||||
Якщо перевірити, функція декорована `@asynccontextmanager`.
|
||||
|
||||
Це перетворює функцію на те, що називають «**async context manager**».
|
||||
|
||||
{* ../../docs_src/events/tutorial003_py39.py hl[1,13] *}
|
||||
|
||||
**Context manager** у Python — це те, що ви можете використати в операторі `with`, наприклад, `open()` можна використати як context manager:
|
||||
|
||||
```Python
|
||||
with open("file.txt") as file:
|
||||
file.read()
|
||||
```
|
||||
|
||||
У нових версіях Python також є **async context manager**. Його слід використовувати з `async with`:
|
||||
|
||||
```Python
|
||||
async with lifespan(app):
|
||||
await do_stuff()
|
||||
```
|
||||
|
||||
Коли ви створюєте context manager або async context manager, як вище, він робить таке: перед входом у блок `with` виконає код до `yield`, а після виходу з блоку `with` виконає код після `yield`.
|
||||
|
||||
У нашому прикладі коду вище ми не використовуємо його напряму, але передаємо FastAPI, щоб він використовував його.
|
||||
|
||||
Параметр `lifespan` застосунку `FastAPI` приймає **async context manager**, тож ми можемо передати йому наш новий async context manager `lifespan`.
|
||||
|
||||
{* ../../docs_src/events/tutorial003_py39.py hl[22] *}
|
||||
|
||||
## Альтернативні події (застаріло) { #alternative-events-deprecated }
|
||||
|
||||
/// warning | Попередження
|
||||
|
||||
Рекомендований спосіб обробляти *startup* і *shutdown* — використовувати параметр `lifespan` застосунку `FastAPI`, як описано вище. Якщо ви надасте параметр `lifespan`, обробники подій `startup` і `shutdown` більше не викликатимуться. Або весь підхід через `lifespan`, або весь підхід через події — не обидва одночасно.
|
||||
|
||||
Ймовірно, ви можете пропустити цю частину.
|
||||
|
||||
///
|
||||
|
||||
Є альтернативний спосіб визначити цю логіку, яка має виконуватися під час *startup* і під час *shutdown*.
|
||||
|
||||
Ви можете визначити обробники подій (функції), які потрібно виконати до запуску застосунку або під час завершення його роботи.
|
||||
|
||||
Ці функції можна оголошувати як `async def` або як звичайні `def`.
|
||||
|
||||
### Подія `startup` { #startup-event }
|
||||
|
||||
Щоб додати функцію, яку потрібно запустити перед стартом застосунку, оголосіть її з подією `"startup"`:
|
||||
|
||||
{* ../../docs_src/events/tutorial001_py39.py hl[8] *}
|
||||
|
||||
У цьому випадку функція-обробник події `startup` ініціалізує елемент `"database"` (це просто `dict`) деякими значеннями.
|
||||
|
||||
Ви можете додати більше ніж одну функцію-обробник події.
|
||||
|
||||
І ваш застосунок не почне приймати запити, доки не завершаться всі обробники події `startup`.
|
||||
|
||||
### Подія `shutdown` { #shutdown-event }
|
||||
|
||||
Щоб додати функцію, яку потрібно запустити під час завершення роботи застосунку, оголосіть її з подією `"shutdown"`:
|
||||
|
||||
{* ../../docs_src/events/tutorial002_py39.py hl[6] *}
|
||||
|
||||
Тут функція-обробник події `shutdown` запише рядок тексту `"Application shutdown"` у файл `log.txt`.
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
У функції `open()` параметр `mode="a"` означає «append», тобто рядок буде додано після того, що вже є у файлі, без перезапису попереднього вмісту.
|
||||
|
||||
///
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Зверніть увагу: у цьому випадку ми використовуємо стандартну функцію Python `open()`, яка взаємодіє з файлом.
|
||||
|
||||
Отже, тут є I/O (input/output), що потребує «очікування», доки дані будуть записані на диск.
|
||||
|
||||
Але `open()` не використовує `async` та `await`.
|
||||
|
||||
Тому ми оголошуємо функцію-обробник події як звичайну `def`, а не `async def`.
|
||||
|
||||
///
|
||||
|
||||
### `startup` і `shutdown` разом { #startup-and-shutdown-together }
|
||||
|
||||
Є висока ймовірність, що логіка для ваших *startup* і *shutdown* пов’язана: ви можете хотіти щось запустити, а потім завершити, отримати ресурс, а потім звільнити його тощо.
|
||||
|
||||
Робити це в розділених функціях, які не ділять між собою логіку або змінні, складніше, бо доведеться зберігати значення в глобальних змінних або застосовувати подібні прийоми.
|
||||
|
||||
Тому зараз рекомендується натомість використовувати `lifespan`, як пояснено вище.
|
||||
|
||||
## Технічні деталі { #technical-details }
|
||||
|
||||
Лише технічна деталь для допитливих ґіків.
|
||||
|
||||
Під капотом, у технічній специфікації ASGI, це частина <a href="https://asgi.readthedocs.io/en/latest/specs/lifespan.html" class="external-link" target="_blank">Lifespan Protocol</a>, і вона визначає події з назвами `startup` та `shutdown`.
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Докладніше про обробники `lifespan` у Starlette читайте в <a href="https://www.starlette.dev/lifespan/" class="external-link" target="_blank">документації Lifespan у Starlette</a>.
|
||||
|
||||
Зокрема, як обробляти стан lifespan, який можна використовувати в інших частинах вашого коду.
|
||||
|
||||
///
|
||||
|
||||
## Підзастосунки { #sub-applications }
|
||||
|
||||
🚨 Майте на увазі, що ці події життєвого циклу (startup і shutdown) виконуватимуться лише для головного застосунку, а не для [Підзастосунків — монтування](sub-applications.md){.internal-link target=_blank}.
|
||||
208
docs/uk/docs/advanced/generate-clients.md
Normal file
208
docs/uk/docs/advanced/generate-clients.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Генерація SDK { #generating-sdks }
|
||||
|
||||
Оскільки **FastAPI** базується на специфікації **OpenAPI**, його API можна описати у стандартному форматі, який розуміє багато інструментів.
|
||||
|
||||
Це спрощує генерацію актуальної **документації**, клієнтських бібліотек (<abbr title="Software Development Kits - Набори засобів розробки програмного забезпечення">**SDKs**</abbr>) кількома мовами, а також **тестування** чи **workflow для автоматизації**, які залишаються синхронізованими з вашим кодом.
|
||||
|
||||
У цьому посібнику ви дізнаєтеся, як згенерувати **TypeScript SDK** для вашого FastAPI бекенду.
|
||||
|
||||
## Open Source генератори SDK { #open-source-sdk-generators }
|
||||
|
||||
Універсальний варіант — <a href="https://openapi-generator.tech/" class="external-link" target="_blank">OpenAPI Generator</a>, який підтримує **багато мов програмування** і може генерувати SDK з вашої OpenAPI специфікації.
|
||||
|
||||
Для **TypeScript-клієнтів** <a href="https://heyapi.dev/" class="external-link" target="_blank">Hey API</a> — спеціалізоване рішення, що забезпечує оптимізований досвід для екосистеми TypeScript.
|
||||
|
||||
Більше генераторів SDK можна знайти на <a href="https://openapi.tools/#sdk" class="external-link" target="_blank">OpenAPI.Tools</a>.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
FastAPI автоматично генерує специфікації **OpenAPI 3.1**, тож будь-який інструмент, який ви використовуєте, має підтримувати цю версію.
|
||||
|
||||
///
|
||||
|
||||
## Генератори SDK від спонсорів FastAPI { #sdk-generators-from-fastapi-sponsors }
|
||||
|
||||
У цьому розділі зібрано рішення, що **підтримуються венчурним капіталом** і **компаніями**, від компаній-спонсорів FastAPI. Ці продукти надають **додаткові можливості** та **інтеграції** поверх якісно згенерованих SDK.
|
||||
|
||||
Спонсоруючи ✨ [**FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, ці компанії допомагають зберігати фреймворк та його **екосистему** здоровими й **стійкими**.
|
||||
|
||||
Їхнє спонсорство також демонструє сильну відданість **спільноті** FastAPI (вам), показуючи, що їм важливо не лише пропонувати **якісний сервіс**, а й підтримувати **надійний і процвітаючий фреймворк** FastAPI.
|
||||
|
||||
Наприклад, ви можете спробувати:
|
||||
|
||||
* <a href="https://speakeasy.com/editor?utm_source=fastapi+repo&utm_medium=github+sponsorship" class="external-link" target="_blank">Speakeasy</a>
|
||||
* <a href="https://www.stainless.com/?utm_source=fastapi&utm_medium=referral" class="external-link" target="_blank">Stainless</a>
|
||||
* <a href="https://developers.liblab.com/tutorials/sdk-for-fastapi?utm_source=fastapi" class="external-link" target="_blank">liblab</a>
|
||||
|
||||
Деякі з цих рішень також можуть бути open source або мати безплатні тарифи, тож ви можете спробувати їх без фінансових зобов’язань. Також доступні інші комерційні генератори SDK, їх можна знайти онлайн.
|
||||
|
||||
## Створення TypeScript SDK { #create-a-typescript-sdk }
|
||||
|
||||
Почнімо з простого застосунку FastAPI:
|
||||
|
||||
{* ../../docs_src/generate_clients/tutorial001_py39.py hl[7:9,12:13,16:17,21] *}
|
||||
|
||||
Зверніть увагу, що *операції шляху* визначають моделі, які вони використовують для тіла запиту та тіла відповіді, використовуючи моделі `Item` і `ResponseMessage`.
|
||||
|
||||
### Документація API { #api-docs }
|
||||
|
||||
Якщо перейти на `/docs`, ви побачите **схеми** для даних, які надсилаються в запитах і отримуються у відповідях:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image01.png">
|
||||
|
||||
Ви бачите ці схеми, тому що їх було оголошено моделями в застосунку.
|
||||
|
||||
Ця інформація доступна в **OpenAPI схемі** застосунку, а далі відображається в документації API.
|
||||
|
||||
Саме ця інформація з моделей, включена в OpenAPI, і може бути використана для **генерації клієнтського коду**.
|
||||
|
||||
### Hey API { #hey-api }
|
||||
|
||||
Коли у нас є застосунок FastAPI з моделями, ми можемо використати Hey API для генерації TypeScript-клієнта. Найшвидший спосіб — через npx.
|
||||
|
||||
```sh
|
||||
npx @hey-api/openapi-ts -i http://localhost:8000/openapi.json -o src/client
|
||||
```
|
||||
|
||||
Це згенерує TypeScript SDK у `./src/client`.
|
||||
|
||||
Ви можете дізнатися, як <a href="https://heyapi.dev/openapi-ts/get-started" class="external-link" target="_blank">встановити `@hey-api/openapi-ts`</a>, а також прочитати про <a href="https://heyapi.dev/openapi-ts/output" class="external-link" target="_blank">згенерований результат</a> на їхньому сайті.
|
||||
|
||||
### Використання SDK { #using-the-sdk }
|
||||
|
||||
Тепер ви можете імпортувати та використовувати клієнтський код. Це може виглядати так — зверніть увагу, що ви отримуєте автодоповнення для методів:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image02.png">
|
||||
|
||||
Ви також отримаєте автодоповнення для payload, який потрібно надіслати:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image03.png">
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Зверніть увагу на автодоповнення для `name` і `price` — це було визначено в застосунку FastAPI, у моделі `Item`.
|
||||
|
||||
///
|
||||
|
||||
Також ви матимете inline-помилки для даних, які надсилаєте:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image04.png">
|
||||
|
||||
Об’єкт відповіді також матиме автодоповнення:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image05.png">
|
||||
|
||||
## Застосунок FastAPI з тегами { #fastapi-app-with-tags }
|
||||
|
||||
У багатьох випадках ваш застосунок FastAPI буде більшим, і ви, ймовірно, використовуватимете теги, щоб розділяти різні групи *операцій шляху*.
|
||||
|
||||
Наприклад, у вас може бути секція для **items** і інша секція для **users**, і їх можна розділити тегами:
|
||||
|
||||
{* ../../docs_src/generate_clients/tutorial002_py39.py hl[21,26,34] *}
|
||||
|
||||
### Генерація TypeScript-клієнта з тегами { #generate-a-typescript-client-with-tags }
|
||||
|
||||
Якщо ви генеруєте клієнт для застосунку FastAPI з використанням тегів, зазвичай клієнтський код також буде розділено відповідно до тегів.
|
||||
|
||||
Так ви зможете мати впорядковане й коректно згруповане клієнтське API:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image06.png">
|
||||
|
||||
У цьому випадку у вас є:
|
||||
|
||||
* `ItemsService`
|
||||
* `UsersService`
|
||||
|
||||
### Назви методів клієнта { #client-method-names }
|
||||
|
||||
Зараз згенеровані назви методів на кшталт `createItemItemsPost` виглядають не дуже охайно:
|
||||
|
||||
```TypeScript
|
||||
ItemsService.createItemItemsPost({name: "Plumbus", price: 5})
|
||||
```
|
||||
|
||||
...це тому, що генератор клієнта використовує внутрішній **operation ID** OpenAPI для кожної *операції шляху*.
|
||||
|
||||
OpenAPI вимагає, щоб кожен operation ID був унікальним серед усіх *операцій шляху*, тож FastAPI використовує **ім’я функції**, **шлях** і **HTTP метод/операцію**, щоб згенерувати цей operation ID — так він може гарантувати унікальність.
|
||||
|
||||
Але далі я покажу, як це покращити.
|
||||
|
||||
## Власні Operation ID і кращі назви методів { #custom-operation-ids-and-better-method-names }
|
||||
|
||||
Ви можете **змінити** спосіб **генерації** цих operation ID, щоб зробити їх простішими та отримати **простішi назви методів** у клієнтах.
|
||||
|
||||
У такому разі вам потрібно забезпечити, щоб кожен operation ID був **унікальним** іншим способом.
|
||||
|
||||
Наприклад, ви можете гарантувати, що кожна *операція шляху* має тег, а потім генерувати operation ID на основі **тега** та **назви** *операції шляху* (імені функції).
|
||||
|
||||
### Власна функція генерації унікального ID { #custom-generate-unique-id-function }
|
||||
|
||||
FastAPI використовує **унікальний ID** для кожної *операції шляху*, який застосовується для **operation ID**, а також для назв потрібних кастомних моделей для запитів або відповідей.
|
||||
|
||||
Ви можете налаштувати цю функцію. Вона приймає `APIRoute` і повертає рядок.
|
||||
|
||||
Наприклад, тут вона використовує перший тег (у вас, імовірно, буде лише один тег) і назву *операції шляху* (ім’я функції).
|
||||
|
||||
Потім ви можете передати цю функцію в **FastAPI** як параметр `generate_unique_id_function`:
|
||||
|
||||
{* ../../docs_src/generate_clients/tutorial003_py39.py hl[6:7,10] *}
|
||||
|
||||
### Генерація TypeScript-клієнта з власними Operation ID { #generate-a-typescript-client-with-custom-operation-ids }
|
||||
|
||||
Тепер, якщо згенерувати клієнт знову, ви побачите, що він має покращені назви методів:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image07.png">
|
||||
|
||||
Як бачите, назви методів тепер містять тег, а далі — ім’я функції; вони більше не включають інформацію з URL-шляху та HTTP-операції.
|
||||
|
||||
### Попередня обробка OpenAPI специфікації для генератора клієнта { #preprocess-the-openapi-specification-for-the-client-generator }
|
||||
|
||||
Згенерований код усе ще містить деяку **дубльовану інформацію**.
|
||||
|
||||
Ми вже знаємо, що цей метод пов’язаний з **items**, бо це слово є в `ItemsService` (взято з тега), але в назві методу все ще є префікс із назвою тега.
|
||||
|
||||
Ймовірно, для OpenAPI загалом ми все одно захочемо це зберегти, адже це забезпечить **унікальність** operation ID.
|
||||
|
||||
Але для згенерованого клієнта ми можемо **змінити** operation ID в OpenAPI прямо перед генерацією клієнтів — лише щоб зробити назви методів гарнішими та **чистішими**.
|
||||
|
||||
Можна завантажити OpenAPI JSON у файл `openapi.json`, а потім **прибрати цей префікс тега** скриптом на кшталт такого:
|
||||
|
||||
{* ../../docs_src/generate_clients/tutorial004_py39.py *}
|
||||
|
||||
//// tab | Node.js
|
||||
|
||||
```Javascript
|
||||
{!> ../../docs_src/generate_clients/tutorial004.js!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
Після цього operation ID буде перейменовано з варіантів на кшталт `items-get_items` просто на `get_items`, і генератор клієнта зможе створювати простіші назви методів.
|
||||
|
||||
### Генерація TypeScript-клієнта з попередньо обробленим OpenAPI { #generate-a-typescript-client-with-the-preprocessed-openapi }
|
||||
|
||||
Оскільки кінцевий результат тепер у файлі `openapi.json`, вам потрібно оновити шлях до вхідних даних:
|
||||
|
||||
```sh
|
||||
npx @hey-api/openapi-ts -i ./openapi.json -o src/client
|
||||
```
|
||||
|
||||
Після генерації нового клієнта ви матимете **чисті назви методів** з усім **автодоповненням**, **inline-помилками** тощо:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image08.png">
|
||||
|
||||
## Переваги { #benefits }
|
||||
|
||||
Використовуючи автоматично згенеровані клієнти, ви отримуєте **автодоповнення** для:
|
||||
|
||||
* Методів.
|
||||
* Payload запиту в тілі, query-параметрів тощо.
|
||||
* Payload відповіді.
|
||||
|
||||
Також ви матимете **inline-помилки** для всього.
|
||||
|
||||
І щоразу, коли ви оновлюєте код бекенду та **перегенеровуєте** фронтенд, нові *операції шляху* будуть доступні як методи, старі — видалені, а будь-які інші зміни відобразяться в згенерованому коді.
|
||||
|
||||
Це також означає, що якщо щось змінилося, це автоматично **відобразиться** в клієнтському коді. А якщо ви **збираєте** (build) клієнт, збірка завершиться помилкою, якщо є будь-яка **невідповідність** у використовуваних даних.
|
||||
|
||||
Отже, ви зможете **виявляти багато помилок** дуже рано в циклі розробки, замість того щоб чекати, поки помилки проявляться у ваших кінцевих користувачів у production, а потім намагатися відлагодити, де саме проблема.
|
||||
21
docs/uk/docs/advanced/index.md
Normal file
21
docs/uk/docs/advanced/index.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Розширений посібник користувача { #advanced-user-guide }
|
||||
|
||||
## Додаткові можливості { #additional-features }
|
||||
|
||||
Основного [Підручника — Посібника користувача](../tutorial/index.md){.internal-link target=_blank} має бути достатньо, щоб ознайомити вас з усіма основними можливостями **FastAPI**.
|
||||
|
||||
У наступних розділах ви побачите інші варіанти, конфігурації та додаткові можливості.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Наступні розділи **не обов’язково є «розширеними»**.
|
||||
|
||||
І цілком можливо, що для вашого випадку використання рішення міститься в одному з них.
|
||||
|
||||
///
|
||||
|
||||
## Спочатку прочитайте підручник { #read-the-tutorial-first }
|
||||
|
||||
Ви все одно можете використовувати більшість можливостей **FastAPI**, маючи знання з основного [Підручника — Посібника користувача](../tutorial/index.md){.internal-link target=_blank}.
|
||||
|
||||
А наступні розділи припускають, що ви вже прочитали його та знаєте ці основні ідеї.
|
||||
97
docs/uk/docs/advanced/middleware.md
Normal file
97
docs/uk/docs/advanced/middleware.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Розширені middleware { #advanced-middleware }
|
||||
|
||||
У головному посібнику ви прочитали, як додати [власний Middleware](../tutorial/middleware.md){.internal-link target=_blank} до вашого застосунку.
|
||||
|
||||
Також ви прочитали, як обробляти [CORS за допомогою `CORSMiddleware`](../tutorial/cors.md){.internal-link target=_blank}.
|
||||
|
||||
У цьому розділі ми розглянемо, як використовувати інші middleware.
|
||||
|
||||
## Додавання ASGI middleware { #adding-asgi-middlewares }
|
||||
|
||||
Оскільки **FastAPI** базується на Starlette і реалізує специфікацію <abbr title="Asynchronous Server Gateway Interface">ASGI</abbr>, ви можете використовувати будь-який ASGI middleware.
|
||||
|
||||
Middleware не обов’язково має бути зроблений спеціально для FastAPI або Starlette, головне — щоб він відповідав специфікації ASGI.
|
||||
|
||||
Загалом, ASGI middleware — це класи, які очікують отримати ASGI застосунок як перший аргумент.
|
||||
|
||||
Тож у документації сторонніх ASGI middleware вам, імовірно, скажуть зробити щось на кшталт:
|
||||
|
||||
```Python
|
||||
from unicorn import UnicornMiddleware
|
||||
|
||||
app = SomeASGIApp()
|
||||
|
||||
new_app = UnicornMiddleware(app, some_config="rainbow")
|
||||
```
|
||||
|
||||
Але FastAPI (фактично Starlette) надає простіший спосіб, який гарантує, що внутрішні middleware коректно обробляють помилки сервера, а користувацькі обробники винятків працюють правильно.
|
||||
|
||||
Для цього використовуйте `app.add_middleware()` (як у прикладі для CORS).
|
||||
|
||||
```Python
|
||||
from fastapi import FastAPI
|
||||
from unicorn import UnicornMiddleware
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.add_middleware(UnicornMiddleware, some_config="rainbow")
|
||||
```
|
||||
|
||||
`app.add_middleware()` отримує клас middleware як перший аргумент і будь-які додаткові аргументи, які буде передано middleware.
|
||||
|
||||
## Інтегровані middleware { #integrated-middlewares }
|
||||
|
||||
**FastAPI** містить кілька middleware для поширених випадків використання; далі розглянемо, як ними користуватися.
|
||||
|
||||
/// note | Технічні деталі
|
||||
|
||||
Для наступних прикладів ви також можете використати `from starlette.middleware.something import SomethingMiddleware`.
|
||||
|
||||
**FastAPI** надає кілька middleware у `fastapi.middleware` лише для зручності для вас, розробника. Але більшість доступних middleware походять безпосередньо зі Starlette.
|
||||
|
||||
///
|
||||
|
||||
## `HTTPSRedirectMiddleware` { #httpsredirectmiddleware }
|
||||
|
||||
Гарантує, що всі вхідні запити мають бути або `https`, або `wss`.
|
||||
|
||||
Будь-який вхідний запит до `http` або `ws` буде перенаправлено на безпечну схему.
|
||||
|
||||
{* ../../docs_src/advanced_middleware/tutorial001_py39.py hl[2,6] *}
|
||||
|
||||
## `TrustedHostMiddleware` { #trustedhostmiddleware }
|
||||
|
||||
Гарантує, що всі вхідні запити мають коректно встановлений заголовок `Host`, щоб захиститися від атак на HTTP Host Header.
|
||||
|
||||
{* ../../docs_src/advanced_middleware/tutorial002_py39.py hl[2,6:8] *}
|
||||
|
||||
Підтримуються такі аргументи:
|
||||
|
||||
* `allowed_hosts` — список доменних імен, які слід дозволити як імена хостів. Підтримуються домени з wildcard, як-от `*.example.com`, для зіставлення піддоменів. Щоб дозволити будь-яке ім’я хоста, використайте `allowed_hosts=["*"]` або не додавайте цей middleware.
|
||||
* `www_redirect` — якщо встановлено в True, запити до версій дозволених хостів без `www` буде перенаправлено на відповідні версії з `www`. За замовчуванням `True`.
|
||||
|
||||
Якщо вхідний запит не проходить перевірку, буде надіслано відповідь `400`.
|
||||
|
||||
## `GZipMiddleware` { #gzipmiddleware }
|
||||
|
||||
Обробляє GZip-відповіді для будь-якого запиту, що містить `"gzip"` у заголовку `Accept-Encoding`.
|
||||
|
||||
Middleware оброблятиме як стандартні, так і потокові відповіді.
|
||||
|
||||
{* ../../docs_src/advanced_middleware/tutorial003_py39.py hl[2,6] *}
|
||||
|
||||
Підтримуються такі аргументи:
|
||||
|
||||
* `minimum_size` — не застосовувати GZip до відповідей, менших за цей мінімальний розмір у байтах. За замовчуванням `500`.
|
||||
* `compresslevel` — використовується під час стиснення GZip. Це ціле число в діапазоні від 1 до 9. За замовчуванням `9`. Менше значення дає швидше стиснення, але більший розмір файлів, тоді як більше значення — повільніше стиснення, але менший розмір файлів.
|
||||
|
||||
## Інші middleware { #other-middlewares }
|
||||
|
||||
Існує багато інших ASGI middleware.
|
||||
|
||||
Наприклад:
|
||||
|
||||
* <a href="https://github.com/encode/uvicorn/blob/master/uvicorn/middleware/proxy_headers.py" class="external-link" target="_blank">`ProxyHeadersMiddleware` в Uvicorn</a>
|
||||
* <a href="https://github.com/florimondmanca/msgpack-asgi" class="external-link" target="_blank">MessagePack</a>
|
||||
|
||||
Щоб переглянути інші доступні middleware, ознайомтеся з <a href="https://www.starlette.dev/middleware/" class="external-link" target="_blank">документацією Starlette щодо Middleware</a> та <a href="https://github.com/florimondmanca/awesome-asgi" class="external-link" target="_blank">ASGI Awesome List</a>.
|
||||
186
docs/uk/docs/advanced/openapi-callbacks.md
Normal file
186
docs/uk/docs/advanced/openapi-callbacks.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# OpenAPI callbacks { #openapi-callbacks }
|
||||
|
||||
Ви можете створити API з *операцією шляху*, яка може запускати запит до *зовнішнього API*, створеного кимось іншим (ймовірно, тим самим розробником, який буде *використовувати* ваше API).
|
||||
|
||||
Процес, що відбувається, коли ваш застосунок API викликає *зовнішнє API*, називається «callback». Тому що програмне забезпечення, яке написав зовнішній розробник, надсилає запит до вашого API, а потім ваше API «викликає у відповідь», надсилаючи запит до *зовнішнього API* (яке, ймовірно, створив той самий розробник).
|
||||
|
||||
У цьому випадку вам може знадобитися задокументувати, як *має* виглядати це зовнішнє API. Яку *операцію шляху* воно повинно мати, яке тіло має очікувати, яку відповідь має повертати тощо.
|
||||
|
||||
## Застосунок із callbacks { #an-app-with-callbacks }
|
||||
|
||||
Розгляньмо це на прикладі.
|
||||
|
||||
Уявіть, що ви розробляєте застосунок, який дає змогу створювати рахунки.
|
||||
|
||||
Ці рахунки матимуть `id`, `title` (необов’язково), `customer` і `total`.
|
||||
|
||||
Користувач вашого API (зовнішній розробник) створить рахунок у вашому API за допомогою POST-запиту.
|
||||
|
||||
Потім ваше API (уявімо) буде:
|
||||
|
||||
* Надсилати рахунок якомусь клієнту зовнішнього розробника.
|
||||
* Отримувати оплату.
|
||||
* Надсилати сповіщення назад користувачу API (зовнішньому розробнику).
|
||||
* Це буде зроблено шляхом надсилання POST-запиту (від *вашого API*) до певного *зовнішнього API*, наданого цим зовнішнім розробником (це і є «callback»).
|
||||
|
||||
## Звичайний застосунок **FastAPI** { #the-normal-fastapi-app }
|
||||
|
||||
Спочатку подивімося, як виглядатиме звичайний застосунок API до додавання callback.
|
||||
|
||||
Він матиме *операцію шляху*, яка отримуватиме тіло `Invoice`, і query-параметр `callback_url`, що міститиме URL для callback.
|
||||
|
||||
Ця частина доволі стандартна, більшість коду вам, ймовірно, уже знайома:
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *}
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Query-параметр `callback_url` використовує тип Pydantic <a href="https://docs.pydantic.dev/latest/api/networks/" class="external-link" target="_blank">Url</a>.
|
||||
|
||||
///
|
||||
|
||||
Єдине нове тут — це `callbacks=invoices_callback_router.routes` як аргумент для *декоратора операції шляху*. Далі подивимося, що це таке.
|
||||
|
||||
## Документування callback { #documenting-the-callback }
|
||||
|
||||
Фактичний код callback сильно залежатиме від вашого застосунку API.
|
||||
|
||||
І, ймовірно, значно відрізнятиметься від застосунку до застосунку.
|
||||
|
||||
Це може бути лише один або два рядки коду, наприклад:
|
||||
|
||||
```Python
|
||||
callback_url = "https://example.com/api/v1/invoices/events/"
|
||||
httpx.post(callback_url, json={"description": "Invoice paid", "paid": True})
|
||||
```
|
||||
|
||||
Але, можливо, найважливіша частина callback — переконатися, що користувач вашого API (зовнішній розробник) реалізує *зовнішнє API* правильно, відповідно до даних, які *ваше API* надсилатиме в тілі запиту callback тощо.
|
||||
|
||||
Тож далі ми додамо код, щоб задокументувати, як має виглядати це *зовнішнє API*, аби приймати callback від *вашого API*.
|
||||
|
||||
Ця документація з’явиться в Swagger UI за адресою `/docs` у вашому API і дасть змогу зовнішнім розробникам зрозуміти, як побудувати *зовнішнє API*.
|
||||
|
||||
Цей приклад не реалізує сам callback (це може бути просто один рядок коду), лише частину документації.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Фактичний callback — це просто HTTP-запит.
|
||||
|
||||
Під час реалізації callback ви можете використати щось на кшталт <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a> або <a href="https://requests.readthedocs.io/" class="external-link" target="_blank">Requests</a>.
|
||||
|
||||
///
|
||||
|
||||
## Напишіть код документації для callback { #write-the-callback-documentation-code }
|
||||
|
||||
Цей код не виконуватиметься у вашому застосунку, він потрібен лише для *документування* того, як має виглядати *зовнішнє API*.
|
||||
|
||||
Але ви вже знаєте, як легко створювати автоматичну документацію для API з **FastAPI**.
|
||||
|
||||
Тож ми використаємо ті самі знання, щоб задокументувати, як має виглядати *зовнішнє API*… створивши *операцію(ї) шляху*, які зовнішнє API повинно реалізувати (ті, які викликатиме ваше API).
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Під час написання коду для документування callback може бути корисно уявити, що ви — той *зовнішній розробник*. І що зараз ви реалізуєте *зовнішнє API*, а не *ваше API*.
|
||||
|
||||
Тимчасово прийнявши цю точку зору (з позиції *зовнішнього розробника*), вам може бути очевидніше, куди розміщувати параметри, Pydantic-модель для тіла, для відповіді тощо для цього *зовнішнього API*.
|
||||
|
||||
///
|
||||
|
||||
### Створіть callback `APIRouter` { #create-a-callback-apirouter }
|
||||
|
||||
Спочатку створіть новий `APIRouter`, який міститиме один або більше callbacks.
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *}
|
||||
|
||||
### Створіть callback *операцію шляху* { #create-the-callback-path-operation }
|
||||
|
||||
Щоб створити callback *операцію шляху*, використайте той самий `APIRouter`, який ви створили вище.
|
||||
|
||||
Вона має виглядати як звичайна *операція шляху* FastAPI:
|
||||
|
||||
* Ймовірно, вона має оголошувати тіло, яке повинна отримувати, наприклад `body: InvoiceEvent`.
|
||||
* Також вона може оголошувати відповідь, яку повинна повертати, наприклад `response_model=InvoiceEventReceived`.
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *}
|
||||
|
||||
Є 2 основні відмінності від звичайної *операції шляху*:
|
||||
|
||||
* Їй не потрібен жоден фактичний код, бо ваш застосунок ніколи не викликатиме цей код. Його використовують лише для документування *зовнішнього API*. Тому функція може просто містити `pass`.
|
||||
* *Шлях* може містити <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#key-expression" class="external-link" target="_blank">вираз OpenAPI 3</a> (див. більше нижче), де можна використовувати змінні з параметрами та частинами початкового запиту, надісланого до *вашого API*.
|
||||
|
||||
### Вираз шляху для callback { #the-callback-path-expression }
|
||||
|
||||
Callback-*шлях* може містити <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#key-expression" class="external-link" target="_blank">вираз OpenAPI 3</a>, який може включати частини початкового запиту, надісланого до *вашого API*.
|
||||
|
||||
У цьому випадку це `str`:
|
||||
|
||||
```Python
|
||||
"{$callback_url}/invoices/{$request.body.id}"
|
||||
```
|
||||
|
||||
Отже, якщо користувач вашого API (зовнішній розробник) надсилає запит до *вашого API* на:
|
||||
|
||||
```
|
||||
https://yourapi.com/invoices/?callback_url=https://www.external.org/events
|
||||
```
|
||||
|
||||
з JSON-тілом:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"id": "2expen51ve",
|
||||
"customer": "Mr. Richie Rich",
|
||||
"total": "9999"
|
||||
}
|
||||
```
|
||||
|
||||
тоді *ваше API* обробить рахунок і в якийсь момент пізніше надішле callback-запит на `callback_url` (до *зовнішнього API*):
|
||||
|
||||
```
|
||||
https://www.external.org/events/invoices/2expen51ve
|
||||
```
|
||||
|
||||
з JSON-тілом, яке міститиме щось на кшталт:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"description": "Payment celebration",
|
||||
"paid": true
|
||||
}
|
||||
```
|
||||
|
||||
і воно очікуватиме відповідь від цього *зовнішнього API* з JSON-тілом на кшталт:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
```
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Зверніть увагу: використаний URL для callback містить URL, отриманий як query-параметр у `callback_url` (`https://www.external.org/events`), а також `id` рахунку зсередини JSON-тіла (`2expen51ve`).
|
||||
|
||||
///
|
||||
|
||||
### Додайте callback router { #add-the-callback-router }
|
||||
|
||||
На цьому етапі у вас є потрібні *callback-операції шляху* (ті, які *зовнішній розробник* має реалізувати в *зовнішньому API*) у callback router, який ви створили вище.
|
||||
|
||||
Тепер використайте параметр `callbacks` у *декораторі операції шляху вашого API*, щоб передати атрибут `.routes` (це фактично просто `list` маршрутів/*операцій шляху*) з цього callback router:
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *}
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Зверніть увагу, що ви передаєте не сам router (`invoices_callback_router`) у `callback=`, а атрибут `.routes`, тобто `invoices_callback_router.routes`.
|
||||
|
||||
///
|
||||
|
||||
### Перевірте документацію { #check-the-docs }
|
||||
|
||||
Тепер ви можете запустити застосунок і перейти за адресою <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||
|
||||
Ви побачите вашу документацію, зокрема розділ «Callbacks» для вашої *операції шляху*, який показує, як має виглядати *зовнішнє API*:
|
||||
|
||||
<img src="/img/tutorial/openapi-callbacks/image01.png">
|
||||
55
docs/uk/docs/advanced/openapi-webhooks.md
Normal file
55
docs/uk/docs/advanced/openapi-webhooks.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# OpenAPI вебхуки { #openapi-webhooks }
|
||||
|
||||
Бувають випадки, коли ви хочете повідомити **користувачам** вашого API, що ваш застосунок може викликати *їхній* застосунок (надсилаючи запит) з певними даними, зазвичай щоб **сповістити** про певний **івент**.
|
||||
|
||||
Це означає, що замість звичного процесу, коли користувачі надсилають запити до вашого API, **ваше API** (або ваш застосунок) може **надсилати запити до їхньої системи** (до їхнього API, їхнього застосунку).
|
||||
|
||||
Зазвичай це називають **webhook**.
|
||||
|
||||
## Кроки роботи вебхуків { #webhooks-steps }
|
||||
|
||||
Зазвичай процес такий: **ви визначаєте** у своєму коді, яке саме повідомлення ви надсилатимете — **тіло запиту**.
|
||||
|
||||
Також ви певним чином визначаєте, у які **моменти** ваш застосунок надсилатиме ці запити або івенти.
|
||||
|
||||
А **ваші користувачі** певним чином (наприклад, у вебпанелі керування десь) визначають **URL**, куди ваш застосунок має надсилати ці запити.
|
||||
|
||||
Уся **логіка** щодо реєстрації URL для вебхуків і код, який фактично надсилає ці запити, залишається на ваш розсуд. Ви пишете це так, як хочете, у **власному коді**.
|
||||
|
||||
## Документування вебхуків за допомогою **FastAPI** та OpenAPI { #documenting-webhooks-with-fastapi-and-openapi }
|
||||
|
||||
У **FastAPI**, використовуючи OpenAPI, ви можете визначати назви цих вебхуків, типи HTTP-операцій, які ваш застосунок може надсилати (наприклад, `POST`, `PUT` тощо), і **тіла** запитів, які ваш застосунок надсилатиме.
|
||||
|
||||
Це може значно спростити для ваших користувачів **реалізацію їхніх API** для отримання ваших запитів **webhook** — вони навіть можуть згенерувати частину власного коду API автоматично.
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Webhooks доступні в OpenAPI 3.1.0 і вище, підтримуються FastAPI `0.99.0` і вище.
|
||||
|
||||
///
|
||||
|
||||
## Застосунок із вебхуками { #an-app-with-webhooks }
|
||||
|
||||
Коли ви створюєте застосунок **FastAPI**, є атрибут `webhooks`, який можна використовувати для визначення *webhooks* так само, як ви визначаєте *операції шляху*, наприклад за допомогою `@app.webhooks.post()`.
|
||||
|
||||
{* ../../docs_src/openapi_webhooks/tutorial001_py39.py hl[9:13,36:53] *}
|
||||
|
||||
Визначені вами webhooks потраплять до схеми **OpenAPI** та автоматичного **інтерфейсу документації**.
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Об’єкт `app.webhooks` насправді є просто `APIRouter`, тим самим типом, який ви використовуєте під час структурування застосунку на кілька файлів.
|
||||
|
||||
///
|
||||
|
||||
Зверніть увагу, що з webhooks ви фактично не оголошуєте *path* (як-от `/items/`) — текст, який ви передаєте, є лише **ідентифікатором** вебхука (назвою івенту). Наприклад, у `@app.webhooks.post("new-subscription")` назва вебхука — `new-subscription`.
|
||||
|
||||
Це тому, що очікується, що **ваші користувачі** визначать фактичний **шлях URL**, за яким вони хочуть отримувати запит webhook, якимось іншим способом (наприклад, у вебпанелі керування).
|
||||
|
||||
### Перевірте документацію { #check-the-docs }
|
||||
|
||||
Тепер ви можете запустити застосунок і перейти на <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||
|
||||
Ви побачите, що у документації є звичайні *операції шляху*, а також деякі **webhooks**:
|
||||
|
||||
<img src="/img/tutorial/openapi-webhooks/image01.png">
|
||||
172
docs/uk/docs/advanced/path-operation-advanced-configuration.md
Normal file
172
docs/uk/docs/advanced/path-operation-advanced-configuration.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# Розширена конфігурація операцій шляху { #path-operation-advanced-configuration }
|
||||
|
||||
## OpenAPI operationId { #openapi-operationid }
|
||||
|
||||
/// warning | Попередження
|
||||
|
||||
Якщо ви не є «експертом» з OpenAPI, імовірно, вам це не потрібно.
|
||||
|
||||
///
|
||||
|
||||
Ви можете встановити OpenAPI `operationId`, який буде використано у вашій *операції шляху*, за допомогою параметра `operation_id`.
|
||||
|
||||
Потрібно переконатися, що він унікальний для кожної операції.
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *}
|
||||
|
||||
### Використання назви *функції операції шляху* як operationId { #using-the-path-operation-function-name-as-the-operationid }
|
||||
|
||||
Якщо ви хочете використовувати назви функцій вашого API як `operationId`, ви можете пройтися по всіх них і перевизначити `operation_id` кожної *операції шляху*, використовуючи `APIRoute.name`.
|
||||
|
||||
Це слід робити після додавання всіх ваших *операцій шляху*.
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial002_py39.py hl[2, 12:21, 24] *}
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Якщо ви викликаєте `app.openapi()` вручну, вам слід оновити `operationId` до цього.
|
||||
|
||||
///
|
||||
|
||||
/// warning | Попередження
|
||||
|
||||
Якщо ви робите це, потрібно переконатися, що кожна з ваших *функцій операції шляху* має унікальне ім’я.
|
||||
|
||||
Навіть якщо вони розташовані в різних модулях (файлах Python).
|
||||
|
||||
///
|
||||
|
||||
## Виключення з OpenAPI { #exclude-from-openapi }
|
||||
|
||||
Щоб виключити *операцію шляху* зі згенерованої схеми OpenAPI (а отже, і з автоматичних систем документації), використайте параметр `include_in_schema` і встановіть його в `False`:
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial003_py39.py hl[6] *}
|
||||
|
||||
## Розширений опис із docstring { #advanced-description-from-docstring }
|
||||
|
||||
Ви можете обмежити кількість рядків docstring *функції операції шляху*, які використовуються для OpenAPI.
|
||||
|
||||
Додавання `\f` (екранованого символу «form feed») змушує **FastAPI** обрізати вивід, що використовується для OpenAPI, у цьому місці.
|
||||
|
||||
Це не відображатиметься в документації, але інші інструменти (наприклад, Sphinx) зможуть використати решту.
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *}
|
||||
|
||||
## Додаткові відповіді { #additional-responses }
|
||||
|
||||
Ви, ймовірно, бачили, як оголошувати `response_model` і `status_code` для *операції шляху*.
|
||||
|
||||
Це визначає метадані про основну відповідь *операції шляху*.
|
||||
|
||||
Ви також можете оголосити додаткові відповіді з їхніми моделями, кодами стану тощо.
|
||||
|
||||
У документації є цілий розділ про це, прочитайте його тут: [Додаткові відповіді в OpenAPI](additional-responses.md){.internal-link target=_blank}.
|
||||
|
||||
## OpenAPI Extra { #openapi-extra }
|
||||
|
||||
Коли ви оголошуєте *операцію шляху* у вашому застосунку, **FastAPI** автоматично генерує відповідні метадані про цю *операцію шляху* для включення в схему OpenAPI.
|
||||
|
||||
/// note | Технічні деталі
|
||||
|
||||
У специфікації OpenAPI це називається <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#operation-object" class="external-link" target="_blank">Operation Object</a>.
|
||||
|
||||
///
|
||||
|
||||
Він містить усю інформацію про *операцію шляху* та використовується для генерації автоматичної документації.
|
||||
|
||||
Він включає `tags`, `parameters`, `requestBody`, `responses` тощо.
|
||||
|
||||
Ця частина схеми OpenAPI, специфічна для *операції шляху*, зазвичай генерується **FastAPI** автоматично, але ви також можете її розширити.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Це низькорівнева точка розширення.
|
||||
|
||||
Якщо вам потрібно лише оголосити додаткові відповіді, зручніший спосіб — через [Додаткові відповіді в OpenAPI](additional-responses.md){.internal-link target=_blank}.
|
||||
|
||||
///
|
||||
|
||||
Ви можете розширити схему OpenAPI для *операції шляху* за допомогою параметра `openapi_extra`.
|
||||
|
||||
### Розширення OpenAPI { #openapi-extensions }
|
||||
|
||||
Цей `openapi_extra` може бути корисним, наприклад, щоб оголосити [OpenAPI Extensions](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions):
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial005_py39.py hl[6] *}
|
||||
|
||||
Якщо ви відкриєте автоматичну документацію API, ваше розширення з’явиться внизу конкретної *операції шляху*.
|
||||
|
||||
<img src="/img/tutorial/path-operation-advanced-configuration/image01.png">
|
||||
|
||||
А якщо ви подивитеся на результуючий OpenAPI (за адресою `/openapi.json` у вашому API), ви також побачите ваше розширення як частину конкретної *операції шляху*:
|
||||
|
||||
```JSON hl_lines="22"
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "FastAPI",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"summary": "Read Items",
|
||||
"operationId": "read_items_items__get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-aperture-labs-portal": "blue"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Власна OpenAPI-схема для *операції шляху* { #custom-openapi-path-operation-schema }
|
||||
|
||||
Словник у `openapi_extra` буде глибоко об’єднано з автоматично згенерованою схемою OpenAPI для *операції шляху*.
|
||||
|
||||
Тож ви можете додати додаткові дані до автоматично згенерованої схеми.
|
||||
|
||||
Наприклад, ви можете вирішити читати й перевіряти запит власним кодом, не використовуючи автоматичні можливості FastAPI з Pydantic, але все одно хотіти визначити запит у схемі OpenAPI.
|
||||
|
||||
Це можна зробити за допомогою `openapi_extra`:
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial006_py39.py hl[19:36, 39:40] *}
|
||||
|
||||
У цьому прикладі ми не оголошували жодної моделі Pydantic. Фактично тіло запиту навіть не <abbr title="converted from some plain format, like bytes, into Python objects - перетворення з простого формату, наприклад bytes, у об’єкти Python">parsed</abbr> як JSON — його читають безпосередньо як `bytes`, а функція `magic_data_reader()` відповідала б за його розбір певним способом.
|
||||
|
||||
Попри це, ми можемо оголосити очікувану схему для тіла запиту.
|
||||
|
||||
### Власний тип вмісту OpenAPI { #custom-openapi-content-type }
|
||||
|
||||
Використовуючи цей самий підхід, ви можете застосувати модель Pydantic, щоб визначити JSON Schema, яку потім буде включено до власної секції схеми OpenAPI для *операції шляху*.
|
||||
|
||||
І ви можете зробити це навіть тоді, коли тип даних у запиті — не JSON.
|
||||
|
||||
Наприклад, у цьому застосунку ми не використовуємо інтегровану функціональність FastAPI для витягування JSON Schema з моделей Pydantic, а також автоматичну валідацію для JSON. Насправді ми оголошуємо тип вмісту запиту як YAML, а не JSON:
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *}
|
||||
|
||||
Попри це, хоча ми не використовуємо стандартну інтегровану функціональність, ми все одно використовуємо модель Pydantic, щоб вручну згенерувати JSON Schema для даних, які ми хочемо отримати в YAML.
|
||||
|
||||
Далі ми використовуємо запит безпосередньо й витягаємо тіло як `bytes`. Це означає, що FastAPI навіть не намагатиметься розбирати корисне навантаження запиту як JSON.
|
||||
|
||||
А потім у нашому коді ми напряму розбираємо цей YAML-вміст і знову використовуємо ту саму модель Pydantic, щоб валідувати YAML-вміст:
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *}
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Тут ми повторно використовуємо ту саму модель Pydantic.
|
||||
|
||||
Але так само ми могли б валідувати це іншим способом.
|
||||
|
||||
///
|
||||
31
docs/uk/docs/advanced/response-change-status-code.md
Normal file
31
docs/uk/docs/advanced/response-change-status-code.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Відповідь — змінення коду статусу { #response-change-status-code }
|
||||
|
||||
Ймовірно, ви вже читали, що можна встановити типовий [Код статусу відповіді](../tutorial/response-status-code.md){.internal-link target=_blank}.
|
||||
|
||||
Але в деяких випадках потрібно повернути інший код статусу, ніж типовий.
|
||||
|
||||
## Випадок використання { #use-case }
|
||||
|
||||
Наприклад, уявіть, що ви хочете типово повертати HTTP-код статусу «OK» `200`.
|
||||
|
||||
Але якщо дані не існували, ви хочете створити їх і повернути HTTP-код статусу «CREATED» `201`.
|
||||
|
||||
Водночас ви все одно хочете мати змогу фільтрувати та перетворювати дані, які повертаєте, за допомогою `response_model`.
|
||||
|
||||
Для таких випадків можна використати параметр `Response`.
|
||||
|
||||
## Використайте параметр `Response` { #use-a-response-parameter }
|
||||
|
||||
Ви можете оголосити параметр типу `Response` у вашій *функції операції шляху* (так само, як це можна зробити для cookies і headers).
|
||||
|
||||
Після цього ви можете встановити `status_code` у цьому *тимчасовому* об’єкті відповіді.
|
||||
|
||||
{* ../../docs_src/response_change_status_code/tutorial001_py39.py hl[1,9,12] *}
|
||||
|
||||
Потім ви можете повернути будь-який потрібний об’єкт, як зазвичай (наприклад, `dict`, модель бази даних тощо).
|
||||
|
||||
І якщо ви оголосили `response_model`, його й надалі буде використано для фільтрації та перетворення об’єкта, який ви повернули.
|
||||
|
||||
**FastAPI** використає цю *тимчасову* відповідь, щоб отримати код статусу (а також cookies і headers), і додасть їх до фінальної відповіді, що містить значення, яке ви повернули, відфільтроване через будь-який `response_model`.
|
||||
|
||||
Також можна оголосити параметр `Response` у залежностях і встановлювати в них код статусу. Але майте на увазі: спрацює останнє встановлене значення.
|
||||
51
docs/uk/docs/advanced/response-cookies.md
Normal file
51
docs/uk/docs/advanced/response-cookies.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Cookies у відповіді { #response-cookies }
|
||||
|
||||
## Використовуйте параметр `Response` { #use-a-response-parameter }
|
||||
|
||||
Ви можете оголосити параметр типу `Response` у вашій *функції операції шляху*.
|
||||
|
||||
Після цього ви можете встановлювати cookies в цьому *тимчасовому* об’єкті відповіді.
|
||||
|
||||
{* ../../docs_src/response_cookies/tutorial002_py39.py hl[1, 8:9] *}
|
||||
|
||||
Далі ви можете повертати будь-який потрібний об’єкт, як зазвичай (наприклад, `dict`, модель бази даних тощо).
|
||||
|
||||
І якщо ви оголосили `response_model`, він усе одно використовуватиметься для фільтрації та перетворення об’єкта, який ви повернули.
|
||||
|
||||
**FastAPI** використає цю *тимчасову* відповідь, щоб витягнути cookies (а також заголовки та код статусу), і додасть їх до фінальної відповіді, що містить значення, яке ви повернули, відфільтроване згідно з будь-яким `response_model`.
|
||||
|
||||
Також ви можете оголосити параметр `Response` у залежностях і встановлювати в них cookies (і заголовки).
|
||||
|
||||
## Повертайте `Response` напряму { #return-a-response-directly }
|
||||
|
||||
Ви також можете створювати cookies, повертаючи `Response` напряму у вашому коді.
|
||||
|
||||
Для цього створіть відповідь, як описано в [Повертайте Response напряму](response-directly.md){.internal-link target=_blank}.
|
||||
|
||||
Потім встановіть у ній Cookies і поверніть її:
|
||||
|
||||
{* ../../docs_src/response_cookies/tutorial001_py39.py hl[10:12] *}
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Пам’ятайте: якщо ви повертаєте відповідь напряму замість використання параметра `Response`, FastAPI поверне її безпосередньо.
|
||||
|
||||
Тож вам потрібно переконатися, що ваші дані мають правильний тип. Наприклад, вони сумісні з JSON, якщо ви повертаєте `JSONResponse`.
|
||||
|
||||
Також переконайтеся, що ви не надсилаєте дані, які мали бути відфільтровані через `response_model`.
|
||||
|
||||
///
|
||||
|
||||
### Докладніше { #more-info }
|
||||
|
||||
/// note | Технічні деталі
|
||||
|
||||
Ви також можете використовувати `from starlette.responses import Response` або `from starlette.responses import JSONResponse`.
|
||||
|
||||
**FastAPI** надає ті самі `starlette.responses` як `fastapi.responses` просто для зручності для вас, розробника. Але більшість доступних відповідей надходять безпосередньо зі Starlette.
|
||||
|
||||
І оскільки `Response` часто використовують для встановлення заголовків і cookies, **FastAPI** також надає його як `fastapi.Response`.
|
||||
|
||||
///
|
||||
|
||||
Щоб переглянути всі доступні параметри й опції, ознайомтеся з <a href="https://www.starlette.dev/responses/#set-cookie" class="external-link" target="_blank">документацією Starlette</a>.
|
||||
65
docs/uk/docs/advanced/response-directly.md
Normal file
65
docs/uk/docs/advanced/response-directly.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Повертайте відповідь напряму { #return-a-response-directly }
|
||||
|
||||
Коли ви створюєте **FastAPI** *операцію шляху*, зазвичай ви можете повертати з неї будь-які дані: `dict`, `list`, модель Pydantic, модель бази даних тощо.
|
||||
|
||||
За замовчуванням **FastAPI** автоматично перетворює це значення, що повертається, на JSON за допомогою `jsonable_encoder`, описаного в розділі [JSON-сумісний енкодер](../tutorial/encoder.md){.internal-link target=_blank}.
|
||||
|
||||
Потім «за лаштунками» він помістить ці JSON-сумісні дані (наприклад, `dict`) всередину `JSONResponse`, який буде використано для надсилання відповіді клієнту.
|
||||
|
||||
Але ви можете повертати `JSONResponse` безпосередньо з ваших *операцій шляху*.
|
||||
|
||||
Це може бути корисно, наприклад, щоб повертати власні заголовки або cookies.
|
||||
|
||||
## Повернення `Response` { #return-a-response }
|
||||
|
||||
Насправді ви можете повернути будь-який `Response` або будь-який його підклас.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
`JSONResponse` сам по собі є підкласом `Response`.
|
||||
|
||||
///
|
||||
|
||||
І коли ви повертаєте `Response`, **FastAPI** передасть його напряму.
|
||||
|
||||
Він не виконуватиме жодних перетворень даних за допомогою моделей Pydantic, не конвертуватиме вміст у будь-які типи тощо.
|
||||
|
||||
Це дає вам багато гнучкості. Ви можете повертати будь-який тип даних, перевизначати будь-які декларації даних або валідацію тощо.
|
||||
|
||||
## Використання `jsonable_encoder` у `Response` { #using-the-jsonable-encoder-in-a-response }
|
||||
|
||||
Оскільки **FastAPI** не вносить жодних змін у `Response`, який ви повертаєте, вам потрібно переконатися, що його вміст уже готовий.
|
||||
|
||||
Наприклад, ви не можете покласти модель Pydantic у `JSONResponse`, не перетворивши її спочатку на `dict` із конвертацією всіх типів даних (як-от `datetime`, `UUID` тощо) у JSON-сумісні типи.
|
||||
|
||||
Для таких випадків ви можете використати `jsonable_encoder`, щоб перетворити ваші дані перед передаванням їх у відповідь:
|
||||
|
||||
{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *}
|
||||
|
||||
/// note | Технічні деталі
|
||||
|
||||
Ви також можете використати `from starlette.responses import JSONResponse`.
|
||||
|
||||
**FastAPI** надає ті самі `starlette.responses` як `fastapi.responses` лише для зручності для вас, розробника. Але більшість доступних відповідей надходять безпосередньо зі Starlette.
|
||||
|
||||
///
|
||||
|
||||
## Повернення власного `Response` { #returning-a-custom-response }
|
||||
|
||||
Приклад вище показує всі потрібні частини, але він поки що не дуже корисний, адже ви могли б просто повернути `item` напряму, і **FastAPI** помістив би його в `JSONResponse` для вас, перетворивши його на `dict` тощо. Усе це — за замовчуванням.
|
||||
|
||||
Тепер подивімося, як ви могли б використати це, щоб повернути власну відповідь.
|
||||
|
||||
Скажімо, ви хочете повернути відповідь у форматі <a href="https://en.wikipedia.org/wiki/XML" class="external-link" target="_blank">XML</a>.
|
||||
|
||||
Ви можете помістити ваш XML-вміст у рядок, покласти його в `Response` і повернути:
|
||||
|
||||
{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *}
|
||||
|
||||
## Примітки { #notes }
|
||||
|
||||
Коли ви повертаєте `Response` напряму, його дані не валідовуються, не конвертуються (серіалізуються) і не документуються автоматично.
|
||||
|
||||
Але ви все одно можете задокументувати це, як описано в [Додаткові відповіді в OpenAPI](additional-responses.md){.internal-link target=_blank}.
|
||||
|
||||
У наступних розділах ви побачите, як використовувати/оголошувати ці власні `Response`, зберігаючи при цьому автоматичну конвертацію даних, документацію тощо.
|
||||
41
docs/uk/docs/advanced/response-headers.md
Normal file
41
docs/uk/docs/advanced/response-headers.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Заголовки відповіді { #response-headers }
|
||||
|
||||
## Використовуйте параметр `Response` { #use-a-response-parameter }
|
||||
|
||||
Ви можете оголосити параметр типу `Response` у вашій *функції операції шляху* (так само, як і для cookies).
|
||||
|
||||
Після цього ви можете встановлювати заголовки в цьому *тимчасовому* об’єкті відповіді.
|
||||
|
||||
{* ../../docs_src/response_headers/tutorial002_py39.py hl[1, 7:8] *}
|
||||
|
||||
Далі ви можете повертати будь-який потрібний об’єкт, як зазвичай (наприклад, `dict`, модель бази даних тощо).
|
||||
|
||||
Якщо ви оголосили `response_model`, він усе одно буде використаний, щоб відфільтрувати та перетворити повернений об’єкт.
|
||||
|
||||
**FastAPI** використає цю *тимчасову* відповідь, щоб витягти заголовки (а також cookies і код стану), і додасть їх до фінальної відповіді, що містить значення, яке ви повернули, відфільтроване через `response_model`.
|
||||
|
||||
Також ви можете оголосити параметр `Response` у залежностях і встановлювати в них заголовки (та cookies).
|
||||
|
||||
## Повертайте `Response` напряму { #return-a-response-directly }
|
||||
|
||||
Ви також можете додавати заголовки, коли повертаєте `Response` безпосередньо.
|
||||
|
||||
Створіть відповідь, як описано в [Повернути Response напряму](response-directly.md){.internal-link target=_blank}, і передайте заголовки як додатковий параметр:
|
||||
|
||||
{* ../../docs_src/response_headers/tutorial001_py39.py hl[10:12] *}
|
||||
|
||||
/// note | Технічні деталі
|
||||
|
||||
Ви також можете використовувати `from starlette.responses import Response` або `from starlette.responses import JSONResponse`.
|
||||
|
||||
**FastAPI** надає ті самі `starlette.responses` як `fastapi.responses` просто для зручності для вас, розробника. Але більшість доступних відповідей надходять безпосередньо зі Starlette.
|
||||
|
||||
А оскільки `Response` часто використовується для встановлення заголовків і cookies, **FastAPI** також надає його як `fastapi.Response`.
|
||||
|
||||
///
|
||||
|
||||
## Користувацькі заголовки { #custom-headers }
|
||||
|
||||
Пам’ятайте, що власні пропрієтарні заголовки можна додавати <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">використовуючи префікс `X-`</a>.
|
||||
|
||||
Але якщо у вас є користувацькі заголовки, які ви хочете зробити видимими для клієнта в браузері, вам потрібно додати їх у налаштування CORS (докладніше в [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), використовуючи параметр `expose_headers`, описаний у <a href="https://www.starlette.dev/middleware/#corsmiddleware" class="external-link" target="_blank">документації Starlette про CORS</a>.
|
||||
107
docs/uk/docs/advanced/security/http-basic-auth.md
Normal file
107
docs/uk/docs/advanced/security/http-basic-auth.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# HTTP Basic Auth { #http-basic-auth }
|
||||
|
||||
Для найпростіших випадків ви можете використати HTTP Basic Auth.
|
||||
|
||||
У HTTP Basic Auth застосунок очікує заголовок, що містить ім’я користувача та пароль.
|
||||
|
||||
Якщо він його не отримує, то повертає помилку HTTP 401 «Unauthorized».
|
||||
|
||||
Також повертає заголовок `WWW-Authenticate` зі значенням `Basic` і необов’язковим параметром `realm`.
|
||||
|
||||
Це повідомляє браузеру показати вбудоване вікно запиту імені користувача та пароля.
|
||||
|
||||
Далі, коли ви вводите це ім’я користувача та пароль, браузер автоматично надсилає їх у заголовку.
|
||||
|
||||
## Простий HTTP Basic Auth { #simple-http-basic-auth }
|
||||
|
||||
* Імпортуйте `HTTPBasic` і `HTTPBasicCredentials`.
|
||||
* Створіть «`security` scheme», використовуючи `HTTPBasic`.
|
||||
* Використайте цей `security` із залежністю у вашій *операції шляху*.
|
||||
* Він повертає об’єкт типу `HTTPBasicCredentials`:
|
||||
* Він містить надіслані `username` і `password`.
|
||||
|
||||
{* ../../docs_src/security/tutorial006_an_py39.py hl[4,8,12] *}
|
||||
|
||||
Коли ви намагаєтеся відкрити URL уперше (або натискаєте кнопку «Execute» в документації), браузер попросить вас ввести ім’я користувача та пароль:
|
||||
|
||||
<img src="/img/tutorial/security/image12.png">
|
||||
|
||||
## Перевірка імені користувача { #check-the-username }
|
||||
|
||||
Ось більш повний приклад.
|
||||
|
||||
Використайте залежність, щоб перевірити, чи правильні ім’я користувача та пароль.
|
||||
|
||||
Для цього використайте стандартний модуль Python <a href="https://docs.python.org/3/library/secrets.html" class="external-link" target="_blank">`secrets`</a>, щоб перевірити ім’я користувача та пароль.
|
||||
|
||||
`secrets.compare_digest()` має отримувати `bytes` або `str`, що містить лише ASCII-символи (англійські), тобто він не працюватиме з такими символами, як `á`, наприклад у `Sebastián`.
|
||||
|
||||
Щоб це обійти, спочатку перетворимо `username` і `password` на `bytes`, закодувавши їх у UTF-8.
|
||||
|
||||
Після цього можна використати `secrets.compare_digest()`, щоб переконатися, що `credentials.username` дорівнює `"stanleyjobson"`, а `credentials.password` — `"swordfish"`.
|
||||
|
||||
{* ../../docs_src/security/tutorial007_an_py39.py hl[1,12:24] *}
|
||||
|
||||
Це було б схоже на:
|
||||
|
||||
```Python
|
||||
if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"):
|
||||
# Return some error
|
||||
...
|
||||
```
|
||||
|
||||
Але завдяки використанню `secrets.compare_digest()` це буде захищено від типу атак, що називаються «timing attacks».
|
||||
|
||||
### Timing Attacks { #timing-attacks }
|
||||
|
||||
Але що таке «timing attack»?
|
||||
|
||||
Уявімо, що зловмисники намагаються вгадати ім’я користувача та пароль.
|
||||
|
||||
І вони надсилають запит з іменем користувача `johndoe` і паролем `love123`.
|
||||
|
||||
Тоді Python-код у вашому застосунку буде еквівалентний приблизно такому:
|
||||
|
||||
```Python
|
||||
if "johndoe" == "stanleyjobson" and "love123" == "swordfish":
|
||||
...
|
||||
```
|
||||
|
||||
Але в момент, коли Python порівнює першу літеру `j` у `johndoe` з першою літерою `s` у `stanleyjobson`, він одразу поверне `False`, бо вже знає, що ці два рядки не однакові, і вважає, що «немає потреби витрачати додаткові обчислення на порівняння решти літер». І ваш застосунок скаже: «Incorrect username or password».
|
||||
|
||||
Потім зловмисники пробують ім’я користувача `stanleyjobsox` і пароль `love123`.
|
||||
|
||||
І код вашого застосунку робить щось на кшталт:
|
||||
|
||||
```Python
|
||||
if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish":
|
||||
...
|
||||
```
|
||||
|
||||
Python доведеться порівняти весь фрагмент `stanleyjobso` в `stanleyjobsox` і `stanleyjobson`, перш ніж зрозуміти, що рядки відрізняються. Тож відповідь «Incorrect username or password» займе на кілька мікросекунд більше.
|
||||
|
||||
#### Час відповіді допомагає зловмисникам { #the-time-to-answer-helps-the-attackers }
|
||||
|
||||
У цей момент, помітивши, що сервер витратив на кілька мікросекунд більше на надсилання відповіді «Incorrect username or password», зловмисники зрозуміють, що вони _в чомусь_ праві — деякі початкові літери збіглися.
|
||||
|
||||
І тоді вони можуть повторити спробу, знаючи, що правильне значення, ймовірно, ближче до `stanleyjobsox`, ніж до `johndoe`.
|
||||
|
||||
#### «Професійна» атака { #a-professional-attack }
|
||||
|
||||
Звісно, зловмисники не робитимуть це вручну — вони напишуть програму, можливо, з тисячами або мільйонами перевірок за секунду. І щоразу отримуватимуть лише одну додаткову правильну літеру.
|
||||
|
||||
Але так, за кілька хвилин або годин, зловмисники вгадають правильні ім’я користувача та пароль за «допомогою» нашого застосунку, використовуючи лише час, потрібний для відповіді.
|
||||
|
||||
#### Виправлення за допомогою `secrets.compare_digest()` { #fix-it-with-secrets-compare-digest }
|
||||
|
||||
Але в нашому коді ми фактично використовуємо `secrets.compare_digest()`.
|
||||
|
||||
Коротко кажучи, він витрачатиме однаковий час на порівняння `stanleyjobsox` із `stanleyjobson`, як і на порівняння `johndoe` із `stanleyjobson`. І так само для пароля.
|
||||
|
||||
Таким чином, використовуючи `secrets.compare_digest()` у коді вашого застосунку, ви будете захищені від усього цього класу атак безпеки.
|
||||
|
||||
### Повернення помилки { #return-the-error }
|
||||
|
||||
Після виявлення, що облікові дані некоректні, поверніть `HTTPException` зі статус-кодом 401 (таким самим, як і коли облікові дані не надано) і додайте заголовок `WWW-Authenticate`, щоб браузер знову показав запит на вхід:
|
||||
|
||||
{* ../../docs_src/security/tutorial007_an_py39.py hl[26:30] *}
|
||||
19
docs/uk/docs/advanced/security/index.md
Normal file
19
docs/uk/docs/advanced/security/index.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Розширена безпека { #advanced-security }
|
||||
|
||||
## Додаткові можливості { #additional-features }
|
||||
|
||||
Є кілька додаткових можливостей для обробки безпеки, окрім тих, що розглянуті в розділі [Підручник — Посібник користувача: Безпека](../../tutorial/security/index.md){.internal-link target=_blank}.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Наступні розділи **не обов’язково є «розширеними»**.
|
||||
|
||||
І можливо, для вашого випадку використання рішення є в одному з них.
|
||||
|
||||
///
|
||||
|
||||
## Спочатку прочитайте підручник { #read-the-tutorial-first }
|
||||
|
||||
Наступні розділи припускають, що ви вже прочитали основний розділ [Підручник — Посібник користувача: Безпека](../../tutorial/security/index.md){.internal-link target=_blank}.
|
||||
|
||||
Усі вони ґрунтуються на тих самих концепціях, але дають змогу використовувати додаткові можливості.
|
||||
274
docs/uk/docs/advanced/security/oauth2-scopes.md
Normal file
274
docs/uk/docs/advanced/security/oauth2-scopes.md
Normal file
@@ -0,0 +1,274 @@
|
||||
# Області OAuth2 { #oauth2-scopes }
|
||||
|
||||
Ви можете використовувати області OAuth2 безпосередньо з **FastAPI** — вони інтегровані так, щоб працювати безшовно.
|
||||
|
||||
Це дає змогу мати більш деталізовану систему дозволів, дотримуючись стандарту OAuth2, інтегровану у ваш застосунок OpenAPI (і документацію API).
|
||||
|
||||
OAuth2 з областями — це механізм, який використовують багато великих провайдерів автентифікації, як-от Facebook, Google, GitHub, Microsoft, X (Twitter) тощо. Вони застосовують його, щоб надавати користувачам і застосункам конкретні дозволи.
|
||||
|
||||
Щоразу, коли ви «входите через» Facebook, Google, GitHub, Microsoft, X (Twitter), цей застосунок використовує OAuth2 з областями.
|
||||
|
||||
У цьому розділі ви побачите, як керувати автентифікацією та авторизацією за допомогою того самого OAuth2 з областями у вашому застосунку **FastAPI**.
|
||||
|
||||
/// warning | Попередження
|
||||
|
||||
Це більш-менш просунутий розділ. Якщо ви лише починаєте, можете його пропустити.
|
||||
|
||||
Вам не обов’язково потрібні області OAuth2, і ви можете організувати автентифікацію та авторизацію так, як вам потрібно.
|
||||
|
||||
Але OAuth2 з областями можна гарно інтегрувати у ваш API (з OpenAPI) і документацію API.
|
||||
|
||||
Водночас ці області, або будь-які інші вимоги безпеки/авторизації, ви все одно застосовуєте так, як потрібно, у вашому коді.
|
||||
|
||||
У багатьох випадках OAuth2 з областями може бути надлишковим.
|
||||
|
||||
Але якщо ви знаєте, що вам це потрібно, або вам цікаво — читайте далі.
|
||||
|
||||
///
|
||||
|
||||
## Області OAuth2 та OpenAPI { #oauth2-scopes-and-openapi }
|
||||
|
||||
Специфікація OAuth2 визначає «scopes» як список рядків, розділених пробілами.
|
||||
|
||||
Вміст кожного з цих рядків може мати будь-який формат, але не повинен містити пробілів.
|
||||
|
||||
Ці області представляють «permissions» (дозволи).
|
||||
|
||||
В OpenAPI (наприклад, у документації API) ви можете визначати «security schemes».
|
||||
|
||||
Коли одна з таких схем безпеки використовує OAuth2, ви також можете оголошувати та використовувати області.
|
||||
|
||||
Кожна «scope» — це просто рядок (без пробілів).
|
||||
|
||||
Зазвичай їх використовують, щоб оголосити конкретні дозволи безпеки, наприклад:
|
||||
|
||||
* `users:read` або `users:write` — поширені приклади.
|
||||
* `instagram_basic` — використовує Facebook / Instagram.
|
||||
* `https://www.googleapis.com/auth/drive` — використовує Google.
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
В OAuth2 «scope» — це просто рядок, що оголошує конкретний необхідний дозвіл.
|
||||
|
||||
Не має значення, чи містить він інші символи, як-от `:` або чи є він URL.
|
||||
|
||||
Ці деталі залежать від реалізації.
|
||||
|
||||
Для OAuth2 це просто рядки.
|
||||
|
||||
///
|
||||
|
||||
## Загальний огляд { #global-view }
|
||||
|
||||
Спочатку швидко подивімося на частини, які змінюються порівняно з прикладами в основному **Підручнику — Посібнику користувача** для [OAuth2 з паролем (і хешуванням), Bearer з JWT токенами](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. Тепер із використанням областей OAuth2:
|
||||
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[5,9,13,47,65,106,108:116,122:126,130:136,141,157] *}
|
||||
|
||||
Тепер розгляньмо ці зміни крок за кроком.
|
||||
|
||||
## Схема безпеки OAuth2 { #oauth2-security-scheme }
|
||||
|
||||
Перша зміна — тепер ми оголошуємо схему безпеки OAuth2 з двома доступними областями: `me` і `items`.
|
||||
|
||||
Параметр `scopes` отримує `dict`, де кожна область є ключем, а опис — значенням:
|
||||
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[63:66] *}
|
||||
|
||||
Оскільки ми тепер оголошуємо ці області, вони з’являться в документації API, коли ви виконаєте вхід/авторизацію.
|
||||
|
||||
І ви зможете вибрати, до яких областей хочете надати доступ: `me` і `items`.
|
||||
|
||||
Це той самий механізм, який використовується, коли ви надаєте дозволи під час входу через Facebook, Google, GitHub тощо:
|
||||
|
||||
<img src="/img/tutorial/security/image11.png">
|
||||
|
||||
## JWT токен з областями { #jwt-token-with-scopes }
|
||||
|
||||
Тепер змініть *операцію шляху* токена так, щоб вона повертала запитані області.
|
||||
|
||||
Ми й далі використовуємо той самий `OAuth2PasswordRequestForm`. Він має властивість `scopes` з `list` із `str`, з кожною областю, яку він отримав у запиті.
|
||||
|
||||
І ми повертаємо області як частину JWT токена.
|
||||
|
||||
/// danger | Обережно
|
||||
|
||||
Для простоти тут ми просто додаємо області, отримані з запиту, безпосередньо до токена.
|
||||
|
||||
Але у вашому застосунку, з міркувань безпеки, слід переконатися, що ви додаєте лише ті області, які користувач справді може мати, або ті, які ви попередньо визначили.
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[157] *}
|
||||
|
||||
## Оголошення областей в *операціях шляху* та залежностях { #declare-scopes-in-path-operations-and-dependencies }
|
||||
|
||||
Тепер ми оголошуємо, що *операція шляху* для `/users/me/items/` потребує область `items`.
|
||||
|
||||
Для цього ми імпортуємо та використовуємо `Security` з `fastapi`.
|
||||
|
||||
Ви можете використовувати `Security`, щоб оголошувати залежності (так само, як `Depends`), але `Security` також отримує параметр `scopes` зі списком областей (рядків).
|
||||
|
||||
У цьому випадку ми передаємо функцію залежності `get_current_active_user` до `Security` (так само, як зробили б із `Depends`).
|
||||
|
||||
Але також передаємо `list` областей — у цьому випадку лише одну: `items` (може бути й більше).
|
||||
|
||||
І функція залежності `get_current_active_user` також може оголошувати підзалежності — не лише через `Depends`, а й через `Security`. Оголошуючи власну функцію підзалежності (`get_current_user`) та додаткові вимоги до областей.
|
||||
|
||||
У цьому випадку потрібна область `me` (може вимагатися більше ніж одна область).
|
||||
|
||||
/// note | Примітка
|
||||
|
||||
Вам не обов’язково додавати різні області в різних місцях.
|
||||
|
||||
Ми робимо це тут, щоб показати, як **FastAPI** обробляє області, оголошені на різних рівнях.
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[5,141,172] *}
|
||||
|
||||
/// info | Технічні деталі
|
||||
|
||||
`Security` насправді є підкласом `Depends` і має лише один додатковий параметр, який ми побачимо пізніше.
|
||||
|
||||
Але використовуючи `Security` замість `Depends`, **FastAPI** знатиме, що може оголошувати області безпеки, використовувати їх внутрішньо та документувати API через OpenAPI.
|
||||
|
||||
Але коли ви імпортуєте `Query`, `Path`, `Depends`, `Security` та інші з `fastapi`, то це фактично функції, які повертають спеціальні класи.
|
||||
|
||||
///
|
||||
|
||||
## Використання `SecurityScopes` { #use-securityscopes }
|
||||
|
||||
Тепер оновіть залежність `get_current_user`.
|
||||
|
||||
Саме її використовують залежності вище.
|
||||
|
||||
Тут ми використовуємо ту саму схему OAuth2, яку створили раніше, оголошуючи її як залежність: `oauth2_scheme`.
|
||||
|
||||
Оскільки ця функція залежності сама по собі не має вимог до областей, ми можемо використати `Depends` з `oauth2_scheme` — не потрібно використовувати `Security`, коли не треба вказувати області безпеки.
|
||||
|
||||
Ми також оголошуємо спеціальний параметр типу `SecurityScopes`, імпортований з `fastapi.security`.
|
||||
|
||||
Клас `SecurityScopes` схожий на `Request` (`Request` використовувався для отримання об’єкта запиту напряму).
|
||||
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[9,106] *}
|
||||
|
||||
## Використання `scopes` { #use-the-scopes }
|
||||
|
||||
Параметр `security_scopes` матиме тип `SecurityScopes`.
|
||||
|
||||
Він матиме властивість `scopes` зі списком, який містить усі області, потрібні для нього самого та для всіх залежностей, що використовують його як підзалежність. Тобто для всіх «dependants»... це може звучати заплутано — нижче це ще раз пояснюється.
|
||||
|
||||
Об’єкт `security_scopes` (класу `SecurityScopes`) також надає атрибут `scope_str` з одним рядком, що містить ці області, розділені пробілами (ми будемо це використовувати).
|
||||
|
||||
Ми створюємо `HTTPException`, який можемо повторно використовувати (`raise`) у кількох місцях.
|
||||
|
||||
У цьому винятку ми включаємо потрібні області (якщо є) у вигляді рядка, розділеного пробілами (використовуючи `scope_str`). Цей рядок з областями ми поміщаємо в заголовок `WWW-Authenticate` (це частина специфікації).
|
||||
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[106,108:116] *}
|
||||
|
||||
## Перевірка `username` і структури даних { #verify-the-username-and-data-shape }
|
||||
|
||||
Ми перевіряємо, що отримали `username`, і витягаємо області.
|
||||
|
||||
Далі валідуюємо ці дані за допомогою Pydantic-моделі (перехоплюючи виняток `ValidationError`), і якщо під час читання JWT токена або перевірки даних через Pydantic виникає помилка — піднімаємо `HTTPException`, який створили раніше.
|
||||
|
||||
Для цього ми оновлюємо Pydantic-модель `TokenData`, додаючи нову властивість `scopes`.
|
||||
|
||||
Валідуючи дані через Pydantic, ми можемо переконатися, що маємо, наприклад, рівно `list` із `str` для областей та `str` для `username`.
|
||||
|
||||
А не, наприклад, `dict` чи щось інше, що могло б зламати застосунок пізніше і створити ризик безпеки.
|
||||
|
||||
Ми також перевіряємо, що існує користувач із таким `username`, і якщо ні — піднімаємо той самий виняток, який створили раніше.
|
||||
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[47,117:129] *}
|
||||
|
||||
## Перевірка `scopes` { #verify-the-scopes }
|
||||
|
||||
Тепер ми перевіряємо, що всі області, потрібні для цієї залежності та всіх «dependants» (включно з *операціями шляху*), присутні в областях, наданих у отриманому токені. Інакше піднімаємо `HTTPException`.
|
||||
|
||||
Для цього використовуємо `security_scopes.scopes`, що містить `list` усіх цих областей як `str`.
|
||||
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[130:136] *}
|
||||
|
||||
## Дерево залежностей та області { #dependency-tree-and-scopes }
|
||||
|
||||
Розгляньмо ще раз це дерево залежностей та області.
|
||||
|
||||
Оскільки залежність `get_current_active_user` має підзалежність `get_current_user`, область `"me"`, оголошена в `get_current_active_user`, буде включена до списку потрібних областей у `security_scopes.scopes`, переданому в `get_current_user`.
|
||||
|
||||
Сама *операція шляху* також оголошує область `"items"`, тож вона теж буде в списку `security_scopes.scopes`, переданому в `get_current_user`.
|
||||
|
||||
Ось як виглядає ієрархія залежностей та областей:
|
||||
|
||||
* *Операція шляху* `read_own_items` має:
|
||||
* Потрібні області `["items"]` із залежністю:
|
||||
* `get_current_active_user`:
|
||||
* Функція залежності `get_current_active_user` має:
|
||||
* Потрібні області `["me"]` із залежністю:
|
||||
* `get_current_user`:
|
||||
* Функція залежності `get_current_user` має:
|
||||
* Немає областей, потрібних для неї самої.
|
||||
* Залежність, що використовує `oauth2_scheme`.
|
||||
* Параметр `security_scopes` типу `SecurityScopes`:
|
||||
* Цей параметр `security_scopes` має властивість `scopes` із `list`, який містить усі оголошені вище області, отже:
|
||||
* `security_scopes.scopes` міститиме `["me", "items"]` для *операції шляху* `read_own_items`.
|
||||
* `security_scopes.scopes` міститиме `["me"]` для *операції шляху* `read_users_me`, бо вона оголошена в залежності `get_current_active_user`.
|
||||
* `security_scopes.scopes` міститиме `[]` (нічого) для *операції шляху* `read_system_status`, бо вона не оголосила жодного `Security` зі `scopes`, і її залежність `get_current_user` також не оголошує жодних `scopes`.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Важлива й «магічна» річ тут у тому, що `get_current_user` матиме різний список `scopes` для перевірки для кожної *операції шляху*.
|
||||
|
||||
Усе залежить від `scopes`, оголошених у кожній *операції шляху* та в кожній залежності в дереві залежностей для цієї конкретної *операції шляху*.
|
||||
|
||||
///
|
||||
|
||||
## Докладніше про `SecurityScopes` { #more-details-about-securityscopes }
|
||||
|
||||
Ви можете використовувати `SecurityScopes` у будь-якій точці й у кількох місцях — він не обов’язково має бути в «кореневій» залежності.
|
||||
|
||||
Він завжди міститиме області безпеки, оголошені в поточних залежностях `Security`, та у всіх «dependants» для **саме цієї** *операції шляху* і **саме цього** дерева залежностей.
|
||||
|
||||
Оскільки `SecurityScopes` міститиме всі області, оголошені «dependants», ви можете використати його, щоб перевіряти наявність потрібних областей у токені в центральній функції залежності, а потім оголошувати різні вимоги до областей у різних *операціях шляху*.
|
||||
|
||||
Вони перевірятимуться незалежно для кожної *операції шляху*.
|
||||
|
||||
## Перевірте { #check-it }
|
||||
|
||||
Якщо ви відкриєте документацію API, ви зможете автентифікуватися та вказати, які області ви хочете авторизувати.
|
||||
|
||||
<img src="/img/tutorial/security/image11.png">
|
||||
|
||||
Якщо ви не виберете жодної області, ви будете «автентифіковані», але коли спробуєте отримати доступ до `/users/me/` або `/users/me/items/`, отримаєте помилку про недостатні дозволи. Водночас ви все ще зможете отримати доступ до `/status/`.
|
||||
|
||||
А якщо ви виберете область `me`, але не область `items`, ви зможете отримати доступ до `/users/me/`, але не до `/users/me/items/`.
|
||||
|
||||
Саме так поводитиметься сторонній застосунок, який спробував би отримати доступ до однієї з цих *операцій шляху* з токеном, наданим користувачем, залежно від того, скільки дозволів користувач надав цьому застосунку.
|
||||
|
||||
## Про інтеграції зі сторонніми застосунками { #about-third-party-integrations }
|
||||
|
||||
У цьому прикладі ми використовуємо OAuth2 «password» flow.
|
||||
|
||||
Це доречно, коли ми входимо у власний застосунок — імовірно, через власний frontend.
|
||||
|
||||
Тому що ми можемо довіряти йому отримання `username` та `password`, адже ми його контролюємо.
|
||||
|
||||
Але якщо ви створюєте OAuth2-застосунок, до якого підключатимуться інші (тобто якщо ви створюєте провайдера автентифікації на кшталт Facebook, Google, GitHub тощо), вам слід використати один з інших flow.
|
||||
|
||||
Найпоширеніший — implicit flow.
|
||||
|
||||
Найбезпечніший — code flow, але його складніше реалізувати, бо він вимагає більше кроків. Через цю складність багато провайдерів у підсумку рекомендують implicit flow.
|
||||
|
||||
/// note | Примітка
|
||||
|
||||
Часто кожен провайдер автентифікації називає свої flow по-різному, роблячи це частиною бренду.
|
||||
|
||||
Але зрештою вони реалізують той самий стандарт OAuth2.
|
||||
|
||||
///
|
||||
|
||||
**FastAPI** містить утиліти для всіх цих OAuth2 flow автентифікації в `fastapi.security.oauth2`.
|
||||
|
||||
## `Security` у параметрі декоратора `dependencies` { #security-in-decorator-dependencies }
|
||||
|
||||
Так само, як ви можете визначити `list` із `Depends` у параметрі декоратора `dependencies` (як пояснено в [Залежності в декораторах операцій шляху](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), ви також можете використати там `Security` зі `scopes`.
|
||||
302
docs/uk/docs/advanced/settings.md
Normal file
302
docs/uk/docs/advanced/settings.md
Normal file
@@ -0,0 +1,302 @@
|
||||
# Налаштування та змінні середовища { #settings-and-environment-variables }
|
||||
|
||||
У багатьох випадках вашому застосунку можуть знадобитися зовнішні налаштування або конфігурації, наприклад секретні ключі, облікові дані бази даних, облікові дані для email-сервісів тощо.
|
||||
|
||||
Більшість із цих налаштувань є змінними (можуть змінюватися), як-от URL бази даних. І багато з них можуть бути чутливими, як-от секрети.
|
||||
|
||||
Тому зазвичай їх передають через змінні середовища, які застосунок зчитує.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Щоб зрозуміти змінні середовища, можете прочитати [Змінні середовища](../environment-variables.md){.internal-link target=_blank}.
|
||||
|
||||
///
|
||||
|
||||
## Типи та валідація { #types-and-validation }
|
||||
|
||||
Ці змінні середовища можуть містити лише текстові рядки, адже вони зовнішні для Python і мають бути сумісними з іншими програмами та рештою системи (і навіть із різними операційними системами, як-от Linux, Windows, macOS).
|
||||
|
||||
Це означає, що будь-яке значення, зчитане в Python зі змінної середовища, буде `str`, а будь-яке перетворення на інший тип або будь-яку валідацію потрібно виконувати в коді.
|
||||
|
||||
## Pydantic `Settings` { #pydantic-settings }
|
||||
|
||||
На щастя, Pydantic надає чудову утиліту для обробки цих налаштувань, що надходять зі змінних середовища, за допомогою <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/" class="external-link" target="_blank">Pydantic: Settings management</a>.
|
||||
|
||||
### Встановіть `pydantic-settings` { #install-pydantic-settings }
|
||||
|
||||
Спочатку переконайтеся, що ви створили своє [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановіть пакет `pydantic-settings`:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ pip install pydantic-settings
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Він також встановлюється, коли ви встановлюєте extra `all` командою:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ pip install "fastapi[all]"
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
### Створіть об’єкт `Settings` { #create-the-settings-object }
|
||||
|
||||
Імпортуйте `BaseSettings` із Pydantic і створіть підклас, майже так само, як із моделлю Pydantic.
|
||||
|
||||
Так само, як і з моделями Pydantic, ви оголошуєте атрибути класу з анотаціями типів і, за потреби, зі значеннями за замовчуванням.
|
||||
|
||||
Ви можете використовувати всі ті самі можливості та інструменти валідації, що й для моделей Pydantic, як-от різні типи даних і додаткові перевірки через `Field()`.
|
||||
|
||||
{* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *}
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Якщо вам потрібно щось швидко скопіювати й вставити, не використовуйте цей приклад — використайте останній нижче.
|
||||
|
||||
///
|
||||
|
||||
Далі, коли ви створите екземпляр цього класу `Settings` (у цьому випадку — в об’єкті `settings`), Pydantic зчитає змінні середовища без урахування регістру, тож змінна у верхньому регістрі `APP_NAME` все одно буде прочитана для атрибута `app_name`.
|
||||
|
||||
Потім він перетворить і провалідовує дані. Отже, коли ви використовуєте об’єкт `settings`, ви матимете дані тих типів, які оголосили (наприклад, `items_per_user` буде `int`).
|
||||
|
||||
### Використовуйте `settings` { #use-the-settings }
|
||||
|
||||
Далі ви можете використати новий об’єкт `settings` у вашому застосунку:
|
||||
|
||||
{* ../../docs_src/settings/tutorial001_py39.py hl[18:20] *}
|
||||
|
||||
### Запустіть сервер { #run-the-server }
|
||||
|
||||
Потім ви запускатимете сервер, передаючи конфігурації як змінні середовища. Наприклад, ви можете встановити `ADMIN_EMAIL` і `APP_NAME` так:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.py
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Щоб встановити кілька env vars для однієї команди, просто розділіть їх пробілом і розмістіть усі перед командою.
|
||||
|
||||
///
|
||||
|
||||
Після цього налаштування `admin_email` буде встановлено в `"deadpool@example.com"`.
|
||||
|
||||
`app_name` буде `"ChimichangApp"`.
|
||||
|
||||
А `items_per_user` збереже своє значення за замовчуванням `50`.
|
||||
|
||||
## Налаштування в іншому модулі { #settings-in-another-module }
|
||||
|
||||
Ви можете винести ці налаштування в інший файл модуля, як ви бачили в розділі [Більші застосунки — кілька файлів](../tutorial/bigger-applications.md){.internal-link target=_blank}.
|
||||
|
||||
Наприклад, у вас може бути файл `config.py` з таким вмістом:
|
||||
|
||||
{* ../../docs_src/settings/app01_py39/config.py *}
|
||||
|
||||
А потім використати його у файлі `main.py`:
|
||||
|
||||
{* ../../docs_src/settings/app01_py39/main.py hl[3,11:13] *}
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Вам також знадобиться файл `__init__.py`, як ви бачили в розділі [Більші застосунки — кілька файлів](../tutorial/bigger-applications.md){.internal-link target=_blank}.
|
||||
|
||||
///
|
||||
|
||||
## Налаштування в залежності { #settings-in-a-dependency }
|
||||
|
||||
У деяких випадках може бути корисно надавати налаштування через залежність, замість того щоб мати глобальний об’єкт `settings`, який використовується всюди.
|
||||
|
||||
Це може бути особливо корисно під час тестування, адже дуже легко перевизначити залежність власними кастомними налаштуваннями.
|
||||
|
||||
### Файл конфігурації { #the-config-file }
|
||||
|
||||
На основі попереднього прикладу, ваш файл `config.py` може виглядати так:
|
||||
|
||||
{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *}
|
||||
|
||||
Зверніть увагу, що тепер ми не створюємо екземпляр за замовчуванням `settings = Settings()`.
|
||||
|
||||
### Основний файл застосунку { #the-main-app-file }
|
||||
|
||||
Тепер ми створюємо залежність, яка повертає новий `config.Settings()`.
|
||||
|
||||
{* ../../docs_src/settings/app02_an_py39/main.py hl[6,12:13] *}
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Трохи згодом ми обговоримо `@lru_cache`.
|
||||
|
||||
Поки що можете вважати `get_settings()` звичайною функцією.
|
||||
|
||||
///
|
||||
|
||||
Потім ми можемо вимагати її як залежність у *функції операції шляху* і використовувати всюди, де вона потрібна.
|
||||
|
||||
{* ../../docs_src/settings/app02_an_py39/main.py hl[17,19:21] *}
|
||||
|
||||
### Налаштування та тестування { #settings-and-testing }
|
||||
|
||||
Тоді під час тестування буде дуже легко надати інший об’єкт налаштувань, створивши перевизначення залежності для `get_settings`:
|
||||
|
||||
{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *}
|
||||
|
||||
У перевизначенні залежності ми задаємо нове значення для `admin_email` під час створення нового об’єкта `Settings`, а потім повертаємо цей новий об’єкт.
|
||||
|
||||
Потім ми можемо перевірити, що використовується саме він.
|
||||
|
||||
## Читання файлу `.env` { #reading-a-env-file }
|
||||
|
||||
Якщо у вас багато налаштувань, які, ймовірно, часто змінюються, можливо в різних середовищах, може бути корисно зберігати їх у файлі, а потім зчитувати з нього так, ніби це змінні середовища.
|
||||
|
||||
Ця практика настільки поширена, що має назву: ці змінні середовища зазвичай розміщують у файлі `.env`, а файл називають «dotenv».
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Файл, назва якого починається з крапки (`.`), є прихованим у Unix-подібних системах, як-от Linux і macOS.
|
||||
|
||||
Але файл dotenv не обов’язково має мати саме таку назву.
|
||||
|
||||
///
|
||||
|
||||
Pydantic підтримує читання таких файлів за допомогою зовнішньої бібліотеки. Докладніше дивіться в <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/#dotenv-env-support" class="external-link" target="_blank">Pydantic Settings: Dotenv (.env) support</a>.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Щоб це працювало, вам потрібно виконати `pip install python-dotenv`.
|
||||
|
||||
///
|
||||
|
||||
### Файл `.env` { #the-env-file }
|
||||
|
||||
Ви можете мати файл `.env` з таким вмістом:
|
||||
|
||||
```bash
|
||||
ADMIN_EMAIL="deadpool@example.com"
|
||||
APP_NAME="ChimichangApp"
|
||||
```
|
||||
|
||||
### Читайте налаштування з `.env` { #read-settings-from-env }
|
||||
|
||||
А потім оновіть ваш `config.py` так:
|
||||
|
||||
{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *}
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Атрибут `model_config` використовується лише для конфігурації Pydantic. Докладніше дивіться в <a href="https://docs.pydantic.dev/latest/concepts/config/" class="external-link" target="_blank">Pydantic: Concepts: Configuration</a>.
|
||||
|
||||
///
|
||||
|
||||
Тут ми визначаємо `env_file` всередині вашого класу `Settings` у Pydantic і встановлюємо значення як ім’я файлу з dotenv, який ми хочемо використати.
|
||||
|
||||
### Створюйте `Settings` лише один раз за допомогою `lru_cache` { #creating-the-settings-only-once-with-lru-cache }
|
||||
|
||||
Читання файлу з диска зазвичай є затратною (повільною) операцією, тож, імовірно, ви захочете робити це лише один раз і потім повторно використовувати той самий об’єкт налаштувань, замість читання для кожного запиту.
|
||||
|
||||
Але щоразу, коли ми робимо:
|
||||
|
||||
```Python
|
||||
Settings()
|
||||
```
|
||||
|
||||
буде створюватися новий об’єкт `Settings`, і під час створення він знову читатиме файл `.env`.
|
||||
|
||||
Якби функція-залежність виглядала так:
|
||||
|
||||
```Python
|
||||
def get_settings():
|
||||
return Settings()
|
||||
```
|
||||
|
||||
ми створювали б цей об’єкт для кожного запиту й читали б файл `.env` для кожного запиту. ⚠️
|
||||
|
||||
Але оскільки зверху ми використовуємо декоратор `@lru_cache`, об’єкт `Settings` буде створений лише один раз — під час першого виклику. ✔️
|
||||
|
||||
{* ../../docs_src/settings/app03_an_py39/main.py hl[1,11] *}
|
||||
|
||||
Потім, для будь-якого наступного виклику `get_settings()` у залежностях для наступних запитів, замість виконання внутрішнього коду `get_settings()` і створення нового об’єкта `Settings`, буде повертатися той самий об’єкт, що був повернутий під час першого виклику, знову і знову.
|
||||
|
||||
#### Технічні деталі `lru_cache` { #lru-cache-technical-details }
|
||||
|
||||
`@lru_cache` модифікує функцію, яку він декорує, так, щоб вона повертала те саме значення, яке було повернене вперше, замість повторного обчислення й виконання коду функції щоразу.
|
||||
|
||||
Тому функція під ним буде виконана один раз для кожної комбінації аргументів. А потім значення, повернуті для кожної з цих комбінацій аргументів, будуть використовуватися знову і знову щоразу, коли функцію викликають з точно такою самою комбінацією аргументів.
|
||||
|
||||
Наприклад, якщо у вас є функція:
|
||||
|
||||
```Python
|
||||
@lru_cache
|
||||
def say_hi(name: str, salutation: str = "Ms."):
|
||||
return f"Hello {salutation} {name}"
|
||||
```
|
||||
|
||||
ваша програма може виконуватися так:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
|
||||
participant code as Code
|
||||
participant function as say_hi()
|
||||
participant execute as Execute function
|
||||
|
||||
rect rgba(0, 255, 0, .1)
|
||||
code ->> function: say_hi(name="Camila")
|
||||
function ->> execute: execute function code
|
||||
execute ->> code: return the result
|
||||
end
|
||||
|
||||
rect rgba(0, 255, 255, .1)
|
||||
code ->> function: say_hi(name="Camila")
|
||||
function ->> code: return stored result
|
||||
end
|
||||
|
||||
rect rgba(0, 255, 0, .1)
|
||||
code ->> function: say_hi(name="Rick")
|
||||
function ->> execute: execute function code
|
||||
execute ->> code: return the result
|
||||
end
|
||||
|
||||
rect rgba(0, 255, 0, .1)
|
||||
code ->> function: say_hi(name="Rick", salutation="Mr.")
|
||||
function ->> execute: execute function code
|
||||
execute ->> code: return the result
|
||||
end
|
||||
|
||||
rect rgba(0, 255, 255, .1)
|
||||
code ->> function: say_hi(name="Rick")
|
||||
function ->> code: return stored result
|
||||
end
|
||||
|
||||
rect rgba(0, 255, 255, .1)
|
||||
code ->> function: say_hi(name="Camila")
|
||||
function ->> code: return stored result
|
||||
end
|
||||
```
|
||||
|
||||
У випадку нашої залежності `get_settings()`, функція навіть не приймає жодних аргументів, тож вона завжди повертає те саме значення.
|
||||
|
||||
Таким чином вона поводиться майже так, ніби це просто глобальна змінна. Але оскільки використовується функція-залежність, ми можемо легко перевизначати її для тестування.
|
||||
|
||||
`@lru_cache` є частиною `functools`, який є частиною стандартної бібліотеки Python. Можете прочитати більше в <a href="https://docs.python.org/3/library/functools.html#functools.lru_cache" class="external-link" target="_blank">документації Python про `@lru_cache`</a>.
|
||||
|
||||
## Підсумок { #recap }
|
||||
|
||||
Ви можете використовувати Pydantic Settings для керування налаштуваннями або конфігураціями вашого застосунку з усією потужністю моделей Pydantic.
|
||||
|
||||
* Використовуючи залежність, ви можете спростити тестування.
|
||||
* Ви можете використовувати з ним файли `.env`.
|
||||
* Використання `@lru_cache` дозволяє уникнути повторного читання файла dotenv для кожного запиту, водночас даючи змогу перевизначати це під час тестування.
|
||||
67
docs/uk/docs/advanced/sub-applications.md
Normal file
67
docs/uk/docs/advanced/sub-applications.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Підзастосунки — монтування { #sub-applications-mounts }
|
||||
|
||||
Якщо вам потрібно мати два незалежні застосунки FastAPI з власним незалежним OpenAPI та власними UI документації, ви можете мати головний застосунок і «змонтувати» один (або кілька) підзастосунок(ів).
|
||||
|
||||
## Монтування застосунку **FastAPI** { #mounting-a-fastapi-application }
|
||||
|
||||
«Монтування» означає додавання повністю «незалежного» застосунку на певному шляху, який потім відповідатиме за обробку всього під цим шляхом, із _операціями шляху_, оголошеними в цьому підзастосунку.
|
||||
|
||||
### Застосунок верхнього рівня { #top-level-application }
|
||||
|
||||
Спочатку створіть головний, застосунок верхнього рівня **FastAPI** та його *операції шляху*:
|
||||
|
||||
{* ../../docs_src/sub_applications/tutorial001_py39.py hl[3, 6:8] *}
|
||||
|
||||
### Підзастосунок { #sub-application }
|
||||
|
||||
Далі створіть ваш підзастосунок та його *операції шляху*.
|
||||
|
||||
Цей підзастосунок — це просто ще один стандартний застосунок FastAPI, але саме його буде «змонтовано»:
|
||||
|
||||
{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 14:16] *}
|
||||
|
||||
### Змонтуйте підзастосунок { #mount-the-sub-application }
|
||||
|
||||
У вашому застосунку верхнього рівня `app` змонтуйте підзастосунок `subapi`.
|
||||
|
||||
У цьому випадку його буде змонтовано за шляхом `/subapi`:
|
||||
|
||||
{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 19] *}
|
||||
|
||||
### Перевірте автоматичну документацію API { #check-the-automatic-api-docs }
|
||||
|
||||
Тепер запустіть команду `fastapi` з вашим файлом:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi dev main.py
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
І відкрийте документацію за адресою <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||
|
||||
Ви побачите автоматичну документацію API для головного застосунку, яка включає лише його власні _операції шляху_:
|
||||
|
||||
<img src="/img/tutorial/sub-applications/image01.png">
|
||||
|
||||
А потім відкрийте документацію для підзастосунку за адресою <a href="http://127.0.0.1:8000/subapi/docs" class="external-link" target="_blank">http://127.0.0.1:8000/subapi/docs</a>.
|
||||
|
||||
Ви побачите автоматичну документацію API для підзастосунку, яка включає лише його власні _операції шляху_, і все це під правильним префіксом підшляху `/subapi`:
|
||||
|
||||
<img src="/img/tutorial/sub-applications/image02.png">
|
||||
|
||||
Якщо ви спробуєте взаємодіяти з будь-яким із двох інтерфейсів, вони працюватимуть коректно, тому що браузер зможе спілкуватися з кожним конкретним застосунком або підзастосунком.
|
||||
|
||||
### Технічні деталі: `root_path` { #technical-details-root-path }
|
||||
|
||||
Коли ви монтуєте підзастосунок, як описано вище, FastAPI подбає про передавання шляху монтування для підзастосунку, використовуючи механізм зі специфікації ASGI під назвою `root_path`.
|
||||
|
||||
Так підзастосунок знатиме, що потрібно використовувати цей префікс шляху для UI документації.
|
||||
|
||||
А підзастосунок також може мати власні змонтовані підзастосунки, і все працюватиме коректно, тому що FastAPI автоматично обробляє всі ці `root_path`.
|
||||
|
||||
Детальніше про `root_path` і про те, як явно його використовувати, ви дізнаєтеся в розділі [За проксі](behind-a-proxy.md){.internal-link target=_blank}.
|
||||
126
docs/uk/docs/advanced/templates.md
Normal file
126
docs/uk/docs/advanced/templates.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Шаблони { #templates }
|
||||
|
||||
Ви можете використовувати з **FastAPI** будь-який рушій шаблонів.
|
||||
|
||||
Поширений вибір — Jinja2, той самий, що використовується у Flask та інших інструментах.
|
||||
|
||||
Є утиліти для простого налаштування, які ви можете напряму використовувати у своєму застосунку **FastAPI** (надаються Starlette).
|
||||
|
||||
## Встановіть залежності { #install-dependencies }
|
||||
|
||||
Переконайтеся, що ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його та встановили `jinja2`:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ pip install jinja2
|
||||
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Використання `Jinja2Templates` { #using-jinja2templates }
|
||||
|
||||
* Імпортуйте `Jinja2Templates`.
|
||||
* Створіть об’єкт `templates`, який зможете повторно використовувати пізніше.
|
||||
* Оголосіть параметр `Request` в *операції шляху*, яка повертатиме шаблон.
|
||||
* Використайте створений `templates`, щоб відрендерити та повернути `TemplateResponse`, передайте назву шаблону, об’єкт запиту та словник «context» (контекст) з парами ключ-значення, які будуть доступні всередині шаблону Jinja2.
|
||||
|
||||
{* ../../docs_src/templates/tutorial001_py39.py hl[4,11,15:18] *}
|
||||
|
||||
/// note | Примітка
|
||||
|
||||
До FastAPI 0.108.0 та Starlette 0.29.0 параметр `name` був першим параметром.
|
||||
|
||||
Також до цього, у попередніх версіях, об’єкт `request` передавався як частина пар ключ-значення у контексті для Jinja2.
|
||||
|
||||
///
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Оголосивши `response_class=HTMLResponse`, інтерфейс документації зможе визначити, що відповідь буде HTML.
|
||||
|
||||
///
|
||||
|
||||
/// note | Технічні деталі
|
||||
|
||||
Ви також можете використати `from starlette.templating import Jinja2Templates`.
|
||||
|
||||
**FastAPI** надає той самий `starlette.templating` як `fastapi.templating` просто для зручності для вас, розробника. Але більшість доступних відповідей надходять безпосередньо зі Starlette. Те саме стосується `Request` і `StaticFiles`.
|
||||
|
||||
///
|
||||
|
||||
## Написання шаблонів { #writing-templates }
|
||||
|
||||
Далі ви можете написати шаблон у `templates/item.html`, наприклад:
|
||||
|
||||
```jinja hl_lines="7"
|
||||
{!../../docs_src/templates/templates/item.html!}
|
||||
```
|
||||
|
||||
### Значення контексту шаблону { #template-context-values }
|
||||
|
||||
У HTML, що містить:
|
||||
|
||||
{% raw %}
|
||||
|
||||
```jinja
|
||||
Item ID: {{ id }}
|
||||
```
|
||||
|
||||
{% endraw %}
|
||||
|
||||
...буде показано `id`, взятий зі словника «context» `dict`, який ви передали:
|
||||
|
||||
```Python
|
||||
{"id": id}
|
||||
```
|
||||
|
||||
Наприклад, з ID `42` це відрендериться як:
|
||||
|
||||
```html
|
||||
Item ID: 42
|
||||
```
|
||||
|
||||
### Аргументи `url_for` у шаблоні { #template-url-for-arguments }
|
||||
|
||||
Ви також можете використовувати `url_for()` всередині шаблону; як аргументи він приймає ті самі аргументи, які використовувалися б вашою *функцією операції шляху*.
|
||||
|
||||
Отже, фрагмент із:
|
||||
|
||||
{% raw %}
|
||||
|
||||
```jinja
|
||||
<a href="{{ url_for('read_item', id=id) }}">
|
||||
```
|
||||
|
||||
{% endraw %}
|
||||
|
||||
...згенерує посилання на ту саму URL-адресу, яку обробляла б *функція операції шляху* `read_item(id=id)`.
|
||||
|
||||
Наприклад, з ID `42` це відрендериться як:
|
||||
|
||||
```html
|
||||
<a href="/items/42">
|
||||
```
|
||||
|
||||
## Шаблони та статичні файли { #templates-and-static-files }
|
||||
|
||||
Ви також можете використовувати `url_for()` всередині шаблону і застосовувати його, наприклад, зі `StaticFiles`, які ви змонтували з `name="static"`.
|
||||
|
||||
```jinja hl_lines="4"
|
||||
{!../../docs_src/templates/templates/item.html!}
|
||||
```
|
||||
|
||||
У цьому прикладі буде посилання на CSS-файл `static/styles.css` через:
|
||||
|
||||
```CSS hl_lines="4"
|
||||
{!../../docs_src/templates/static/styles.css!}
|
||||
```
|
||||
|
||||
І оскільки ви використовуєте `StaticFiles`, цей CSS-файл автоматично буде віддаватися вашим застосунком **FastAPI** за URL `/static/styles.css`.
|
||||
|
||||
## Докладніше { #more-details }
|
||||
|
||||
Докладніше, зокрема про те, як тестувати шаблони, дивіться в <a href="https://www.starlette.dev/templates/" class="external-link" target="_blank">документації Starlette про шаблони</a>.
|
||||
53
docs/uk/docs/advanced/testing-dependencies.md
Normal file
53
docs/uk/docs/advanced/testing-dependencies.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Тестування залежностей із перевизначеннями { #testing-dependencies-with-overrides }
|
||||
|
||||
## Перевизначення залежностей під час тестування { #overriding-dependencies-during-testing }
|
||||
|
||||
Є кілька сценаріїв, коли під час тестування може знадобитися перевизначити залежність.
|
||||
|
||||
Ви не хочете, щоб виконувалася початкова залежність (а також будь-які її підзалежності).
|
||||
|
||||
Натомість ви хочете надати іншу залежність, яка використовуватиметься лише під час тестів (можливо, лише в деяких конкретних тестах) і повертатиме значення, яке можна використати там, де використовувалося значення початкової залежності.
|
||||
|
||||
### Випадки використання: зовнішній сервіс { #use-cases-external-service }
|
||||
|
||||
Наприклад, у вас може бути зовнішній провайдер автентифікації, якого потрібно викликати.
|
||||
|
||||
Ви надсилаєте йому токен, і він повертає автентифікованого користувача.
|
||||
|
||||
Цей провайдер може брати плату за кожен запит, а виклик може займати більше часу, ніж якби для тестів ви мали фіксованого mock-користувача.
|
||||
|
||||
Імовірно, ви хочете протестувати зовнішнього провайдера один раз, але не обов’язково викликати його для кожного тесту, що виконується.
|
||||
|
||||
У такому разі ви можете перевизначити залежність, яка викликає цього провайдера, і використовувати власну залежність, що повертає mock-користувача, лише для ваших тестів.
|
||||
|
||||
### Використання атрибута `app.dependency_overrides` { #use-the-app-dependency-overrides-attribute }
|
||||
|
||||
Для таких випадків ваш застосунок **FastAPI** має атрибут `app.dependency_overrides` — це простий `dict`.
|
||||
|
||||
Щоб перевизначити залежність для тестування, ви вказуєте як ключ початкову залежність (функцію), а як значення — ваше перевизначення залежності (іншу функцію).
|
||||
|
||||
Після цього **FastAPI** викликатиме перевизначення замість початкової залежності.
|
||||
|
||||
{* ../../docs_src/dependency_testing/tutorial001_an_py310.py hl[26:27,30] *}
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Ви можете встановити перевизначення залежності для залежності, що використовується будь-де у вашому застосунку **FastAPI**.
|
||||
|
||||
Початкова залежність може використовуватися у *функції операції шляху*, у *декораторі операції шляху* (коли ви не використовуєте повернене значення), у виклику `.include_router()` тощо.
|
||||
|
||||
FastAPI все одно зможе її перевизначити.
|
||||
|
||||
///
|
||||
|
||||
Потім ви можете скинути перевизначення (прибрати їх), встановивши `app.dependency_overrides` як порожній `dict`:
|
||||
|
||||
```Python
|
||||
app.dependency_overrides = {}
|
||||
```
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Якщо ви хочете перевизначувати залежність лише під час деяких тестів, ви можете встановити перевизначення на початку тесту (всередині функції тесту) і скинути його в кінці (наприкінці функції тесту).
|
||||
|
||||
///
|
||||
12
docs/uk/docs/advanced/testing-events.md
Normal file
12
docs/uk/docs/advanced/testing-events.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Тестування подій: `lifespan` і `startup` - `shutdown` { #testing-events-lifespan-and-startup-shutdown }
|
||||
|
||||
Коли вам потрібно, щоб `lifespan` виконувався у ваших тестах, ви можете використати `TestClient` з оператором `with`:
|
||||
|
||||
{* ../../docs_src/app_testing/tutorial004_py39.py hl[9:15,18,27:28,30:32,41:43] *}
|
||||
|
||||
|
||||
Ви можете прочитати більше деталей про [«Running lifespan in tests in the official Starlette documentation site.»](https://www.starlette.dev/lifespan/#running-lifespan-in-tests)
|
||||
|
||||
Для застарілих подій `startup` і `shutdown` ви можете використати `TestClient` так:
|
||||
|
||||
{* ../../docs_src/app_testing/tutorial003_py39.py hl[9:12,20:24] *}
|
||||
13
docs/uk/docs/advanced/testing-websockets.md
Normal file
13
docs/uk/docs/advanced/testing-websockets.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Тестування WebSockets { #testing-websockets }
|
||||
|
||||
Ви можете використовувати той самий `TestClient` для тестування WebSockets.
|
||||
|
||||
Для цього використайте `TestClient` в інструкції `with`, підключившись до WebSocket:
|
||||
|
||||
{* ../../docs_src/app_testing/tutorial002_py39.py hl[27:31] *}
|
||||
|
||||
/// note | Примітка
|
||||
|
||||
Докладніше дивіться документацію Starlette щодо <a href="https://www.starlette.dev/testclient/#testing-websocket-sessions" class="external-link" target="_blank">тестування WebSockets</a>.
|
||||
|
||||
///
|
||||
56
docs/uk/docs/advanced/using-request-directly.md
Normal file
56
docs/uk/docs/advanced/using-request-directly.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Безпосереднє використання Request { #using-the-request-directly }
|
||||
|
||||
До цього моменту ви оголошували частини запиту, які вам потрібні, разом із їхніми типами.
|
||||
|
||||
Отримуючи дані з:
|
||||
|
||||
* шляху як параметрів;
|
||||
* заголовків;
|
||||
* cookies;
|
||||
* тощо.
|
||||
|
||||
І таким чином **FastAPI** автоматично перевіряє ці дані, перетворює їх і генерує документацію для вашого API.
|
||||
|
||||
Але бувають ситуації, коли вам може знадобитися доступ безпосередньо до об’єкта `Request`.
|
||||
|
||||
## Деталі про об’єкт `Request` { #details-about-the-request-object }
|
||||
|
||||
Оскільки в основі **FastAPI** фактично лежить **Starlette**, із шаром кількох інструментів поверх, за потреби ви можете використовувати об’єкт Starlette <a href="https://www.starlette.dev/requests/" class="external-link" target="_blank">`Request`</a> безпосередньо.
|
||||
|
||||
Це також означає, що якщо ви отримуєте дані безпосередньо з об’єкта `Request` (наприклад, читаєте body), FastAPI не буде їх валідовувати, перетворювати або документувати (через OpenAPI, для автоматичного UI API).
|
||||
|
||||
Хоча будь-який інший параметр, оголошений звичним способом (наприклад, body за допомогою Pydantic-моделі), все одно буде валідовано, перетворено, анотовано тощо.
|
||||
|
||||
Але є специфічні випадки, коли корисно отримати об’єкт `Request`.
|
||||
|
||||
## Використовуйте об’єкт `Request` безпосередньо { #use-the-request-object-directly }
|
||||
|
||||
Уявімо, що ви хочете отримати IP-адресу/хост клієнта всередині вашої *функції операції шляху*.
|
||||
|
||||
Для цього потрібно отримати доступ до запиту безпосередньо.
|
||||
|
||||
{* ../../docs_src/using_request_directly/tutorial001_py39.py hl[1,7:8] *}
|
||||
|
||||
Оголосивши параметр *функції операції шляху* з типом `Request`, **FastAPI** зрозуміє, що потрібно передати `Request` у цей параметр.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Зверніть увагу, що в цьому випадку ми оголошуємо параметр шляху поруч із параметром запиту.
|
||||
|
||||
Тож параметр шляху буде витягнуто, провалідовано, перетворено до вказаного типу та анотовано за допомогою OpenAPI.
|
||||
|
||||
Так само ви можете оголошувати будь-який інший параметр як зазвичай і додатково отримувати `Request`.
|
||||
|
||||
///
|
||||
|
||||
## Документація `Request` { #request-documentation }
|
||||
|
||||
Детальніше про <a href="https://www.starlette.dev/requests/" class="external-link" target="_blank">об’єкт `Request` можна прочитати на офіційному сайті документації Starlette</a>.
|
||||
|
||||
/// note | Технічні деталі
|
||||
|
||||
Також можна використовувати `from starlette.requests import Request`.
|
||||
|
||||
**FastAPI** надає його напряму лише для вашої зручності як розробника. Але він походить безпосередньо зі Starlette.
|
||||
|
||||
///
|
||||
186
docs/uk/docs/advanced/websockets.md
Normal file
186
docs/uk/docs/advanced/websockets.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# WebSockets { #websockets }
|
||||
|
||||
Ви можете використовувати <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API" class="external-link" target="_blank">WebSockets</a> з **FastAPI**.
|
||||
|
||||
## Встановлення `websockets` { #install-websockets }
|
||||
|
||||
Переконайтеся, що ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його та встановили `websockets` (бібліотеку Python, яка спрощує використання протоколу «WebSocket»):
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ pip install websockets
|
||||
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Клієнт WebSockets { #websockets-client }
|
||||
|
||||
### У продакшені { #in-production }
|
||||
|
||||
У вашій продакшен-системі, ймовірно, є фронтенд, створений за допомогою сучасного фреймворку на кшталт React, Vue.js або Angular.
|
||||
|
||||
А для обміну даними через WebSockets з вашим бекендом ви, ймовірно, використовуватимете утиліти вашого фронтенда.
|
||||
|
||||
Або у вас може бути нативний мобільний застосунок, який напряму взаємодіє з вашим WebSocket-бекендом у нативному коді.
|
||||
|
||||
Або у вас може бути будь-який інший спосіб зв’язку з WebSocket endpoint.
|
||||
|
||||
---
|
||||
|
||||
Але для цього прикладу ми використаємо дуже простий HTML-документ з невеликою кількістю JavaScript — усе всередині довгого рядка.
|
||||
|
||||
Звісно, це не оптимально, і ви не використовували б це в продакшені.
|
||||
|
||||
У продакшені ви б скористалися одним із варіантів вище.
|
||||
|
||||
Але це найпростіший спосіб зосередитися на серверній частині WebSockets і мати робочий приклад:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py39.py hl[2,6:38,41:43] *}
|
||||
|
||||
## Створення `websocket` { #create-a-websocket }
|
||||
|
||||
У вашому застосунку **FastAPI** створіть `websocket`:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py39.py hl[1,46:47] *}
|
||||
|
||||
/// note | Технічні деталі
|
||||
|
||||
Ви також можете використати `from starlette.websockets import WebSocket`.
|
||||
|
||||
**FastAPI** надає той самий `WebSocket` напряму просто для зручності розробника. Але він походить безпосередньо зі Starlette.
|
||||
|
||||
///
|
||||
|
||||
## Очікування повідомлень і надсилання повідомлень { #await-for-messages-and-send-messages }
|
||||
|
||||
У вашому WebSocket маршруті ви можете робити `await` для отримання повідомлень і надсилати повідомлення.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001_py39.py hl[48:52] *}
|
||||
|
||||
Ви можете отримувати та надсилати двійкові дані, текст і JSON.
|
||||
|
||||
## Спробуйте { #try-it }
|
||||
|
||||
Якщо ваш файл має назву `main.py`, запустіть застосунок командою:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi dev main.py
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Відкрийте браузер за адресою <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>.
|
||||
|
||||
Ви побачите просту сторінку на кшталт:
|
||||
|
||||
<img src="/img/tutorial/websockets/image01.png">
|
||||
|
||||
Ви можете вводити повідомлення в полі вводу та надсилати їх:
|
||||
|
||||
<img src="/img/tutorial/websockets/image02.png">
|
||||
|
||||
І ваш застосунок **FastAPI** з WebSockets надішле відповідь у відповідь:
|
||||
|
||||
<img src="/img/tutorial/websockets/image03.png">
|
||||
|
||||
Ви можете надсилати (і отримувати) багато повідомлень:
|
||||
|
||||
<img src="/img/tutorial/websockets/image04.png">
|
||||
|
||||
І всі вони використовуватимуть одне й те саме WebSocket-з’єднання.
|
||||
|
||||
## Використання `Depends` та інших { #using-depends-and-others }
|
||||
|
||||
У WebSocket endpoints ви можете імпортувати з `fastapi` та використовувати:
|
||||
|
||||
* `Depends`
|
||||
* `Security`
|
||||
* `Cookie`
|
||||
* `Header`
|
||||
* `Path`
|
||||
* `Query`
|
||||
|
||||
Вони працюють так само, як і для інших endpoints FastAPI/*операцій шляху*:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Оскільки це WebSocket, піднімати `HTTPException` насправді не має сенсу — натомість ми піднімаємо `WebSocketException`.
|
||||
|
||||
Ви можете використати код закриття зі <a href="https://tools.ietf.org/html/rfc6455#section-7.4.1" class="external-link" target="_blank">списку валідних кодів, визначених у специфікації</a>.
|
||||
|
||||
///
|
||||
|
||||
### Спробуйте WebSockets із залежностями { #try-the-websockets-with-dependencies }
|
||||
|
||||
Якщо ваш файл має назву `main.py`, запустіть застосунок командою:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi dev main.py
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Відкрийте браузер за адресою <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>.
|
||||
|
||||
Там ви можете задати:
|
||||
|
||||
* «Item ID», який використовується в path.
|
||||
* «Token», який використовується як query parameter.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Зверніть увагу, що query `token` буде оброблено залежністю.
|
||||
|
||||
///
|
||||
|
||||
Після цього ви зможете під’єднати WebSocket, а потім надсилати й отримувати повідомлення:
|
||||
|
||||
<img src="/img/tutorial/websockets/image05.png">
|
||||
|
||||
## Обробка роз’єднань і кількох клієнтів { #handling-disconnections-and-multiple-clients }
|
||||
|
||||
Коли WebSocket-з’єднання закрито, `await websocket.receive_text()` підніме виняток `WebSocketDisconnect`, який ви можете перехопити та обробити, як у цьому прикладі.
|
||||
|
||||
{* ../../docs_src/websockets/tutorial003_py39.py hl[79:81] *}
|
||||
|
||||
Щоб спробувати:
|
||||
|
||||
* Відкрийте застосунок у кількох вкладках браузера.
|
||||
* Надсилайте повідомлення з них.
|
||||
* Потім закрийте одну з вкладок.
|
||||
|
||||
Це підніме виняток `WebSocketDisconnect`, і всі інші клієнти отримають повідомлення на кшталт:
|
||||
|
||||
```
|
||||
Client #1596980209979 left the chat
|
||||
```
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Застосунок вище — мінімальний і простий приклад, що демонструє, як обробляти та транслювати повідомлення на кілька WebSocket-з’єднань.
|
||||
|
||||
Але майте на увазі, що оскільки все обробляється в пам’яті, в одному списку, це працюватиме лише доки працює процес, і лише з одним процесом.
|
||||
|
||||
Якщо вам потрібно щось, що легко інтегрується з FastAPI, але є більш надійним і підтримується Redis, PostgreSQL чи іншими, перегляньте <a href="https://github.com/encode/broadcaster" class="external-link" target="_blank">encode/broadcaster</a>.
|
||||
|
||||
///
|
||||
|
||||
## Додаткова інформація { #more-info }
|
||||
|
||||
Щоб дізнатися більше про можливості, перегляньте документацію Starlette щодо:
|
||||
|
||||
* <a href="https://www.starlette.dev/websockets/" class="external-link" target="_blank">класу `WebSocket`</a>.
|
||||
* <a href="https://www.starlette.dev/endpoints/#websocketendpoint" class="external-link" target="_blank">обробки WebSocket на основі класів</a>.
|
||||
35
docs/uk/docs/advanced/wsgi.md
Normal file
35
docs/uk/docs/advanced/wsgi.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Підключення WSGI — Flask, Django та інші { #including-wsgi-flask-django-others }
|
||||
|
||||
Ви можете монтувати WSGI-застосунки так само, як ви бачили в [Підзастосунки — монтування](sub-applications.md){.internal-link target=_blank}, [За проксі](behind-a-proxy.md){.internal-link target=_blank}.
|
||||
|
||||
Для цього ви можете використати `WSGIMiddleware` і обгорнути ним ваш WSGI-застосунок, наприклад Flask, Django тощо.
|
||||
|
||||
## Використання `WSGIMiddleware` { #using-wsgimiddleware }
|
||||
|
||||
Потрібно імпортувати `WSGIMiddleware`.
|
||||
|
||||
Потім обгорнути WSGI-застосунок (наприклад, Flask) цим middleware.
|
||||
|
||||
І після цього змонтувати його за певним шляхом.
|
||||
|
||||
{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *}
|
||||
|
||||
## Перевірка { #check-it }
|
||||
|
||||
Тепер кожен запит за шляхом `/v1/` буде оброблятися застосунком Flask.
|
||||
|
||||
А решта — **FastAPI**.
|
||||
|
||||
Якщо ви запустите це й перейдете на <a href="http://localhost:8000/v1/" class="external-link" target="_blank">http://localhost:8000/v1/</a>, ви побачите відповідь від Flask:
|
||||
|
||||
```txt
|
||||
Hello, World from Flask!
|
||||
```
|
||||
|
||||
А якщо ви перейдете на <a href="http://localhost:8000/v2" class="external-link" target="_blank">http://localhost:8000/v2</a>, ви побачите відповідь від FastAPI:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"message": "Hello World"
|
||||
}
|
||||
```
|
||||
444
docs/uk/docs/async.md
Normal file
444
docs/uk/docs/async.md
Normal file
@@ -0,0 +1,444 @@
|
||||
# Конкурентність і async / await { #concurrency-and-async-await }
|
||||
|
||||
Деталі про синтаксис `async def` для *функцій операцій шляху* та деякі пояснення щодо асинхронного коду, конкурентності й паралелізму.
|
||||
|
||||
## Поспішаєте { #in-a-hurry }
|
||||
|
||||
<abbr title="too long; didn't read - занадто довго; не читав"><strong>TL;DR:</strong></abbr>
|
||||
|
||||
Якщо ви використовуєте сторонні бібліотеки, які кажуть викликати їх з `await`, наприклад:
|
||||
|
||||
```Python
|
||||
results = await some_library()
|
||||
```
|
||||
|
||||
Тоді оголошуйте ваші *функції операцій шляху* через `async def`, наприклад:
|
||||
|
||||
```Python hl_lines="2"
|
||||
@app.get('/')
|
||||
async def read_results():
|
||||
results = await some_library()
|
||||
return results
|
||||
```
|
||||
|
||||
/// note | Примітка
|
||||
|
||||
Ви можете використовувати `await` лише всередині функцій, створених за допомогою `async def`.
|
||||
|
||||
///
|
||||
|
||||
---
|
||||
|
||||
Якщо ви використовуєте сторонню бібліотеку, яка взаємодіє з чимось (базою даних, API, файловою системою тощо) і не підтримує використання `await` (зараз це стосується більшості бібліотек для баз даних), тоді оголошуйте ваші *функції операцій шляху* звичайно, тобто просто з `def`, наприклад:
|
||||
|
||||
```Python hl_lines="2"
|
||||
@app.get('/')
|
||||
def results():
|
||||
results = some_library()
|
||||
return results
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Якщо вашому застосунку (якимось чином) не потрібно спілкуватися ні з чим іншим і чекати на відповідь, використовуйте `async def`, навіть якщо вам не потрібно використовувати `await` всередині.
|
||||
|
||||
---
|
||||
|
||||
Якщо ви просто не знаєте — використовуйте звичайний `def`.
|
||||
|
||||
---
|
||||
|
||||
**Примітка**: Ви можете змішувати `def` і `async def` у ваших *функціях операцій шляху* стільки, скільки потрібно, і визначати кожну з них, обираючи найкращий для вас варіант. FastAPI зробить усе правильно.
|
||||
|
||||
У будь-якому разі, у всіх наведених ситуаціях FastAPI все одно працюватиме асинхронно й буде надзвичайно швидким.
|
||||
|
||||
Але якщо дотримуватися кроків вище, він зможе виконати деякі оптимізації продуктивності.
|
||||
|
||||
## Технічні деталі { #technical-details }
|
||||
|
||||
Сучасні версії Python підтримують **«асинхронний код»** за допомогою того, що називається **«coroutines»**, із синтаксисом **`async` і `await`**.
|
||||
|
||||
Розберімо цю фразу по частинах у розділах нижче:
|
||||
|
||||
* **Асинхронний код**
|
||||
* **`async` і `await`**
|
||||
* **Coroutines**
|
||||
|
||||
## Асинхронний код { #asynchronous-code }
|
||||
|
||||
Асинхронний код означає, що мова 💬 має спосіб сказати комп’ютеру / програмі 🤖, що в певний момент у коді їй 🤖 доведеться чекати, поки *щось інше* десь в іншому місці завершиться. Скажімо, це *щось інше* називається «slow-file» 📝.
|
||||
|
||||
Отже, протягом цього часу комп’ютер може піти й виконувати іншу роботу, поки «slow-file» 📝 завершується.
|
||||
|
||||
Потім комп’ютер / програма 🤖 повертатиметься щоразу, коли матиме змогу — тому що знову чекає, або коли 🤖 завершить всю роботу, яку мав у той момент. І 🤖 перевірить, чи вже завершилися якісь із завдань, на які він чекав, виконуючи те, що потрібно.
|
||||
|
||||
Далі 🤖 бере перше завдання, яке завершилося (скажімо, наш «slow-file» 📝), і продовжує робити те, що потрібно, вже з ним.
|
||||
|
||||
Це «очікування чогось іншого» зазвичай стосується операцій <abbr title="Input and Output - Ввід і вивід">I/O</abbr>, які є відносно «повільними» (порівняно зі швидкістю процесора й оперативної пам’яті), наприклад очікування:
|
||||
|
||||
* даних від клієнта, що надсилаються мережею
|
||||
* даних, відправлених вашою програмою, щоб клієнт отримав їх через мережу
|
||||
* вмісту файлу на диску, який система має прочитати та передати вашій програмі
|
||||
* вмісту, який ваша програма передала системі, щоб записати на диск
|
||||
* операції віддаленого API
|
||||
* завершення операції з базою даних
|
||||
* повернення результатів запиту до бази даних
|
||||
* тощо
|
||||
|
||||
Оскільки час виконання здебільшого витрачається на очікування операцій <abbr title="Input and Output - Ввід і вивід">I/O</abbr>, їх називають операціями «I/O bound».
|
||||
|
||||
Це називають «асинхронним», бо комп’ютеру / програмі не потрібно бути «синхронізованими» з повільним завданням — чекати точного моменту завершення завдання, нічого не роблячи, щоб забрати результат і продовжити роботу.
|
||||
|
||||
Натомість, будучи «асинхронною» системою, після завершення завдання може трохи почекати в черзі (кілька мікросекунд), доки комп’ютер / програма завершить те, що пішов робити, а потім повернеться, забере результати та продовжить роботу з ними.
|
||||
|
||||
Для «синхронного» (на противагу «асинхронному») також часто використовують термін «послідовний», бо комп’ютер / програма виконує всі кроки послідовно перед тим, як перемкнутися на інше завдання, навіть якщо ці кроки містять очікування.
|
||||
|
||||
### Конкурентність і бургери { #concurrency-and-burgers }
|
||||
|
||||
Описану вище ідею **асинхронного** коду інколи також називають **«конкурентністю»**. Вона відрізняється від **«паралелізму»**.
|
||||
|
||||
І **конкурентність**, і **паралелізм** стосуються того, що «різні речі відбуваються більш-менш одночасно».
|
||||
|
||||
Але деталі між *конкурентністю* та *паралелізмом* доволі різні.
|
||||
|
||||
Щоб побачити різницю, уявіть таку історію про бургери:
|
||||
|
||||
### Конкурентні бургери { #concurrent-burgers }
|
||||
|
||||
Ви йдете зі своєю симпатією по фастфуд, стоїте в черзі, поки касир приймає замовлення в людей перед вами. 😍
|
||||
|
||||
<img src="/img/async/concurrent-burgers/concurrent-burgers-01.png" class="illustration">
|
||||
|
||||
Потім настає ваша черга, ви робите замовлення з 2 дуже вишуканих бургерів для вашої симпатії та для вас. 🍔🍔
|
||||
|
||||
<img src="/img/async/concurrent-burgers/concurrent-burgers-02.png" class="illustration">
|
||||
|
||||
Касир щось каже кухарю на кухні, щоб ті знали, що мають приготувати ваші бургери (навіть якщо зараз вони готують для попередніх клієнтів).
|
||||
|
||||
<img src="/img/async/concurrent-burgers/concurrent-burgers-03.png" class="illustration">
|
||||
|
||||
Ви платите. 💸
|
||||
|
||||
Касир дає вам номер вашої черги.
|
||||
|
||||
<img src="/img/async/concurrent-burgers/concurrent-burgers-04.png" class="illustration">
|
||||
|
||||
Поки ви чекаєте, ви разом зі своєю симпатією знаходите столик, сідаєте й довго розмовляєте (бо ваші бургери дуже вишукані й готуються певний час).
|
||||
|
||||
Поки ви сидите за столиком зі своєю симпатією та чекаєте на бургери, ви можете витратити цей час, милуючись тим, яка чудова, мила й розумна ваша симпатія ✨😍✨.
|
||||
|
||||
<img src="/img/async/concurrent-burgers/concurrent-burgers-05.png" class="illustration">
|
||||
|
||||
Під час очікування й розмови зі своєю симпатією, час від часу ви перевіряєте номер, який показує табло на прилавку, щоб побачити, чи вже ваша черга.
|
||||
|
||||
І в якийсь момент нарешті стає ваша черга. Ви підходите до прилавка, берете бургери та повертаєтеся до столика.
|
||||
|
||||
<img src="/img/async/concurrent-burgers/concurrent-burgers-06.png" class="illustration">
|
||||
|
||||
Ви зі своєю симпатією їсте бургери й гарно проводите час. ✨
|
||||
|
||||
<img src="/img/async/concurrent-burgers/concurrent-burgers-07.png" class="illustration">
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Чудові ілюстрації від <a href="https://www.instagram.com/ketrinadrawsalot" class="external-link" target="_blank">Ketrina Thompson</a>. 🎨
|
||||
|
||||
///
|
||||
|
||||
---
|
||||
|
||||
Уявіть, що в цій історії ви — це комп’ютер / програма 🤖.
|
||||
|
||||
Поки ви в черзі, ви просто простоюєте 😴, чекаючи своєї черги й не роблячи нічого особливо «продуктивного». Але черга рухається швидко, бо касир лише приймає замовлення (а не готує їх), тож це нормально.
|
||||
|
||||
Потім, коли настає ваша черга, ви робите справді «продуктивну» роботу: дивитеся меню, вирішуєте, що хочете, дізнаєтеся вибір вашої симпатії, платите, перевіряєте, що віддаєте правильну купюру чи картку, перевіряєте, що з вас списали правильно, перевіряєте, що в замовленні правильні позиції, тощо.
|
||||
|
||||
Але далі, навіть якщо у вас ще немає бургерів, ваша робота з касиром «на паузі» ⏸, бо вам треба чекати 🕙, доки бургери будуть готові.
|
||||
|
||||
Та оскільки ви відходите від прилавка й сідаєте за столик з номером своєї черги, ви можете перемкнути 🔀 увагу на свою симпатію й «працювати» ⏯ 🤓 над цим. Тоді ви знову робите щось дуже «продуктивне» — наприклад, фліртуєте зі своєю симпатією 😍.
|
||||
|
||||
Потім касир 💁 каже «я закінчив готувати бургери», виставляючи ваш номер на табло, але ви не стрибаєте одразу, щойно номер змінюється на ваш. Ви знаєте, що ніхто не вкраде ваші бургери, бо у вас є ваш номер, а в інших — їхній.
|
||||
|
||||
Тож ви чекаєте, поки ваша симпатія закінчить історію (завершить поточну роботу ⏯ / завдання, яке обробляється 🤓), лагідно усміхаєтеся й кажете, що підете по бургери ⏸.
|
||||
|
||||
Потім ви йдете до прилавка 🔀, до початкового завдання, яке вже завершене ⏯, забираєте бургери, дякуєте та несете їх до столика. Це завершує цей крок / завдання взаємодії з прилавком ⏹. І, своєю чергою, створює нове завдання — «їсти бургери» 🔀 ⏯, але попереднє «отримати бургери» вже завершене ⏹.
|
||||
|
||||
### Паралельні бургери { #parallel-burgers }
|
||||
|
||||
А тепер уявімо, що це не «конкурентні бургери», а «паралельні бургери».
|
||||
|
||||
Ви йдете зі своєю симпатією по паралельний фастфуд.
|
||||
|
||||
Ви стоїте в черзі, поки кілька (скажімо, 8) касирів, які одночасно є кухарями, приймають замовлення у людей перед вами.
|
||||
|
||||
Кожен перед вами чекає, доки його бургери будуть готові, перш ніж відійти від прилавка, бо кожен із 8 касирів одразу йде готувати бургер, перш ніж взяти наступне замовлення.
|
||||
|
||||
<img src="/img/async/parallel-burgers/parallel-burgers-01.png" class="illustration">
|
||||
|
||||
Нарешті настає ваша черга, ви робите замовлення з 2 дуже вишуканих бургерів для вашої симпатії та для вас.
|
||||
|
||||
Ви платите 💸.
|
||||
|
||||
<img src="/img/async/parallel-burgers/parallel-burgers-02.png" class="illustration">
|
||||
|
||||
Касир іде на кухню.
|
||||
|
||||
Ви чекаєте, стоячи перед прилавком 🕙, щоб ніхто інший не забрав ваші бургери раніше за вас, адже немає номерів черги.
|
||||
|
||||
<img src="/img/async/parallel-burgers/parallel-burgers-03.png" class="illustration">
|
||||
|
||||
Оскільки ви та ваша симпатія зайняті тим, щоб ніхто не проліз уперед і не забрав ваші бургери, коли вони з’являться, ви не можете приділяти увагу своїй симпатії. 😞
|
||||
|
||||
Це «синхронна» робота: ви «синхронізовані» з касиром/кухарем 👨🍳. Вам треба чекати 🕙 й бути там у точний момент, коли касир/кухар 👨🍳 закінчить бургери й віддасть їх вам, інакше хтось інший може забрати їх.
|
||||
|
||||
<img src="/img/async/parallel-burgers/parallel-burgers-04.png" class="illustration">
|
||||
|
||||
Потім ваш касир/кухар 👨🍳 нарешті повертається з вашими бургерами, після довгого очікування 🕙 там, перед прилавком.
|
||||
|
||||
<img src="/img/async/parallel-burgers/parallel-burgers-05.png" class="illustration">
|
||||
|
||||
Ви берете бургери й ідете до столика зі своєю симпатією.
|
||||
|
||||
Ви просто їсте їх, і все. ⏹
|
||||
|
||||
<img src="/img/async/parallel-burgers/parallel-burgers-06.png" class="illustration">
|
||||
|
||||
Розмов чи флірту було небагато, бо більшість часу витратили на очікування 🕙 перед прилавком. 😞
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Чудові ілюстрації від <a href="https://www.instagram.com/ketrinadrawsalot" class="external-link" target="_blank">Ketrina Thompson</a>. 🎨
|
||||
|
||||
///
|
||||
|
||||
---
|
||||
|
||||
У сценарії з паралельними бургерами ви — комп’ютер / програма 🤖 із двома процесорами (ви та ваша симпатія), обидва чекають 🕙 і утримують увагу ⏯ на «очікуванні біля прилавка» 🕙 протягом тривалого часу.
|
||||
|
||||
У закладі фастфуду є 8 процесорів (касири/кухарі). Тоді як у закладі з конкурентними бургерами могло бути лише 2 (один касир і один кухар).
|
||||
|
||||
Але все одно, підсумковий досвід не найкращий. 😞
|
||||
|
||||
---
|
||||
|
||||
Це була паралельна «бургерна» історія. 🍔
|
||||
|
||||
Для більш «життєвого» прикладу уявіть банк.
|
||||
|
||||
Ще донедавна в більшості банків було багато касирів 👨💼👨💼👨💼👨💼 і велика черга 🕙🕙🕙🕙🕙🕙🕙🕙.
|
||||
|
||||
Усі касири виконували всю роботу з одним клієнтом за іншим 👨💼⏯.
|
||||
|
||||
А вам треба чекати 🕙 у черзі довго, інакше ви втрачаєте свою чергу.
|
||||
|
||||
Ймовірно, ви б не хотіли брати свою симпатію 😍 з собою у справах до банку 🏦.
|
||||
|
||||
### Висновок про бургери { #burger-conclusion }
|
||||
|
||||
У цьому сценарії «фастфуд із бургерами зі своєю симпатією», оскільки є багато очікування 🕙, значно логічніше мати конкурентну систему ⏸🔀⏯.
|
||||
|
||||
Так буває для більшості вебзастосунків.
|
||||
|
||||
Дуже багато користувачів, але ваш сервер чекає 🕙, поки їхнє не дуже хороше з’єднання надішле їхні запити.
|
||||
|
||||
А потім знову чекає 🕙, поки повернуться відповіді.
|
||||
|
||||
Це «очікування» 🕙 вимірюється мікросекундами, але якщо все підсумувати, зрештою це багато часу очікування.
|
||||
|
||||
Саме тому має сенс використовувати асинхронний ⏸🔀⏯ код для веб-API.
|
||||
|
||||
Саме така асинхронність зробила NodeJS популярним (хоча NodeJS не є паралельним), і це сильна сторона Go як мови програмування.
|
||||
|
||||
І це той самий рівень продуктивності, який ви отримуєте з **FastAPI**.
|
||||
|
||||
А оскільки ви можете мати паралелізм і асинхронність одночасно, ви отримуєте вищу продуктивність, ніж у більшості протестованих NodeJS-фреймворків, і рівень, порівняний з Go, який є компільованою мовою, ближчою до C <a href="https://www.techempower.com/benchmarks/#section=data-r17&hw=ph&test=query&l=zijmkf-1" class="external-link" target="_blank">(усе завдяки Starlette)</a>.
|
||||
|
||||
### Чи конкурентність краща за паралелізм { #is-concurrency-better-than-parallelism }
|
||||
|
||||
Ні! Це не мораль історії.
|
||||
|
||||
Конкурентність відрізняється від паралелізму. І вона краща в **певних** сценаріях, які включають багато очікування. Через це вона зазвичай значно краща за паралелізм для розробки вебзастосунків. Але не для всього.
|
||||
|
||||
Тож, щоб урівноважити це, уявіть таку коротку історію:
|
||||
|
||||
> Вам треба прибрати великий, брудний будинок.
|
||||
|
||||
*Так, це вся історія*.
|
||||
|
||||
---
|
||||
|
||||
Немає жодного очікування 🕙, лише багато роботи, яку треба виконати в різних місцях будинку.
|
||||
|
||||
Ви могли б робити «черги», як у прикладі з бургерами: спочатку вітальня, потім кухня, але оскільки ви ні на що не чекаєте 🕙 — лише прибираєте й прибираєте — черговість нічого б не змінила.
|
||||
|
||||
Це зайняло б стільки ж часу із «чергами» (конкурентністю) або без них, і ви виконали б ту саму кількість роботи.
|
||||
|
||||
Але в цьому випадку, якби ви могли привести 8 колишніх касирів/кухарів/тепер-прибиральників, і кожен із них (плюс ви) взяв би зону будинку, ви могли б виконувати всю роботу **паралельно**, з додатковою допомогою, і закінчити набагато швидше.
|
||||
|
||||
У цьому сценарії кожен прибиральник (включно з вами) був би процесором, що робить свою частину роботи.
|
||||
|
||||
І оскільки більшість часу виконання займає реальна робота (а не очікування), а робота в комп’ютері виконується <abbr title="Central Processing Unit - Центральний процесор">CPU</abbr>, такі задачі називають «CPU bound».
|
||||
|
||||
---
|
||||
|
||||
Поширені приклади CPU bound операцій — це те, що потребує складної математичної обробки.
|
||||
|
||||
Наприклад:
|
||||
|
||||
* **Обробка аудіо** або **зображень**.
|
||||
* **Комп’ютерний зір**: зображення складається з мільйонів пікселів, кожен піксель має 3 значення/кольори; обробка зазвичай вимагає обчислювати щось над цими пікселями — усіма одночасно.
|
||||
* **Machine Learning**: зазвичай потребує великої кількості множень «матриць» і «векторів». Уявіть величезну електронну таблицю з числами та множення їх усіх одночасно.
|
||||
* **Deep Learning**: це підгалузь Machine Learning, тож діє те саме. Просто це не одна таблиця чисел для множення, а величезний набір таких таблиць, і в багатьох випадках ви використовуєте спеціальний процесор для побудови та/або використання цих моделей.
|
||||
|
||||
### Конкурентність + паралелізм: веб + Machine Learning { #concurrency-parallelism-web-machine-learning }
|
||||
|
||||
З **FastAPI** ви можете скористатися конкурентністю, яка дуже типова для веброзробки (це та сама головна перевага NodeJS).
|
||||
|
||||
Але ви також можете використовувати переваги паралелізму та multiprocessing (коли кілька процесів працюють паралельно) для **CPU bound** навантажень, як у системах Machine Learning.
|
||||
|
||||
Це, плюс простий факт, що Python — головна мова для **Data Science**, Machine Learning і особливо Deep Learning, робить FastAPI дуже вдалим вибором для веб-API та застосунків у Data Science / Machine Learning (серед багатьох інших).
|
||||
|
||||
Щоб побачити, як досягти цього паралелізму у production, дивіться розділ про [Розгортання](deployment/index.md){.internal-link target=_blank}.
|
||||
|
||||
## `async` і `await` { #async-and-await }
|
||||
|
||||
Сучасні версії Python мають дуже інтуїтивний спосіб визначати асинхронний код. Це робить його схожим на звичайний «послідовний» код і виконує «очікування» за вас у потрібні моменти.
|
||||
|
||||
Коли є операція, яка вимагатиме очікування перед тим, як надати результати, і має підтримку цих нових можливостей Python, ви можете написати так:
|
||||
|
||||
```Python
|
||||
burgers = await get_burgers(2)
|
||||
```
|
||||
|
||||
Ключове тут — `await`. Він каже Python, що треба зачекати ⏸, доки `get_burgers(2)` завершить свою справу 🕙, перш ніж зберегти результат у `burgers`. Завдяки цьому Python знатиме, що тим часом може піти й зробити щось інше 🔀 ⏯ (наприклад, прийняти інший запит).
|
||||
|
||||
Щоб `await` працював, він має бути всередині функції, яка підтримує цю асинхронність. Для цього ви просто оголошуєте її через `async def`:
|
||||
|
||||
```Python hl_lines="1"
|
||||
async def get_burgers(number: int):
|
||||
# Do some asynchronous stuff to create the burgers
|
||||
return burgers
|
||||
```
|
||||
|
||||
...замість `def`:
|
||||
|
||||
```Python hl_lines="2"
|
||||
# This is not asynchronous
|
||||
def get_sequential_burgers(number: int):
|
||||
# Do some sequential stuff to create the burgers
|
||||
return burgers
|
||||
```
|
||||
|
||||
З `async def` Python знає, що всередині цієї функції треба враховувати вирази `await`, і що він може «призупинити» ⏸ виконання цієї функції та піти зробити щось інше 🔀, перш ніж повернутися.
|
||||
|
||||
Коли ви хочете викликати функцію `async def`, вам треба її «await». Тож це не працюватиме:
|
||||
|
||||
```Python
|
||||
# This won't work, because get_burgers was defined with: async def
|
||||
burgers = get_burgers(2)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Отже, якщо ви використовуєте бібліотеку, яка каже, що її можна викликати з `await`, вам потрібно створювати *функції операцій шляху*, які її використовують, через `async def`, як тут:
|
||||
|
||||
```Python hl_lines="2-3"
|
||||
@app.get('/burgers')
|
||||
async def read_burgers():
|
||||
burgers = await get_burgers(2)
|
||||
return burgers
|
||||
```
|
||||
|
||||
### Більш технічні деталі { #more-technical-details }
|
||||
|
||||
Ви могли помітити, що `await` можна використовувати лише всередині функцій, визначених через `async def`.
|
||||
|
||||
Але водночас функції, визначені через `async def`, потрібно «await». Тож функції з `async def` можна викликати лише всередині функцій, визначених через `async def`.
|
||||
|
||||
Тож щодо «курки й яйця»: як викликати першу `async`-функцію?
|
||||
|
||||
Якщо ви працюєте з **FastAPI**, вам не потрібно про це турбуватися, бо «першою» функцією буде ваша *функція операції шляху*, і FastAPI знатиме, як зробити все правильно.
|
||||
|
||||
Але якщо ви хочете використовувати `async` / `await` без FastAPI — ви теж можете.
|
||||
|
||||
### Пишіть власний async-код { #write-your-own-async-code }
|
||||
|
||||
Starlette (і **FastAPI**) базуються на <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a>, що робить їх сумісними і зі стандартною бібліотекою Python <a href="https://docs.python.org/3/library/asyncio-task.html" class="external-link" target="_blank">asyncio</a>, і з <a href="https://trio.readthedocs.io/en/stable/" class="external-link" target="_blank">Trio</a>.
|
||||
|
||||
Зокрема, ви можете безпосередньо використовувати <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a> для ваших просунутих сценаріїв конкурентності, які потребують складніших шаблонів у власному коді.
|
||||
|
||||
І навіть якщо ви не використовуєте FastAPI, ви також можете писати власні async-застосунки з <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a>, щоб мати високу сумісність і отримати його переваги (наприклад, *structured concurrency*).
|
||||
|
||||
Я створив ще одну бібліотеку поверх AnyIO як тонкий шар, щоб трохи покращити type annotations і отримати кращі **autocompletion**, **inline errors** тощо. Вона також має дружній вступ і навчальний посібник, щоб допомогти вам **зрозуміти** та писати **власний async-код**: <a href="https://asyncer.tiangolo.com/" class="external-link" target="_blank">Asyncer</a>. Вона буде особливо корисною, якщо вам потрібно **поєднувати async-код зі звичайним** (blocking/synchronous) кодом.
|
||||
|
||||
### Інші форми асинхронного коду { #other-forms-of-asynchronous-code }
|
||||
|
||||
Цей стиль використання `async` і `await` є відносно новим у мові.
|
||||
|
||||
Але він значно спрощує роботу з асинхронним кодом.
|
||||
|
||||
Подібний (або майже ідентичний) синтаксис нещодавно з’явився також у сучасних версіях JavaScript (у Browser і NodeJS).
|
||||
|
||||
Але до цього обробка асинхронного коду була значно складнішою.
|
||||
|
||||
У попередніх версіях Python ви могли використовувати потоки (threads) або <a href="https://www.gevent.org/" class="external-link" target="_blank">Gevent</a>. Але такий код значно складніше розуміти, налагоджувати й продумувати.
|
||||
|
||||
У попередніх версіях NodeJS / Browser JavaScript ви б використовували «callbacks», що призводить до «callback hell».
|
||||
|
||||
## Coroutines { #coroutines }
|
||||
|
||||
**Coroutine** — це просто дуже «пишний» термін для того, що повертає функція `async def`. Python знає, що це щось на кшталт функції, яку можна запустити й яка колись завершиться, але яка також може бути призупинена ⏸ всередині, коли є `await`.
|
||||
|
||||
Але всю цю функціональність використання асинхронного коду з `async` і `await` часто узагальнюють як використання «coroutines». Це можна порівняти з головною фішкою Go — «Goroutines».
|
||||
|
||||
## Підсумок { #conclusion }
|
||||
|
||||
Подивімося на ту саму фразу з вище:
|
||||
|
||||
> Сучасні версії Python підтримують **«асинхронний код»** за допомогою того, що називається **«coroutines»**, із синтаксисом **`async` і `await`**.
|
||||
|
||||
Тепер це має бути зрозумілішим. ✨
|
||||
|
||||
Усе це — те, що «живить» FastAPI (через Starlette) і дає йому таку вражаючу продуктивність.
|
||||
|
||||
## Дуже технічні деталі { #very-technical-details }
|
||||
|
||||
/// warning | Попередження
|
||||
|
||||
Ймовірно, ви можете пропустити це.
|
||||
|
||||
Це дуже технічні деталі того, як **FastAPI** працює всередині.
|
||||
|
||||
Якщо ви маєте достатньо технічних знань (coroutines, threads, blocking тощо) і вам цікаво, як FastAPI обробляє `async def` проти звичайного `def`, продовжуйте.
|
||||
|
||||
///
|
||||
|
||||
### Функції операцій шляху { #path-operation-functions }
|
||||
|
||||
Коли ви оголошуєте *функцію операції шляху* звичайним `def` замість `async def`, вона виконується в зовнішньому threadpool, і вже його «await», замість прямого виклику (бо це заблокувало б сервер).
|
||||
|
||||
Якщо ви переходите з іншого async-фреймворку, який не працює так, як описано вище, і звикли визначати тривіальні *функції операцій шляху* лише з обчисленнями через простий `def` заради невеликого виграшу продуктивності (приблизно 100 наносекунд), зверніть увагу, що в **FastAPI** ефект буде протилежним. У таких випадках краще використовувати `async def`, якщо тільки ваші *функції операцій шляху* не використовують код, що виконує блокувальні <abbr title="Input/Output - Ввід/вивід: читання або запис на диск, мережеві комунікації.">I/O</abbr>.
|
||||
|
||||
Утім, у обох ситуаціях, найімовірніше, **FastAPI** буде [все одно швидшим](index.md#performance){.internal-link target=_blank}, ніж (або принаймні порівнянним із) ваш попередній фреймворк.
|
||||
|
||||
### Залежності { #dependencies }
|
||||
|
||||
Те саме стосується [залежностей](tutorial/dependencies/index.md){.internal-link target=_blank}. Якщо залежність — це стандартна функція `def`, а не `async def`, вона виконується в зовнішньому threadpool.
|
||||
|
||||
### Підзалежності { #sub-dependencies }
|
||||
|
||||
Ви можете мати кілька залежностей і [підзалежностей](tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank}, які потребують одна одну (як параметри у визначеннях функцій); деякі з них можуть бути створені через `async def`, а деякі — через звичайний `def`. Це все одно працюватиме, а ті, що створені через звичайний `def`, будуть викликані в зовнішньому потоці (із threadpool), замість того щоб їх «await».
|
||||
|
||||
### Інші допоміжні функції { #other-utility-functions }
|
||||
|
||||
Будь-яка інша допоміжна функція, яку ви викликаєте безпосередньо, може бути створена як звичайним `def`, так і `async def`, і FastAPI не впливатиме на те, як ви її викликаєте.
|
||||
|
||||
Це відрізняється від функцій, які FastAPI викликає за вас: *функцій операцій шляху* та залежностей.
|
||||
|
||||
Якщо ваша допоміжна функція — звичайна з `def`, вона буде викликана напряму (як ви написали у своєму коді), не в threadpool; якщо функція створена через `async def`, тоді під час виклику у вашому коді вам слід зробити `await`.
|
||||
|
||||
---
|
||||
|
||||
Знову ж таки, це дуже технічні деталі, які, ймовірно, корисні, якщо ви спеціально їх шукали.
|
||||
|
||||
Інакше вам має вистачити рекомендацій із розділу вище: <a href="#in-a-hurry">Поспішаєте?</a>.
|
||||
34
docs/uk/docs/benchmarks.md
Normal file
34
docs/uk/docs/benchmarks.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Бенчмарки { #benchmarks }
|
||||
|
||||
Незалежні бенчмарки TechEmpower показують, що застосунки **FastAPI**, які працюють під Uvicorn, є <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">одними з найшвидших доступних Python-фреймворків</a>, поступаючись лише Starlette та самому Uvicorn (які FastAPI використовує всередині).
|
||||
|
||||
Але, переглядаючи бенчмарки та порівняння, варто пам’ятати про таке.
|
||||
|
||||
## Бенчмарки та швидкість { #benchmarks-and-speed }
|
||||
|
||||
Коли ви переглядаєте бенчмарки, часто можна побачити, що кілька інструментів різних типів порівнюють так, ніби вони рівноцінні.
|
||||
|
||||
Зокрема, можна побачити порівняння Uvicorn, Starlette і FastAPI разом (серед багатьох інших інструментів).
|
||||
|
||||
Що простішу задачу розв’язує інструмент, то кращу продуктивність він покаже. А більшість бенчмарків не тестує додаткові можливості, які надає інструмент.
|
||||
|
||||
Ієрархія така:
|
||||
|
||||
* **Uvicorn**: ASGI-сервер
|
||||
* **Starlette**: (використовує Uvicorn) веб-мікрофреймворк
|
||||
* **FastAPI**: (використовує Starlette) API-мікрофреймворк із кількома додатковими можливостями для побудови API, із валідацією даних тощо
|
||||
|
||||
* **Uvicorn**:
|
||||
* Матиме найкращу продуктивність, адже майже не містить додаткового коду, окрім самого сервера.
|
||||
* Ви не будете писати застосунок безпосередньо на Uvicorn. Це означало б, що у ваш код довелося б включити більш-менш принаймні весь код, який надає Starlette (або **FastAPI**). А якщо зробити так, ваш фінальний застосунок матиме таке саме накладне навантаження, як і при використанні фреймворку, але без переваг мінімізації коду застосунку та кількості помилок.
|
||||
* Якщо ви порівнюєте Uvicorn, порівнюйте його з Daphne, Hypercorn, uWSGI тощо — серверами застосунків.
|
||||
* **Starlette**:
|
||||
* Матиме наступну найкращу продуктивність після Uvicorn. Фактично Starlette використовує Uvicorn для запуску. Тож, імовірно, він може бути лише «повільнішим» за Uvicorn через потребу виконувати більше коду.
|
||||
* Але він надає вам інструменти для побудови простих вебзастосунків, з маршрутизацією за шляхами тощо.
|
||||
* Якщо ви порівнюєте Starlette, порівнюйте його з Sanic, Flask, Django тощо — вебфреймворками (або мікрофреймворками).
|
||||
* **FastAPI**:
|
||||
* Так само як Starlette використовує Uvicorn і не може бути швидшим за нього, **FastAPI** використовує Starlette, тож не може бути швидшим за нього.
|
||||
* FastAPI надає більше можливостей поверх Starlette. Це можливості, які майже завжди потрібні під час побудови API, як-от валідація та серіалізація даних. А використовуючи його, ви отримуєте автоматичну документацію без додаткових зусиль (автоматична документація навіть не додає накладних витрат під час виконання застосунків — вона генерується під час запуску).
|
||||
* Якби ви не використовували FastAPI й застосовували Starlette напряму (або інший інструмент, як-от Sanic, Flask, Responder тощо), вам довелося б реалізувати всю валідацію та серіалізацію даних самостійно. Тож ваш фінальний застосунок усе одно мав би таке саме накладне навантаження, як і застосунок, побудований із FastAPI. І в багатьох випадках саме ця валідація та серіалізація є найбільшим обсягом коду, який пишуть у застосунках.
|
||||
* Отже, використовуючи FastAPI, ви заощаджуєте час розробки, зменшуєте кількість помилок і рядків коду та, ймовірно, отримаєте таку саму продуктивність (або кращу), як і без нього (бо все одно довелося б реалізувати все це у вашому коді).
|
||||
* Якщо ви порівнюєте FastAPI, порівнюйте його з фреймворком вебзастосунків (або набором інструментів), який надає валідацію даних, серіалізацію та документацію, як-от Flask-apispec, NestJS, Molten тощо — фреймворками з інтегрованими автоматичними валідацією даних, серіалізацією та документацією.
|
||||
24
docs/uk/docs/deployment/cloud.md
Normal file
24
docs/uk/docs/deployment/cloud.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Розгортання FastAPI у хмарних провайдерів { #deploy-fastapi-on-cloud-providers }
|
||||
|
||||
Ви можете використовувати практично **будь-якого хмарного провайдера** для розгортання вашого застосунку FastAPI.
|
||||
|
||||
У більшості випадків основні хмарні провайдери мають інструкції з розгортання FastAPI у їхньому середовищі.
|
||||
|
||||
## FastAPI Cloud { #fastapi-cloud }
|
||||
|
||||
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** створено тим самим автором і командою, що стоять за **FastAPI**.
|
||||
|
||||
Він спрощує процес **збирання**, **розгортання** та **доступу** до API з мінімальними зусиллями.
|
||||
|
||||
Він забезпечує такий самий **досвід розробника** під час розробки застосунків на FastAPI, і під час **розгортання** їх у хмарі. 🎉
|
||||
|
||||
FastAPI Cloud — основний спонсор і джерело фінансування для open source проєктів *FastAPI and friends*. ✨
|
||||
|
||||
## Хмарні провайдери — спонсори { #cloud-providers-sponsors }
|
||||
|
||||
Деякі інші хмарні провайдери ✨ також [**спонсорують FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨. 🙇
|
||||
|
||||
Ви також можете розглянути їх, щоб скористатися їхніми інструкціями та спробувати їхні сервіси:
|
||||
|
||||
* <a href="https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi" class="external-link" target="_blank">Render</a>
|
||||
* <a href="https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi" class="external-link" target="_blank">Railway</a>
|
||||
321
docs/uk/docs/deployment/concepts.md
Normal file
321
docs/uk/docs/deployment/concepts.md
Normal file
@@ -0,0 +1,321 @@
|
||||
# Концепції розгортання { #deployments-concepts }
|
||||
|
||||
Під час розгортання застосунку **FastAPI** (або будь-якого веб-API) є кілька концепцій, які, ймовірно, вас цікавлять. Спираючись на них, ви зможете знайти **найбільш відповідний** спосіб **розгорнути ваш застосунок**.
|
||||
|
||||
Деякі з важливих концепцій:
|
||||
|
||||
* Безпека — HTTPS
|
||||
* Запуск під час старту
|
||||
* Перезапуски
|
||||
* Реплікація (кількість запущених процесів)
|
||||
* Пам’ять
|
||||
* Попередні кроки перед запуском
|
||||
|
||||
Розглянемо, як вони впливають на **розгортання**.
|
||||
|
||||
Зрештою, головна мета — **обслуговувати клієнтів вашого API** так, щоб це було **безпечно**, **уникати простоїв** і якнайефективніше використовувати **обчислювальні ресурси** (наприклад, віддалені сервери/віртуальні машини). 🚀
|
||||
|
||||
Далі я трохи докладніше розповім про ці **концепції** — це, сподіваюся, дасть вам **інтуїтивне розуміння**, потрібне для вибору способу розгортання вашого API в дуже різних середовищах, можливо навіть у **майбутніх**, яких ще не існує.
|
||||
|
||||
Розглядаючи ці концепції, ви зможете **оцінити та спроєктувати** найкращий спосіб розгортання **власних API**.
|
||||
|
||||
У наступних розділах я наведу більш **конкретні рецепти** розгортання застосунків FastAPI.
|
||||
|
||||
А поки що розгляньмо ці важливі **концептуальні ідеї**. Вони також застосовні до будь-якого іншого типу веб-API. 💡
|
||||
|
||||
## Безпека — HTTPS { #security-https }
|
||||
|
||||
У [попередньому розділі про HTTPS](https.md){.internal-link target=_blank} ми дізналися, як HTTPS забезпечує шифрування для вашого API.
|
||||
|
||||
Ми також побачили, що HTTPS зазвичай надається компонентом, **зовнішнім** щодо вашого сервера застосунку — **TLS Termination Proxy**.
|
||||
|
||||
І має бути щось, відповідальне за **оновлення HTTPS-сертифікатів** — це може бути той самий компонент або інший.
|
||||
|
||||
### Приклади інструментів для HTTPS { #example-tools-for-https }
|
||||
|
||||
Деякі інструменти, які ви можете використати як TLS Termination Proxy:
|
||||
|
||||
* Traefik
|
||||
* Автоматично виконує оновлення сертифікатів ✨
|
||||
* Caddy
|
||||
* Автоматично виконує оновлення сертифікатів ✨
|
||||
* Nginx
|
||||
* Із зовнішнім компонентом на кшталт Certbot для оновлення сертифікатів
|
||||
* HAProxy
|
||||
* Із зовнішнім компонентом на кшталт Certbot для оновлення сертифікатів
|
||||
* Kubernetes з Ingress Controller на кшталт Nginx
|
||||
* Із зовнішнім компонентом на кшталт cert-manager для оновлення сертифікатів
|
||||
* Внутрішньо обробляється хмарним провайдером як частина їхніх сервісів (читайте нижче 👇)
|
||||
|
||||
Інший варіант — використати **хмарний сервіс**, який бере на себе більше роботи, зокрема налаштування HTTPS. Це може мати обмеження або коштувати дорожче тощо. Але в такому разі вам не доведеться налаштовувати TLS Termination Proxy самостійно.
|
||||
|
||||
Кілька конкретних прикладів я покажу в наступних розділах.
|
||||
|
||||
---
|
||||
|
||||
Далі йдуть концепції, пов’язані з програмою, що запускає ваш фактичний API (наприклад, Uvicorn).
|
||||
|
||||
## Програма і процес { #program-and-process }
|
||||
|
||||
Ми багато говоритимемо про запущений «**процес**», тож корисно чітко розуміти, що це означає, і в чому різниця з терміном «**програма**».
|
||||
|
||||
### Що таке програма { #what-is-a-program }
|
||||
|
||||
Слово **програма** часто використовують для опису багатьох речей:
|
||||
|
||||
* **Код**, який ви пишете, **файли Python**.
|
||||
* **Файл**, який може **виконуватися** операційною системою, наприклад: `python`, `python.exe` або `uvicorn`.
|
||||
* Конкретна програма, коли вона **виконується** в операційній системі, використовуючи CPU та зберігаючи дані в пам’яті. Це також називається **процесом**.
|
||||
|
||||
### Що таке процес { #what-is-a-process }
|
||||
|
||||
Слово **процес** зазвичай використовується більш конкретно — лише для того, що виконується в операційній системі (як в останньому пункті вище):
|
||||
|
||||
* Конкретна програма, коли вона **виконується** в операційній системі.
|
||||
* Це не про файл і не про код — це **саме** те, що **виконується** та керується операційною системою.
|
||||
* Будь-яка програма, будь-який код **може щось робити** лише тоді, коли він **виконується**. Тобто коли **працює процес**.
|
||||
* Процес може бути **завершений** (або «вбитий») вами або операційною системою. Після цього він перестає виконуватися і **більше не може нічого робити**.
|
||||
* Кожен застосунок, який у вас запущений на комп’ютері, має свій процес: кожна запущена програма, кожне вікно тощо. І зазвичай одночасно працює багато процесів, доки комп’ютер увімкнений.
|
||||
* Може бути **кілька процесів** **однієї й тієї самої програми**, запущених одночасно.
|
||||
|
||||
Якщо у вашій операційній системі відкрити «task manager» або «system monitor» (чи подібні інструменти), ви зможете побачити багато таких процесів.
|
||||
|
||||
І, наприклад, ви, ймовірно, побачите кілька процесів одного браузера (Firefox, Chrome, Edge тощо). Зазвичай він запускає один процес на вкладку плюс деякі додаткові процеси.
|
||||
|
||||
<img class="shadow" src="/img/deployment/concepts/image01.png">
|
||||
|
||||
---
|
||||
|
||||
Тепер, коли ми знаємо різницю між термінами **процес** і **програма**, продовжимо говорити про розгортання.
|
||||
|
||||
## Запуск під час старту { #running-on-startup }
|
||||
|
||||
У більшості випадків, коли ви створюєте веб-API, ви хочете, щоб воно **працювало завжди**, без перерв, аби клієнти могли постійно мати доступ. Звісно, якщо у вас немає конкретної причини запускати його лише в певних ситуаціях, найчастіше ви хочете, щоб воно було постійно запущене й **доступне**.
|
||||
|
||||
### На віддаленому сервері { #in-a-remote-server }
|
||||
|
||||
Коли ви налаштовуєте віддалений сервер (хмарний сервер, віртуальну машину тощо), найпростіше — запускати `fastapi run` (який використовує Uvicorn) або щось подібне вручну, так само як під час локальної розробки.
|
||||
|
||||
Це працюватиме й буде корисним **під час розробки**.
|
||||
|
||||
Але якщо ваше з’єднання із сервером буде втрачено, **запущений процес**, імовірно, завершиться.
|
||||
|
||||
І якщо сервер перезапуститься (наприклад, після оновлень або міграцій у хмарного провайдера), ви, ймовірно, **цього не помітите**. Через це ви навіть не знатимете, що процес треба перезапустити вручну. Тож ваш API просто залишиться «мертвим». 😱
|
||||
|
||||
### Автоматичний запуск під час старту { #run-automatically-on-startup }
|
||||
|
||||
Загалом, вам, ймовірно, потрібно, щоб серверна програма (наприклад, Uvicorn) запускалася автоматично під час старту сервера, без будь-якого **втручання людини**, і щоб процес із вашим API завжди працював (наприклад, Uvicorn, який запускає ваш застосунок FastAPI).
|
||||
|
||||
### Окрема програма { #separate-program }
|
||||
|
||||
Щоб цього досягти, зазвичай використовують **окрему програму**, яка гарантуватиме запуск вашого застосунку під час старту. У багатьох випадках вона також подбає про запуск інших компонентів або застосунків, наприклад бази даних.
|
||||
|
||||
### Приклади інструментів для запуску під час старту { #example-tools-to-run-at-startup }
|
||||
|
||||
Ось кілька прикладів інструментів, які можуть виконувати цю роль:
|
||||
|
||||
* Docker
|
||||
* Kubernetes
|
||||
* Docker Compose
|
||||
* Docker у режимі Swarm Mode
|
||||
* Systemd
|
||||
* Supervisor
|
||||
* Внутрішньо обробляється хмарним провайдером як частина їхніх сервісів
|
||||
* Інші...
|
||||
|
||||
Більш конкретні приклади я наведу в наступних розділах.
|
||||
|
||||
## Перезапуски { #restarts }
|
||||
|
||||
Подібно до того, як ви хочете забезпечити запуск застосунку під час старту, ви, ймовірно, також хочете, щоб він **перезапускався** після збоїв.
|
||||
|
||||
### Ми помиляємося { #we-make-mistakes }
|
||||
|
||||
Ми, люди, **помиляємося** постійно. У програмному забезпеченні майже *завжди* є приховані **помилки** в різних місцях. 🐛
|
||||
|
||||
А ми, як розробники, продовжуємо покращувати код, знаходячи ці помилки та додаючи нові функції (можливо, додаючи й нові баги 😅).
|
||||
|
||||
### Невеликі помилки обробляються автоматично { #small-errors-automatically-handled }
|
||||
|
||||
Під час побудови веб-API з FastAPI, якщо в нашому коді трапляється помилка, FastAPI зазвичай ізолює її до одного запиту, який спричинив помилку. 🛡
|
||||
|
||||
Клієнт отримає **500 Internal Server Error** для цього запиту, але застосунок продовжить працювати для наступних запитів замість повного падіння.
|
||||
|
||||
### Більші помилки — падіння { #bigger-errors-crashes }
|
||||
|
||||
Втім, можуть бути випадки, коли ми напишемо код, що **завалює весь застосунок**, змушуючи Uvicorn і Python аварійно завершитися. 💥
|
||||
|
||||
І все одно, ви, ймовірно, не хочете, щоб застосунок залишався «мертвим» через помилку в одному місці — ви хочете, щоб він **продовжував працювати** принаймні для тих *операцій шляху*, які не зламані.
|
||||
|
||||
### Перезапуск після падіння { #restart-after-crash }
|
||||
|
||||
Але в таких випадках із дійсно серйозними помилками, які «валять» запущений **процес**, вам потрібен зовнішній компонент, відповідальний за **перезапуск** процесу, принаймні кілька разів...
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
...Хоча якщо весь застосунок **падає одразу**, то, ймовірно, немає сенсу перезапускати його безкінечно. Але в таких випадках ви, швидше за все, помітите це під час розробки або принаймні одразу після розгортання.
|
||||
|
||||
Тож зосередьмося на основних випадках: коли він може повністю впасти у певних ситуаціях **у майбутньому**, і тоді перезапуск усе ще має сенс.
|
||||
|
||||
///
|
||||
|
||||
Ви, ймовірно, захочете, щоб компонент, відповідальний за перезапуск вашого застосунку, був **зовнішнім**, бо на той момент сам застосунок із Uvicorn і Python уже впав, тож у тому ж коді цього ж застосунку не залишиться нічого, що могло б на це вплинути.
|
||||
|
||||
### Приклади інструментів для автоматичного перезапуску { #example-tools-to-restart-automatically }
|
||||
|
||||
У більшості випадків той самий інструмент, який використовується для **запуску програми під час старту**, також застосовується для автоматичних **перезапусків**.
|
||||
|
||||
Наприклад, це можуть робити:
|
||||
|
||||
* Docker
|
||||
* Kubernetes
|
||||
* Docker Compose
|
||||
* Docker у режимі Swarm Mode
|
||||
* Systemd
|
||||
* Supervisor
|
||||
* Внутрішньо обробляється хмарним провайдером як частина їхніх сервісів
|
||||
* Інші...
|
||||
|
||||
## Реплікація — процеси та пам’ять { #replication-processes-and-memory }
|
||||
|
||||
Для застосунку FastAPI, якщо запускати його через серверну програму на кшталт команди `fastapi`, що запускає Uvicorn, одного запуску **в одному процесі** достатньо, щоб обслуговувати кількох клієнтів одночасно.
|
||||
|
||||
Але в багатьох випадках ви захочете запускати одразу кілька worker-процесів.
|
||||
|
||||
### Кілька процесів — workers { #multiple-processes-workers }
|
||||
|
||||
Якщо клієнтів більше, ніж може обробити один процес (наприклад, якщо віртуальна машина не надто потужна) і на сервері є **кілька ядер** CPU, тоді ви можете запустити **кілька процесів** з одним і тим самим застосунком і розподіляти між ними всі запити.
|
||||
|
||||
Коли ви запускаєте **кілька процесів** однієї програми API, їх зазвичай називають **workers**.
|
||||
|
||||
### Worker-процеси та порти { #worker-processes-and-ports }
|
||||
|
||||
Пам’ятаєте з документації [Про HTTPS](https.md){.internal-link target=_blank}, що лише один процес може «слухати» одну комбінацію порту й IP-адреси на сервері?
|
||||
|
||||
Це все ще так.
|
||||
|
||||
Отже, щоб мати **кілька процесів** одночасно, має бути **один процес, що слухає порт**, і який потім якимось чином передає комунікацію кожному worker-процесу.
|
||||
|
||||
### Пам’ять на процес { #memory-per-process }
|
||||
|
||||
Коли програма завантажує дані в пам’ять, наприклад модель машинного навчання у змінну або вміст великого файлу в змінну, усе це **споживає частину пам’яті (RAM)** сервера.
|
||||
|
||||
А кілька процесів зазвичай **не ділять пам’ять**. Це означає, що кожен запущений процес має свої дані, змінні та пам’ять. І якщо ваш код споживає багато пам’яті, **кожен процес** споживатиме приблизно таку саму кількість пам’яті.
|
||||
|
||||
### Пам’ять сервера { #server-memory }
|
||||
|
||||
Наприклад, якщо ваш код завантажує модель машинного навчання розміром **1 GB**, то під час запуску одного процесу з вашим API він споживатиме щонайменше 1 GB RAM. А якщо ви запустите **4 процеси** (4 workers), кожен споживатиме 1 GB RAM. Тобто загалом ваш API споживатиме **4 GB RAM**.
|
||||
|
||||
І якщо на вашому віддаленому сервері або віртуальній машині є лише 3 GB RAM, спроба завантажити понад 4 GB RAM спричинить проблеми. 🚨
|
||||
|
||||
### Кілька процесів — приклад { #multiple-processes-an-example }
|
||||
|
||||
У цьому прикладі є **керівний процес (Manager Process)**, який запускає та контролює два **worker-процеси (Worker Processes)**.
|
||||
|
||||
Цей керівний процес, імовірно, буде тим, хто «слухає» **порт** на IP. І він передаватиме всю комунікацію worker-процесам.
|
||||
|
||||
А worker-процеси запускатимуть ваш застосунок, виконуватимуть основні обчислення для отримання **запиту** й повернення **відповіді**, та завантажуватимуть у RAM усе, що ви зберігаєте у змінних.
|
||||
|
||||
<img src="/img/deployment/concepts/process-ram.drawio.svg">
|
||||
|
||||
І, звісно, та сама машина, ймовірно, матиме **інші процеси**, окрім вашого застосунку.
|
||||
|
||||
Цікавий нюанс: відсоток **використання CPU** кожним процесом може сильно **змінюватися** з часом, тоді як **пам’ять (RAM)** зазвичай лишається більш-менш **стабільною**.
|
||||
|
||||
Якщо у вас API, яке щоразу виконує порівнянний обсяг обчислень, і у вас багато клієнтів, то **завантаження CPU**, ймовірно, *також буде стабільним* (замість того, щоб постійно швидко зростати й падати).
|
||||
|
||||
### Приклади інструментів і стратегій реплікації { #examples-of-replication-tools-and-strategies }
|
||||
|
||||
Є кілька підходів, як цього досягти. Про конкретні стратегії я розповім у наступних розділах, наприклад коли говоритимемо про Docker і контейнери.
|
||||
|
||||
Головне обмеження, яке слід врахувати: має бути **єдиний** компонент, що керує **портом** на **публічній IP-адресі**. А потім він має мати спосіб **передавати** комунікацію до реплікованих **процесів/workers**.
|
||||
|
||||
Ось кілька можливих комбінацій і стратегій:
|
||||
|
||||
* **Uvicorn** з `--workers`
|
||||
* Один **менеджер процесів** Uvicorn слухатиме **IP** і **порт** та запускатиме **кілька worker-процесів** Uvicorn.
|
||||
* **Kubernetes** та інші розподілені **контейнерні системи**
|
||||
* Щось на рівні **Kubernetes** слухатиме **IP** і **порт**. Реплікація відбуватиметься через **кілька контейнерів**, у кожному з яких запущено **один процес Uvicorn**.
|
||||
* **Хмарні сервіси**, які роблять це за вас
|
||||
* Хмарний сервіс, імовірно, **виконає реплікацію за вас**. Він може дозволити вам визначити **процес для запуску** або **образ контейнера**. У будь-якому разі, найімовірніше, це буде **один процес Uvicorn**, а хмарний сервіс відповідатиме за його реплікацію.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Не хвилюйтеся, якщо деякі пункти про **контейнери**, Docker або Kubernetes поки що не дуже зрозумілі.
|
||||
|
||||
Докладніше про образи контейнерів, Docker, Kubernetes тощо буде в майбутньому розділі: [FastAPI у контейнерах — Docker](docker.md){.internal-link target=_blank}.
|
||||
|
||||
///
|
||||
|
||||
## Попередні кроки перед запуском { #previous-steps-before-starting }
|
||||
|
||||
Є багато випадків, коли ви хочете виконати певні кроки **перед запуском** застосунку.
|
||||
|
||||
Наприклад, ви можете захотіти виконати **міграції бази даних**.
|
||||
|
||||
Але в більшості випадків ці кроки потрібно виконувати лише **один раз**.
|
||||
|
||||
Тож вам потрібен **єдиний процес**, який виконає ці **попередні кроки** перед запуском застосунку.
|
||||
|
||||
І вам доведеться гарантувати, що це справді один процес, який виконує попередні кроки, *навіть якщо* після цього ви запускаєте **кілька процесів** (кілька workers) для самого застосунку. Якби ці кроки виконували **кілька процесів**, вони **дублювали** б роботу, запускаючи її **паралельно**. А якщо ці кроки делікатні, як-от міграція бази даних, це може спричинити конфлікти між ними.
|
||||
|
||||
Звісно, є випадки, коли багаторазове виконання попередніх кроків не є проблемою — тоді це значно простіше.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Також майте на увазі, що залежно від вашого налаштування інколи вам **можуть взагалі не знадобитися жодні попередні кроки** перед запуском застосунку.
|
||||
|
||||
У такому разі вам не доведеться турбуватися про все це. 🤷
|
||||
|
||||
///
|
||||
|
||||
### Приклади стратегій для попередніх кроків { #examples-of-previous-steps-strategies }
|
||||
|
||||
Це **сильно залежить** від того, як саме ви **розгортаєте систему**, і, ймовірно, буде пов’язано зі способом запуску програм, обробкою перезапусків тощо.
|
||||
|
||||
Ось кілька можливих ідей:
|
||||
|
||||
* «Init Container» у Kubernetes, який запускається перед контейнером застосунку
|
||||
* Bash-скрипт, який виконує попередні кроки, а потім запускає застосунок
|
||||
* Вам все одно потрібен спосіб запускати/перезапускати *цей* bash-скрипт, виявляти помилки тощо.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Більш конкретні приклади для контейнерів я наведу в майбутньому розділі: [FastAPI у контейнерах — Docker](docker.md){.internal-link target=_blank}.
|
||||
|
||||
///
|
||||
|
||||
## Використання ресурсів { #resource-utilization }
|
||||
|
||||
Ваш(і) сервер(и) — це **ресурс**, який ваші програми можуть споживати або **використовувати**: обчислювальний час CPU та доступну RAM.
|
||||
|
||||
Скільки системних ресурсів ви хочете споживати/використовувати? Легко подумати: «небагато», але на практиці ви, ймовірно, захочете використовувати **якомога більше, не доводячи до падіння**.
|
||||
|
||||
Якщо ви платите за 3 сервери, але використовуєте лише малу частину їхньої RAM і CPU, ви, ймовірно, **марнуєте гроші** 💸 і, ймовірно, **марнуєте електроенергію серверів** 🌎 тощо.
|
||||
|
||||
У такому разі краще мати лише 2 сервери й використовувати більший відсоток їхніх ресурсів (CPU, пам’ять, диск, пропускну здатність мережі тощо).
|
||||
|
||||
З іншого боку, якщо у вас 2 сервери й ви використовуєте **100% їхнього CPU та RAM**, у якийсь момент один процес попросить більше пам’яті, і серверу доведеться використовувати диск як «пам’ять» (що може бути в тисячі разів повільніше), або навіть **впасти**. Або процесу може знадобитися виконати обчислення, і йому доведеться чекати, доки CPU знову стане доступним.
|
||||
|
||||
У такому випадку краще додати **ще один сервер** і запустити на ньому частину процесів, щоб у всіх було **достатньо RAM і часу CPU**.
|
||||
|
||||
Також є шанс, що з певної причини станеться **сплеск** використання вашого API. Можливо, воно стало вірусним, або якісь інші сервіси чи боти почали активно його використовувати. І в таких випадках варто мати запас ресурсів.
|
||||
|
||||
Ви можете встановити **довільну ціль**, наприклад десь **між 50% і 90%** використання ресурсів. Суть у тому, що це, ймовірно, основні показники, які ви захочете вимірювати та використовувати для налаштування ваших розгортань.
|
||||
|
||||
Ви можете використовувати прості інструменти на кшталт `htop`, щоб бачити використання CPU і RAM на сервері або обсяг, який використовує кожен процес. Або можете використати складніші інструменти моніторингу, які можуть бути розподілені між серверами тощо.
|
||||
|
||||
## Підсумок { #recap }
|
||||
|
||||
Тут ви ознайомилися з основними концепціями, які, ймовірно, потрібно тримати в голові, коли ви вирішуєте, як розгорнути ваш застосунок:
|
||||
|
||||
* Безпека — HTTPS
|
||||
* Запуск під час старту
|
||||
* Перезапуски
|
||||
* Реплікація (кількість запущених процесів)
|
||||
* Пам’ять
|
||||
* Попередні кроки перед запуском
|
||||
|
||||
Розуміння цих ідей і вміння застосовувати їх має дати вам потрібну інтуїцію для ухвалення рішень під час налаштування й тонкого підкручування ваших розгортань. 🤓
|
||||
|
||||
У наступних розділах я наведу більш конкретні приклади можливих стратегій, яких ви можете дотримуватися. 🚀
|
||||
620
docs/uk/docs/deployment/docker.md
Normal file
620
docs/uk/docs/deployment/docker.md
Normal file
@@ -0,0 +1,620 @@
|
||||
# FastAPI у контейнерах — Docker { #fastapi-in-containers-docker }
|
||||
|
||||
Під час розгортання застосунків FastAPI поширеним підходом є збирання **образу Linux-контейнера**. Зазвичай це роблять за допомогою <a href="https://www.docker.com/" class="external-link" target="_blank">**Docker**</a>. Далі ви можете розгорнути цей образ контейнера одним із кількох можливих способів.
|
||||
|
||||
Використання Linux-контейнерів має кілька переваг, зокрема **безпеку**, **відтворюваність**, **простоту** та інші.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Поспішаєте й уже це знаєте? Перейдіть до [`Dockerfile` нижче 👇](#build-a-docker-image-for-fastapi).
|
||||
|
||||
///
|
||||
|
||||
<details>
|
||||
<summary>Попередній перегляд Dockerfile 👀</summary>
|
||||
|
||||
```Dockerfile
|
||||
FROM python:3.9
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
COPY ./requirements.txt /code/requirements.txt
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||
|
||||
COPY ./app /code/app
|
||||
|
||||
CMD ["fastapi", "run", "app/main.py", "--port", "80"]
|
||||
|
||||
# If running behind a proxy like Nginx or Traefik add --proxy-headers
|
||||
# CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Що таке контейнер { #what-is-a-container }
|
||||
|
||||
Контейнери (переважно Linux-контейнери) — це дуже **легкий** спосіб пакування застосунків разом із усіма їхніми залежностями та потрібними файлами, зберігаючи при цьому ізоляцію від інших контейнерів (інших застосунків або компонентів) у тій самій системі.
|
||||
|
||||
Linux-контейнери працюють, використовуючи те саме Linux-ядро, що й хост (машина, віртуальна машина, хмарний сервер тощо). Це означає, що вони дуже легкі (порівняно з повноцінними віртуальними машинами, які емулюють цілу операційну систему).
|
||||
|
||||
У такий спосіб контейнери споживають **мало ресурсів** — приблизно як під час запуску процесів безпосередньо (віртуальна машина споживала б значно більше).
|
||||
|
||||
Контейнери також мають власні **ізольовані** процеси виконання (зазвичай лише один процес), файлову систему та мережу, що спрощує розгортання, безпеку, розробку тощо.
|
||||
|
||||
## Що таке образ контейнера { #what-is-a-container-image }
|
||||
|
||||
**Контейнер** запускається з **образу контейнера**.
|
||||
|
||||
Образ контейнера — це **статична** версія всіх файлів, змінних середовища та команди/програми за замовчуванням, які мають бути в контейнері. **Статична** тут означає, що **образ** контейнера не запущений, не виконується — це лише запаковані файли та метадані.
|
||||
|
||||
На відміну від «**образу контейнера**», який є збереженим статичним вмістом, «**контейнер**» зазвичай означає запущений екземпляр — те, що **виконується**.
|
||||
|
||||
Коли **контейнер** стартує і працює (запускається з **образу контейнера**), він може створювати або змінювати файли, змінні середовища тощо. Ці зміни існуватимуть лише в цьому контейнері, але не збережуться в базовому образі контейнера (не будуть записані на диск).
|
||||
|
||||
Образ контейнера можна порівняти з файлом **програми** та її вмістом, наприклад `python` і деякий файл `main.py`.
|
||||
|
||||
А сам **контейнер** (на відміну від **образу контейнера**) — це фактичний запущений екземпляр образу, подібний до **процесу**. Насправді контейнер працює лише тоді, коли в ньому **виконується процес** (і зазвичай це один процес). Контейнер зупиняється, коли в ньому немає запущеного процесу.
|
||||
|
||||
## Образи контейнерів { #container-images }
|
||||
|
||||
Docker був одним з основних інструментів для створення та керування **образами контейнерів** і **контейнерами**.
|
||||
|
||||
Також існує публічний <a href="https://hub.docker.com/" class="external-link" target="_blank">Docker Hub</a> із заздалегідь підготовленими **офіційними образами контейнерів** для багатьох інструментів, середовищ, баз даних і застосунків.
|
||||
|
||||
Наприклад, є офіційний <a href="https://hub.docker.com/_/python" class="external-link" target="_blank">образ Python</a>.
|
||||
|
||||
Є й багато інших образів для різних речей, наприклад для баз даних:
|
||||
|
||||
* <a href="https://hub.docker.com/_/postgres" class="external-link" target="_blank">PostgreSQL</a>
|
||||
* <a href="https://hub.docker.com/_/mysql" class="external-link" target="_blank">MySQL</a>
|
||||
* <a href="https://hub.docker.com/_/mongo" class="external-link" target="_blank">MongoDB</a>
|
||||
* <a href="https://hub.docker.com/_/redis" class="external-link" target="_blank">Redis</a> тощо.
|
||||
|
||||
Використовуючи готовий образ контейнера, дуже легко **поєднувати** й застосовувати різні інструменти. Наприклад, щоб спробувати нову базу даних. У більшості випадків ви можете використати **офіційні образи** і просто налаштувати їх через змінні середовища.
|
||||
|
||||
Так у багатьох випадках ви можете вивчити контейнери й Docker та повторно застосовувати ці знання з багатьма різними інструментами й компонентами.
|
||||
|
||||
Отже, ви запускатимете **кілька контейнерів** з різними складовими — наприклад, базою даних, Python-застосунком, вебсервером із фронтендом на React — і поєднуватимете їх через внутрішню мережу.
|
||||
|
||||
Усі системи керування контейнерами (як-от Docker або Kubernetes) мають ці мережеві можливості вбудованими.
|
||||
|
||||
## Контейнери та процеси { #containers-and-processes }
|
||||
|
||||
**Образ контейнера** зазвичай містить у метаданих програму або команду за замовчуванням, яку потрібно запустити, коли стартує **контейнер**, а також параметри, які треба передати цій програмі. Дуже схоже на те, як це виглядало б у командному рядку.
|
||||
|
||||
Коли **контейнер** запускається, він виконає цю команду/програму (хоча ви можете перевизначити її і змусити запускати іншу команду/програму).
|
||||
|
||||
Контейнер працює доти, доки працює **головний процес** (команда або програма).
|
||||
|
||||
Контейнер зазвичай має **один процес**, але також можливо запускати підпроцеси з головного процесу — і тоді в одному контейнері буде **кілька процесів**.
|
||||
|
||||
Але неможливо мати запущений контейнер без **принаймні одного запущеного процесу**. Якщо головний процес зупиняється — контейнер зупиняється.
|
||||
|
||||
## Зібрати Docker-образ для FastAPI { #build-a-docker-image-for-fastapi }
|
||||
|
||||
Гаразд, тепер зберімо щось! 🚀
|
||||
|
||||
Я покажу, як створити **Docker-образ** для FastAPI **з нуля**, базуючись на **офіційному образі Python**.
|
||||
|
||||
Саме так ви захочете робити в **більшості випадків**, наприклад:
|
||||
|
||||
* використовуючи **Kubernetes** або подібні інструменти;
|
||||
* під час запуску на **Raspberry Pi**;
|
||||
* використовуючи хмарний сервіс, який запускатиме образ контейнера за вас, тощо.
|
||||
|
||||
### Вимоги до пакетів { #package-requirements }
|
||||
|
||||
Зазвичай ви зберігатимете **вимоги до пакетів** вашого застосунку в якомусь файлі.
|
||||
|
||||
Це здебільшого залежатиме від інструмента, який ви використовуєте, щоб **встановити** ці вимоги.
|
||||
|
||||
Найпоширеніший спосіб — файл `requirements.txt` з назвами пакетів і їхніми версіями, по одному в рядку.
|
||||
|
||||
Звісно, ви використаєте ті самі ідеї, про які читали в [Про версії FastAPI](versions.md){.internal-link target=_blank}, щоб задавати діапазони версій.
|
||||
|
||||
Наприклад, ваш `requirements.txt` може виглядати так:
|
||||
|
||||
```
|
||||
fastapi[standard]>=0.113.0,<0.114.0
|
||||
pydantic>=2.7.0,<3.0.0
|
||||
```
|
||||
|
||||
Зазвичай ви встановлюватимете ці залежності пакетів за допомогою `pip`, наприклад:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ pip install -r requirements.txt
|
||||
---> 100%
|
||||
Successfully installed fastapi pydantic
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Існують інші формати й інструменти для визначення та встановлення залежностей пакетів.
|
||||
|
||||
///
|
||||
|
||||
### Створити код **FastAPI** { #create-the-fastapi-code }
|
||||
|
||||
* Створіть директорію `app` і перейдіть у неї.
|
||||
* Створіть порожній файл `__init__.py`.
|
||||
* Створіть файл `main.py` із таким вмістом:
|
||||
|
||||
```Python
|
||||
from typing import Union
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def read_root():
|
||||
return {"Hello": "World"}
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
def read_item(item_id: int, q: Union[str, None] = None):
|
||||
return {"item_id": item_id, "q": q}
|
||||
```
|
||||
|
||||
### Dockerfile { #dockerfile }
|
||||
|
||||
Тепер у тій самій директорії проєкту створіть файл `Dockerfile` з таким вмістом:
|
||||
|
||||
```{ .dockerfile .annotate }
|
||||
# (1)!
|
||||
FROM python:3.9
|
||||
|
||||
# (2)!
|
||||
WORKDIR /code
|
||||
|
||||
# (3)!
|
||||
COPY ./requirements.txt /code/requirements.txt
|
||||
|
||||
# (4)!
|
||||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||
|
||||
# (5)!
|
||||
COPY ./app /code/app
|
||||
|
||||
# (6)!
|
||||
CMD ["fastapi", "run", "app/main.py", "--port", "80"]
|
||||
```
|
||||
|
||||
1. Почніть з офіційного базового образу Python.
|
||||
|
||||
2. Встановіть поточну робочу директорію на `/code`.
|
||||
|
||||
Саме тут ми розмістимо файл `requirements.txt` і директорію `app`.
|
||||
|
||||
3. Скопіюйте файл з вимогами до директорії `/code`.
|
||||
|
||||
Спочатку копіюйте **лише** файл із вимогами, а не решту коду.
|
||||
|
||||
Оскільки цей файл **не змінюється часто**, Docker виявить це й використає **кеш** для цього кроку, що також увімкне кеш для наступного кроку.
|
||||
|
||||
4. Встановіть залежності пакетів із файлу вимог.
|
||||
|
||||
Опція `--no-cache-dir` вказує `pip` не зберігати завантажені пакети локально, оскільки це має сенс лише тоді, коли `pip` планують запускати знову для встановлення тих самих пакетів — але під час роботи з контейнерами це не так.
|
||||
|
||||
/// note | Примітка
|
||||
|
||||
`--no-cache-dir` стосується лише `pip` і не має жодного відношення до Docker або контейнерів.
|
||||
|
||||
///
|
||||
|
||||
Опція `--upgrade` вказує `pip` оновити пакети, якщо вони вже встановлені.
|
||||
|
||||
Оскільки попередній крок копіювання файлу може бути розпізнаний **Docker cache**, цей крок також **використає Docker cache**, коли він доступний.
|
||||
|
||||
Використання кешу на цьому кроці **заощадить** вам багато **часу** під час повторного збирання образу знову і знову в процесі розробки — замість **завантаження та встановлення** всіх залежностей **кожного разу**.
|
||||
|
||||
5. Скопіюйте директорію `./app` всередину директорії `/code`.
|
||||
|
||||
Оскільки тут міститься весь код, який **змінюється найчастіше**, **Docker cache** для цього або будь-яких **наступних кроків** не використовуватиметься легко.
|
||||
|
||||
Тому важливо розмістити це **ближче до кінця** `Dockerfile`, щоб оптимізувати час збирання образу контейнера.
|
||||
|
||||
6. Встановіть **команду** запуску `fastapi run`, яка під капотом використовує Uvicorn.
|
||||
|
||||
`CMD` приймає список рядків; кожен рядок — це те, що ви ввели б у командному рядку, розділяючи аргументи пробілами.
|
||||
|
||||
Ця команда буде виконана з **поточної робочої директорії** — тієї самої `/code`, яку ви встановили вище через `WORKDIR /code`.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Перегляньте, що робить кожен рядок, натискаючи на кожну бульбашку з номером у коді. 👆
|
||||
|
||||
///
|
||||
|
||||
/// warning | Попередження
|
||||
|
||||
Переконайтеся, що **завжди** використовуєте **exec form** інструкції `CMD`, як пояснено нижче.
|
||||
|
||||
///
|
||||
|
||||
#### Використання `CMD` — exec form { #use-cmd-exec-form }
|
||||
|
||||
Docker-інструкцію <a href="https://docs.docker.com/reference/dockerfile/#cmd" class="external-link" target="_blank">`CMD`</a> можна записувати у двох формах:
|
||||
|
||||
✅ **Exec** form:
|
||||
|
||||
```Dockerfile
|
||||
# ✅ Do this
|
||||
CMD ["fastapi", "run", "app/main.py", "--port", "80"]
|
||||
```
|
||||
|
||||
⛔️ **Shell** form:
|
||||
|
||||
```Dockerfile
|
||||
# ⛔️ Don't do this
|
||||
CMD fastapi run app/main.py --port 80
|
||||
```
|
||||
|
||||
Завжди використовуйте **exec** form, щоб гарантувати коректне завершення роботи FastAPI та запуск [lifespan events](../advanced/events.md){.internal-link target=_blank}.
|
||||
|
||||
Детальніше читайте в <a href="https://docs.docker.com/reference/dockerfile/#shell-and-exec-form" class="external-link" target="_blank">документації Docker про shell і exec form</a>.
|
||||
|
||||
Це може бути особливо помітно під час використання `docker compose`. Дивіться цей розділ FAQ Docker Compose з технічними деталями: <a href="https://docs.docker.com/compose/faq/#why-do-my-services-take-10-seconds-to-recreate-or-stop" class="external-link" target="_blank">Why do my services take 10 seconds to recreate or stop?</a>.
|
||||
|
||||
#### Структура директорій { #directory-structure }
|
||||
|
||||
Тепер у вас має бути така структура директорій:
|
||||
|
||||
```
|
||||
.
|
||||
├── app
|
||||
│ ├── __init__.py
|
||||
│ └── main.py
|
||||
├── Dockerfile
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
#### За TLS termination proxy { #behind-a-tls-termination-proxy }
|
||||
|
||||
Якщо ви запускаєте контейнер за TLS Termination Proxy (балансувальником навантаження), таким як Nginx або Traefik, додайте опцію `--proxy-headers`. Вона скаже Uvicorn (через FastAPI CLI) довіряти заголовкам, які надсилає цей проксі, і які повідомляють, що застосунок працює за HTTPS тощо.
|
||||
|
||||
```Dockerfile
|
||||
CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"]
|
||||
```
|
||||
|
||||
#### Docker cache { #docker-cache }
|
||||
|
||||
У цьому `Dockerfile` є важливий прийом: спочатку ми копіюємо **лише файл із залежностями**, а не решту коду. Поясню, навіщо це.
|
||||
|
||||
```Dockerfile
|
||||
COPY ./requirements.txt /code/requirements.txt
|
||||
```
|
||||
|
||||
Docker та інші інструменти **збирають** ці образи контейнерів **інкрементально**, додаючи **шар за шаром**, починаючи з верхньої частини `Dockerfile` і додаючи файли, створені кожною інструкцією `Dockerfile`.
|
||||
|
||||
Docker і подібні інструменти також використовують **внутрішній кеш** під час збирання образу: якщо файл не змінився з моменту останнього збирання образу контейнера, він **повторно використає той самий шар**, створений минулого разу, замість того щоб копіювати файл знову й створювати новий шар з нуля.
|
||||
|
||||
Просто уникнення копіювання файлів не обов’язково сильно покращує ситуацію, але оскільки на цьому кроці використовується кеш, він може **використати кеш і на наступному кроці**. Наприклад, він може використати кеш для інструкції встановлення залежностей:
|
||||
|
||||
```Dockerfile
|
||||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||
```
|
||||
|
||||
Файл із вимогами до пакетів **не змінюватиметься часто**. Тож, копіюючи лише цей файл, Docker зможе **використати кеш** на цьому кроці.
|
||||
|
||||
І тоді Docker зможе **використати кеш для наступного кроку**, який завантажує та встановлює ці залежності. Саме тут ми **економимо багато часу**. ✨ ...і уникаємо нудного очікування. 😪😆
|
||||
|
||||
Завантаження й встановлення залежностей **може тривати хвилини**, але використання **кешу** займатиме **максимум секунди**.
|
||||
|
||||
А оскільки під час розробки ви збиратимете образ контейнера знову і знову, щоб перевіряти, чи працюють зміни в коді, сукупно це заощадить чимало часу.
|
||||
|
||||
Далі, ближче до кінця `Dockerfile`, ми копіюємо весь код. Оскільки саме він **змінюється найчастіше**, ми розміщуємо його наприкінці, бо майже завжди все після цього кроку вже не зможе використовувати кеш.
|
||||
|
||||
```Dockerfile
|
||||
COPY ./app /code/app
|
||||
```
|
||||
|
||||
### Зібрати Docker-образ { #build-the-docker-image }
|
||||
|
||||
Тепер, коли всі файли на місці, зберімо образ контейнера.
|
||||
|
||||
* Перейдіть у директорію проєкту (де знаходиться ваш `Dockerfile`, а також директорія `app`).
|
||||
* Зберіть ваш FastAPI-образ:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ docker build -t myimage .
|
||||
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Зверніть увагу на `.` в кінці — це еквівалент `./`. Він вказує Docker, яку директорію використовувати для збирання образу контейнера.
|
||||
|
||||
У цьому випадку це поточна директорія (`.`).
|
||||
|
||||
///
|
||||
|
||||
### Запустити Docker-контейнер { #start-the-docker-container }
|
||||
|
||||
* Запустіть контейнер на основі вашого образу:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ docker run -d --name mycontainer -p 80:80 myimage
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Перевірка { #check-it }
|
||||
|
||||
Ви маєте змогу перевірити це за URL вашого Docker-контейнера, наприклад: <a href="http://192.168.99.100/items/5?q=somequery" class="external-link" target="_blank">http://192.168.99.100/items/5?q=somequery</a> або <a href="http://127.0.0.1/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1/items/5?q=somequery</a> (або еквівалентно, залежно від вашого Docker-хоста).
|
||||
|
||||
Ви побачите щось на кшталт:
|
||||
|
||||
```JSON
|
||||
{"item_id": 5, "q": "somequery"}
|
||||
```
|
||||
|
||||
## Інтерактивна документація API { #interactive-api-docs }
|
||||
|
||||
Тепер ви можете перейти на <a href="http://192.168.99.100/docs" class="external-link" target="_blank">http://192.168.99.100/docs</a> або <a href="http://127.0.0.1/docs" class="external-link" target="_blank">http://127.0.0.1/docs</a> (або еквівалентно, залежно від вашого Docker-хоста).
|
||||
|
||||
Ви побачите автоматичну інтерактивну документацію API (надається <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>):
|
||||
|
||||

|
||||
|
||||
## Альтернативна документація API { #alternative-api-docs }
|
||||
|
||||
Також ви можете перейти на <a href="http://192.168.99.100/redoc" class="external-link" target="_blank">http://192.168.99.100/redoc</a> або <a href="http://127.0.0.1/redoc" class="external-link" target="_blank">http://127.0.0.1/redoc</a> (або еквівалентно, залежно від вашого Docker-хоста).
|
||||
|
||||
Ви побачите альтернативну автоматичну документацію (надається <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>):
|
||||
|
||||

|
||||
|
||||
## Зібрати Docker-образ для однофайлового FastAPI { #build-a-docker-image-with-a-single-file-fastapi }
|
||||
|
||||
Якщо ваш FastAPI — це один файл, наприклад `main.py` без директорії `./app`, структура файлів може виглядати так:
|
||||
|
||||
```
|
||||
.
|
||||
├── Dockerfile
|
||||
├── main.py
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
Тоді вам потрібно лише змінити відповідні шляхи, щоб скопіювати файл у `Dockerfile`:
|
||||
|
||||
```{ .dockerfile .annotate hl_lines="10 13" }
|
||||
FROM python:3.9
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
COPY ./requirements.txt /code/requirements.txt
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||
|
||||
# (1)!
|
||||
COPY ./main.py /code/
|
||||
|
||||
# (2)!
|
||||
CMD ["fastapi", "run", "main.py", "--port", "80"]
|
||||
```
|
||||
|
||||
1. Скопіюйте файл `main.py` безпосередньо в директорію `/code` (без директорії `./app`).
|
||||
|
||||
2. Використайте `fastapi run`, щоб віддавати ваш застосунок із єдиного файла `main.py`.
|
||||
|
||||
Коли ви передаєте файл до `fastapi run`, він автоматично визначить, що це одиночний файл, а не частина пакета, і знатиме, як його імпортувати та віддавати ваш FastAPI-застосунок. 😎
|
||||
|
||||
## Концепції розгортання { #deployment-concepts }
|
||||
|
||||
Повернімося до деяких із тих самих [концепцій розгортання](concepts.md){.internal-link target=_blank}, але в контексті контейнерів.
|
||||
|
||||
Контейнери — це насамперед інструмент для спрощення процесу **збирання та розгортання** застосунку, але вони не нав’язують конкретний підхід до реалізації цих **концепцій розгортання**, і існує кілька можливих стратегій.
|
||||
|
||||
**Гарна новина** в тому, що для кожної стратегії є спосіб покрити всі концепції розгортання. 🎉
|
||||
|
||||
Розгляньмо ці **концепції розгортання** у термінах контейнерів:
|
||||
|
||||
* HTTPS
|
||||
* Запуск під час старту
|
||||
* Перезапуски
|
||||
* Реплікація (кількість запущених процесів)
|
||||
* Пам’ять
|
||||
* Попередні кроки перед запуском
|
||||
|
||||
## HTTPS { #https }
|
||||
|
||||
Якщо зосередитися лише на **образі контейнера** для застосунку FastAPI (а потім на запущеному **контейнері**), то HTTPS зазвичай обробляється **зовні** іншим інструментом.
|
||||
|
||||
Це може бути інший контейнер, наприклад із <a href="https://traefik.io/" class="external-link" target="_blank">Traefik</a>, який обробляє **HTTPS** та **автоматичне** отримання **сертифікатів**.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Traefik має інтеграції з Docker, Kubernetes та іншими, тож його дуже легко налаштувати для HTTPS ваших контейнерів.
|
||||
|
||||
///
|
||||
|
||||
Альтернативно, HTTPS може бути реалізований хмарним провайдером як одна з його послуг (при цьому сам застосунок все одно працюватиме в контейнері).
|
||||
|
||||
## Запуск під час старту та перезапуски { #running-on-startup-and-restarts }
|
||||
|
||||
Зазвичай є інший інструмент, відповідальний за **запуск і виконання** вашого контейнера.
|
||||
|
||||
Це може бути безпосередньо **Docker**, **Docker Compose**, **Kubernetes**, **хмарний сервіс** тощо.
|
||||
|
||||
У більшості (або всіх) випадків є проста опція, щоб увімкнути запуск контейнера під час старту системи та перезапуски у разі збоїв. Наприклад, у Docker це опція командного рядка `--restart`.
|
||||
|
||||
Без контейнерів забезпечити запуск застосунків під час старту системи та їхні перезапуски може бути незручно й складно. Але під час **роботи з контейнерами** у більшості випадків ця функціональність доступна за замовчуванням. ✨
|
||||
|
||||
## Реплікація — кількість процесів { #replication-number-of-processes }
|
||||
|
||||
Якщо у вас є <abbr title="A group of machines that are configured to be connected and work together in some way.">cluster</abbr> машин із **Kubernetes**, Docker Swarm Mode, Nomad або іншою подібною складною системою керування розподіленими контейнерами на багатьох машинах, тоді вам, імовірно, варто **керувати реплікацією** на **рівні кластера**, замість використання **менеджера процесів** (як-от Uvicorn із workers) у кожному контейнері.
|
||||
|
||||
Одна з таких систем керування розподіленими контейнерами, як Kubernetes, зазвичай має вбудований спосіб обробки **реплікації контейнерів** із підтримкою **балансування навантаження** для вхідних запитів — усе на **рівні кластера**.
|
||||
|
||||
У таких випадках вам, імовірно, варто зібрати **Docker-образ з нуля**, як [пояснено вище](#dockerfile): встановити залежності та запускати **один процес Uvicorn**, замість кількох Uvicorn workers.
|
||||
|
||||
### Балансувальник навантаження { #load-balancer }
|
||||
|
||||
Під час роботи з контейнерами зазвичай є компонент, який **слухає основний порт**. Це може бути інший контейнер, який також є **TLS Termination Proxy** для обробки **HTTPS**, або подібний інструмент.
|
||||
|
||||
Оскільки цей компонент приймає **навантаження** запитів і розподіляє його між воркерами більш-менш **збалансовано**, його також часто називають **Load Balancer**.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Той самий компонент **TLS Termination Proxy**, що використовується для HTTPS, імовірно, також буде **Load Balancer**.
|
||||
|
||||
///
|
||||
|
||||
А під час роботи з контейнерами та система, яку ви використовуєте для запуску й керування ними, вже матиме внутрішні інструменти для передачі **мережевої взаємодії** (наприклад, HTTP-запитів) від цього **load balancer** (який також може бути **TLS Termination Proxy**) до контейнера(ів) із вашим застосунком.
|
||||
|
||||
### Один балансувальник — кілька worker-контейнерів { #one-load-balancer-multiple-worker-containers }
|
||||
|
||||
Під час роботи з **Kubernetes** або подібними розподіленими системами керування контейнерами їхні внутрішні мережеві механізми дозволяють одному **load balancer**, який слухає основний **порт**, передавати взаємодію (запити) до потенційно **кількох контейнерів** із вашим застосунком.
|
||||
|
||||
Кожен із цих контейнерів зазвичай матиме **лише один процес** (наприклад, процес Uvicorn, який запускає ваш застосунок FastAPI). Усі вони будуть **ідентичними контейнерами**, запускатимуть те саме, але кожен матиме свій процес, пам’ять тощо. Так ви використаєте **паралелізацію** на **різних ядрах** CPU або навіть на **різних машинах**.
|
||||
|
||||
І розподілена контейнерна система з **load balancer** буде **розподіляти запити** до кожного з контейнерів із вашим застосунком **по черзі**. Тож кожен запит може бути оброблений одним із кількох **реплікованих контейнерів**, що запускають ваш застосунок.
|
||||
|
||||
І зазвичай цей **load balancer** зможе обробляти запити, які спрямовані до *інших* застосунків у вашому кластері (наприклад, на інший домен або під іншим префіксом шляху URL), і передаватиме взаємодію до правильних контейнерів для *того іншого* застосунку, що працює в кластері.
|
||||
|
||||
### Один процес на контейнер { #one-process-per-container }
|
||||
|
||||
У такому сценарії, імовірно, варто мати **один (Uvicorn) процес на контейнер**, адже реплікацію ви вже обробляєте на рівні кластера.
|
||||
|
||||
Отже, у цьому випадку вам **не** потрібно запускати кілька workers у контейнері, наприклад через опцію командного рядка `--workers`. Вам потрібен лише **один процес Uvicorn** на контейнер (але, ймовірно, багато контейнерів).
|
||||
|
||||
Додавати всередину контейнера ще один менеджер процесів (як це було б із кількома workers) — це зайва **непотрібна складність**, яку, скоріш за все, вже вирішує ваша кластерна система.
|
||||
|
||||
### Контейнери з кількома процесами та особливі випадки { #containers-with-multiple-processes-and-special-cases }
|
||||
|
||||
Звісно, є **особливі випадки**, коли вам може знадобитися **контейнер** із кількома **процесами воркерів Uvicorn** всередині.
|
||||
|
||||
У таких випадках ви можете використати опцію командного рядка `--workers`, щоб задати кількість воркерів, яких хочете запустити:
|
||||
|
||||
```{ .dockerfile .annotate }
|
||||
FROM python:3.9
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
COPY ./requirements.txt /code/requirements.txt
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||
|
||||
COPY ./app /code/app
|
||||
|
||||
# (1)!
|
||||
CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"]
|
||||
```
|
||||
|
||||
1. Тут ми використовуємо опцію командного рядка `--workers`, щоб встановити кількість воркерів 4.
|
||||
|
||||
Ось кілька прикладів, коли це може мати сенс:
|
||||
|
||||
#### Простий застосунок { #a-simple-app }
|
||||
|
||||
Вам може знадобитися менеджер процесів у контейнері, якщо ваш застосунок **достатньо простий**, щоб запускати його на **одному сервері**, а не в кластері.
|
||||
|
||||
#### Docker Compose { #docker-compose }
|
||||
|
||||
Ви можете розгортати на **одному сервері** (не в кластері) з **Docker Compose**, тож у вас не буде простого способу керувати реплікацією контейнерів (через Docker Compose), зберігаючи спільну мережу та **балансування навантаження**.
|
||||
|
||||
Тоді вам може знадобитися **один контейнер** із **менеджером процесів**, який запускатиме **кілька процесів воркерів** всередині.
|
||||
|
||||
---
|
||||
|
||||
Головна думка: **жодне** з цього не є **висіченими в камені правилами**, яких ви маєте сліпо дотримуватися. Використовуйте ці ідеї, щоб **оцінити свій власний сценарій** і вирішити, який підхід найкращий для вашої системи, розглянувши, як керувати концепціями:
|
||||
|
||||
* Безпека — HTTPS
|
||||
* Запуск під час старту
|
||||
* Перезапуски
|
||||
* Реплікація (кількість запущених процесів)
|
||||
* Пам’ять
|
||||
* Попередні кроки перед запуском
|
||||
|
||||
## Пам’ять { #memory }
|
||||
|
||||
Якщо ви запускаєте **один процес на контейнер**, ви матимете більш-менш визначений, стабільний і обмежений обсяг пам’яті, який споживає кожен контейнер (або більше, якщо вони репліковані).
|
||||
|
||||
І тоді ви можете встановити відповідні обмеження й вимоги до пам’яті у конфігураціях вашої системи керування контейнерами (наприклад, у **Kubernetes**). Так вона зможе **реплікувати контейнери** на **доступних машинах**, враховуючи потрібний обсяг пам’яті та доступний обсяг у машинах кластера.
|
||||
|
||||
Якщо ваш застосунок **простий**, це, імовірно, **не буде проблемою**, і вам може не знадобитися задавати жорсткі ліміти пам’яті. Але якщо ви **використовуєте багато пам’яті** (наприклад, з моделями **machine learning**), варто перевірити, скільки пам’яті ви споживаєте, і скоригувати **кількість контейнерів**, які запускаються **на кожній машині** (і, можливо, додати більше машин у кластер).
|
||||
|
||||
Якщо ви запускаєте **кілька процесів на контейнер**, вам потрібно переконатися, що кількість запущених процесів не **споживає більше пам’яті**, ніж доступно.
|
||||
|
||||
## Попередні кроки перед запуском і контейнери { #previous-steps-before-starting-and-containers }
|
||||
|
||||
Якщо ви використовуєте контейнери (наприклад Docker, Kubernetes), є два основні підходи, які ви можете застосувати.
|
||||
|
||||
### Кілька контейнерів { #multiple-containers }
|
||||
|
||||
Якщо у вас **кілька контейнерів**, імовірно, кожен запускає **один процес** (наприклад у кластері **Kubernetes**), тоді вам, імовірно, варто мати **окремий контейнер**, який виконує **попередні кроки** як один контейнер з одним процесом **перед** запуском реплікованих worker-контейнерів.
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Якщо ви використовуєте Kubernetes, це, імовірно, буде <a href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/" class="external-link" target="_blank">Init Container</a>.
|
||||
|
||||
///
|
||||
|
||||
Якщо у вашому сценарії немає проблеми запускати ці попередні кроки **паралельно кілька разів** (наприклад, якщо ви не виконуєте міграції бази даних, а лише перевіряєте, чи база даних уже готова), тоді ви можете просто додати їх у кожен контейнер безпосередньо перед стартом головного процесу.
|
||||
|
||||
### Один контейнер { #single-container }
|
||||
|
||||
Якщо у вас просте налаштування з **одним контейнером**, який потім запускає кілька **процесів воркерів** (або також лише один процес), ви можете виконати ці попередні кроки в тому самому контейнері прямо перед запуском процесу із застосунком.
|
||||
|
||||
### Базовий Docker-образ { #base-docker-image }
|
||||
|
||||
Раніше існував офіційний Docker-образ FastAPI: <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" class="external-link" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>. Але тепер він застарілий (deprecated). ⛔️
|
||||
|
||||
Імовірно, вам **не** варто використовувати цей базовий Docker-образ (або будь-який інший подібний).
|
||||
|
||||
Якщо ви використовуєте **Kubernetes** (або інші) і вже задаєте **реплікацію** на рівні кластера з кількома **контейнерами**, то в таких випадках краще **зібрати образ з нуля**, як описано вище: [Зібрати Docker-образ для FastAPI](#build-a-docker-image-for-fastapi).
|
||||
|
||||
А якщо вам потрібно кілька workers, ви можете просто використати опцію командного рядка `--workers`.
|
||||
|
||||
/// note | Технічні деталі
|
||||
|
||||
Docker-образ було створено тоді, коли Uvicorn не підтримував керування та перезапуск «мертвих» worker-процесів, тож потрібно було використовувати Gunicorn разом із Uvicorn. Це додавало чимало складності лише для того, щоб Gunicorn керував і перезапускав worker-процеси Uvicorn.
|
||||
|
||||
Але тепер, коли Uvicorn (і команда `fastapi`) підтримують `--workers`, немає причин використовувати базовий Docker-образ замість створення власного (це майже той самий обсяг коду 😅).
|
||||
|
||||
///
|
||||
|
||||
## Розгорнути образ контейнера { #deploy-the-container-image }
|
||||
|
||||
Після того як у вас є образ контейнера (Docker), є кілька способів його розгорнути.
|
||||
|
||||
Наприклад:
|
||||
|
||||
* за допомогою **Docker Compose** на одному сервері;
|
||||
* у кластері **Kubernetes**;
|
||||
* у кластері Docker Swarm Mode;
|
||||
* іншим інструментом, як-от Nomad;
|
||||
* у хмарному сервісі, який бере ваш образ контейнера і розгортає його.
|
||||
|
||||
## Docker-образ з `uv` { #docker-image-with-uv }
|
||||
|
||||
Якщо ви використовуєте <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a> для встановлення та керування вашим проєктом, ви можете дотримуватися їхнього <a href="https://docs.astral.sh/uv/guides/integration/docker/" class="external-link" target="_blank">uv Docker guide</a>.
|
||||
|
||||
## Підсумок { #recap }
|
||||
|
||||
Використовуючи контейнерні системи (наприклад, **Docker** і **Kubernetes**), стає доволі просто реалізувати всі **концепції розгортання**:
|
||||
|
||||
* HTTPS
|
||||
* Запуск під час старту
|
||||
* Перезапуски
|
||||
* Реплікація (кількість запущених процесів)
|
||||
* Пам’ять
|
||||
* Попередні кроки перед запуском
|
||||
|
||||
У більшості випадків вам, імовірно, не варто використовувати будь-який базовий образ, а натомість **збирати образ контейнера з нуля** на основі офіційного Docker-образу Python.
|
||||
|
||||
Дбаючи про **порядок** інструкцій у `Dockerfile` та **Docker cache**, ви можете **мінімізувати час збирання**, щоб підвищити продуктивність (і уникати нудьги). 😎
|
||||
65
docs/uk/docs/deployment/fastapicloud.md
Normal file
65
docs/uk/docs/deployment/fastapicloud.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# FastAPI Cloud { #fastapi-cloud }
|
||||
|
||||
Ви можете розгорнути свій застосунок FastAPI у <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a> **однією командою**. Приєднуйтеся до списку очікування, якщо ви ще цього не зробили. 🚀
|
||||
|
||||
## Вхід { #login }
|
||||
|
||||
Переконайтеся, що у вас уже є обліковий запис **FastAPI Cloud** (ми запросили вас зі списку очікування 😉).
|
||||
|
||||
Далі виконайте вхід:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi login
|
||||
|
||||
You are logged in to FastAPI Cloud 🚀
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Розгортання { #deploy }
|
||||
|
||||
Тепер розгорніть свій застосунок **однією командою**:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi deploy
|
||||
|
||||
Deploying to FastAPI Cloud...
|
||||
|
||||
✅ Deployment successful!
|
||||
|
||||
🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Ось і все! Тепер ви можете відкрити свій застосунок за цією URL-адресою. ✨
|
||||
|
||||
## Про FastAPI Cloud { #about-fastapi-cloud }
|
||||
|
||||
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** створено тим самим автором і командою, що й **FastAPI**.
|
||||
|
||||
Він спрощує процес **розробки**, **розгортання** та **доступу** до API з мінімальними зусиллями.
|
||||
|
||||
Він переносить той самий **developer experience** створення застосунків із FastAPI на їх **розгортання** в хмарі. 🎉
|
||||
|
||||
Також він подбає про більшість речей, які зазвичай потрібні під час розгортання застосунку, зокрема:
|
||||
|
||||
* HTTPS
|
||||
* Реплікація з autoscaling на основі кількості запитів
|
||||
* тощо
|
||||
|
||||
FastAPI Cloud — основний спонсор і джерело фінансування open source проєктів *FastAPI and friends*. ✨
|
||||
|
||||
## Розгортання в інших хмарних провайдерах { #deploy-to-other-cloud-providers }
|
||||
|
||||
FastAPI має відкритий вихідний код і базується на стандартах. Ви можете розгортати застосунки FastAPI в будь-якого хмарного провайдера на ваш вибір.
|
||||
|
||||
Дотримуйтеся інструкцій вашого хмарного провайдера, щоб розгорнути в них застосунки FastAPI. 🤓
|
||||
|
||||
## Розгортання на власному сервері { #deploy-your-own-server }
|
||||
|
||||
Пізніше в цьому посібнику з **Deployment** я також навчу вас усіх деталей, щоб ви розуміли, що відбувається, що потрібно зробити, і як розгортати застосунки FastAPI самостійно, зокрема на власних серверах. 🤓
|
||||
231
docs/uk/docs/deployment/https.md
Normal file
231
docs/uk/docs/deployment/https.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# Про HTTPS { #about-https }
|
||||
|
||||
Легко припустити, що HTTPS — це щось, що просто «увімкнено» або ні.
|
||||
|
||||
Але все значно складніше.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Якщо ви поспішаєте або вам байдуже, переходьте до наступних розділів із покроковими інструкціями, як усе налаштувати різними способами.
|
||||
|
||||
///
|
||||
|
||||
Щоб **вивчити основи HTTPS** з точки зору користувача, перегляньте <a href="https://howhttps.works/" class="external-link" target="_blank">https://howhttps.works/</a>.
|
||||
|
||||
А тепер, з **точки зору розробника**, ось кілька речей, які варто пам’ятати, думаючи про HTTPS:
|
||||
|
||||
* Для HTTPS **сервер** має **мати «сертифікати»**, отримані від **третьої сторони**.
|
||||
* Насправді ці сертифікати **отримують** у третьої сторони, а не «генерують».
|
||||
* Сертифікати мають **строк дії**.
|
||||
* Вони **закінчуються**.
|
||||
* І тоді їх потрібно **поновлювати**, **отримувати знову** у третьої сторони.
|
||||
* Шифрування з’єднання відбувається на **рівні TCP**.
|
||||
* Це на один рівень **нижче HTTP**.
|
||||
* Тому обробка **сертифіката і шифрування** виконується **до HTTP**.
|
||||
* **TCP не знає про «домени»**. Лише про IP-адреси.
|
||||
* Інформація про **конкретний домен**, який запитують, міститься в **даних HTTP**.
|
||||
* **HTTPS-сертифікати** «засвідчують» **певний домен**, але протокол і шифрування відбуваються на рівні TCP, **до того як стане відомо**, з яким доменом мають справу.
|
||||
* **За замовчуванням** це означало б, що ви можете мати лише **один HTTPS-сертифікат на одну IP-адресу**.
|
||||
* Неважливо, наскільки великий ваш сервер або наскільки малими є окремі застосунки на ньому.
|
||||
* Однак для цього є **рішення**.
|
||||
* Існує **розширення** протоколу **TLS** (який відповідає за шифрування на рівні TCP, до HTTP) під назвою **<a href="https://en.wikipedia.org/wiki/Server_Name_Indication" class="external-link" target="_blank"><abbr title="Server Name Indication - Індикація імені сервера">SNI</abbr></a>**.
|
||||
* Це розширення SNI дозволяє одному серверу (з **однією IP-адресою**) мати **кілька HTTPS-сертифікатів** і обслуговувати **кілька HTTPS-доменів/застосунків**.
|
||||
* Щоб це працювало, **один** компонент (програма), що працює на сервері та слухає **публічну IP-адресу**, має мати на сервері **всі HTTPS-сертифікати**.
|
||||
* **Після** встановлення безпечного з’єднання протокол взаємодії **все ще HTTP**.
|
||||
* Вміст **зашифрований**, навіть попри те, що його надсилають за **протоколом HTTP**.
|
||||
|
||||
Поширена практика — мати на сервері (машині, хості тощо) **одну програму/HTTP-сервер**, який **керує всіма HTTPS-частинами**: приймає **зашифровані HTTPS-запити**, надсилає **розшифровані HTTP-запити** до реального HTTP-застосунку, що працює на тому самому сервері (у цьому випадку — застосунок **FastAPI**), бере **HTTP-відповідь** від застосунку, **шифрує її** відповідним **HTTPS-сертифікатом** і відправляє назад клієнту через **HTTPS**. Такий сервер часто називають **<a href="https://en.wikipedia.org/wiki/TLS_termination_proxy" class="external-link" target="_blank">TLS Termination Proxy</a>**.
|
||||
|
||||
Деякі варіанти, які ви можете використати як TLS Termination Proxy:
|
||||
|
||||
* Traefik (також може обробляти поновлення сертифікатів)
|
||||
* Caddy (також може обробляти поновлення сертифікатів)
|
||||
* Nginx
|
||||
* HAProxy
|
||||
|
||||
## Let's Encrypt { #lets-encrypt }
|
||||
|
||||
До Let's Encrypt ці **HTTPS-сертифікати** продавали довірені треті сторони.
|
||||
|
||||
Процес отримання такого сертифіката був незручним, вимагав чимало паперової роботи, а самі сертифікати були доволі дорогими.
|
||||
|
||||
Але потім з’явився **<a href="https://letsencrypt.org/" class="external-link" target="_blank">Let's Encrypt</a>**.
|
||||
|
||||
Це проєкт Linux Foundation. Він надає **HTTPS-сертифікати безкоштовно**, в автоматизований спосіб. Ці сертифікати використовують усі стандартні криптографічні механізми безпеки та мають короткий строк дії (приблизно 3 місяці), тож **безпека фактично краща** завдяки скороченому терміну.
|
||||
|
||||
Домени надійно перевіряються, а сертифікати генеруються автоматично. Це також дозволяє автоматизувати їх поновлення.
|
||||
|
||||
Ідея в тому, щоб автоматизувати отримання та поновлення цих сертифікатів, аби ви могли мати **безпечний HTTPS безкоштовно й назавжди**.
|
||||
|
||||
## HTTPS для розробників { #https-for-developers }
|
||||
|
||||
Ось приклад того, як може виглядати HTTPS API — крок за кроком, з акцентом на ідеї, важливі для розробників.
|
||||
|
||||
### Доменне ім’я { #domain-name }
|
||||
|
||||
Імовірно, усе почнеться з того, що ви **придбаєте** **доменне ім’я**. Потім ви налаштуєте його в DNS-сервері (можливо, у того ж хмарного провайдера).
|
||||
|
||||
Ймовірно, ви отримаєте хмарний сервер (віртуальну машину) або щось подібне, і він матиме <abbr title="That doesn't change - Це не змінюється">fixed</abbr> **публічну IP-адресу**.
|
||||
|
||||
У DNS-сервері(ах) ви налаштуєте запис ( «`A record`»), щоб він спрямовував **ваш домен** на публічну **IP-адресу вашого сервера**.
|
||||
|
||||
Швидше за все, ви зробите це лише один раз — уперше, коли все налаштовуватимете.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Частина про доменне ім’я йде задовго до HTTPS, але оскільки все залежить від домену та IP-адреси, варто згадати про це тут.
|
||||
|
||||
///
|
||||
|
||||
### DNS { #dns }
|
||||
|
||||
Тепер зосередьмося на власне HTTPS-частинах.
|
||||
|
||||
Спочатку браузер запитає в **DNS-серверів**, яка **IP-адреса відповідає домену**, у цьому випадку `someapp.example.com`.
|
||||
|
||||
DNS-сервери скажуть браузеру використовувати конкретну **IP-адресу**. Це буде публічна IP-адреса, яку використовує ваш сервер і яку ви налаштували в DNS-серверах.
|
||||
|
||||
<img src="/img/deployment/https/https01.drawio.svg">
|
||||
|
||||
### Початок TLS handshake { #tls-handshake-start }
|
||||
|
||||
Далі браузер зв’яжеться з цією IP-адресою через **порт 443** (порт HTTPS).
|
||||
|
||||
Перша частина взаємодії — це встановити з’єднання між клієнтом і сервером та узгодити криптографічні ключі тощо.
|
||||
|
||||
<img src="/img/deployment/https/https02.drawio.svg">
|
||||
|
||||
Ця взаємодія між клієнтом і сервером для встановлення TLS-з’єднання називається **TLS handshake**.
|
||||
|
||||
### TLS із розширенням SNI { #tls-with-sni-extension }
|
||||
|
||||
**Лише один процес** на сервері може слухати конкретний **порт** на конкретній **IP-адресі**. Можуть бути інші процеси, що слухають інші порти на тій самій IP-адресі, але для кожної комбінації IP-адреси й порту — лише один.
|
||||
|
||||
TLS (HTTPS) за замовчуванням використовує порт `443`. Тож потрібен саме він.
|
||||
|
||||
Оскільки лише один процес може слухати цей порт, таким процесом буде **TLS Termination Proxy**.
|
||||
|
||||
TLS Termination Proxy матиме доступ до одного або кількох **TLS-сертифікатів** (HTTPS-сертифікатів).
|
||||
|
||||
Використовуючи описане вище **розширення SNI**, TLS Termination Proxy перевірить, який із доступних TLS (HTTPS) сертифікатів слід використати для цього з’єднання — обравши той, що відповідає домену, який очікує клієнт.
|
||||
|
||||
У цьому випадку він використає сертифікат для `someapp.example.com`.
|
||||
|
||||
<img src="/img/deployment/https/https03.drawio.svg">
|
||||
|
||||
Клієнт уже **довіряє** організації, яка згенерувала цей TLS-сертифікат (у цьому випадку Let's Encrypt, але про це буде пізніше), тож він може **перевірити**, що сертифікат дійсний.
|
||||
|
||||
Далі, використовуючи сертифікат, клієнт і TLS Termination Proxy **узгоджують, як шифрувати** решту **TCP-обміну**. На цьому частина **TLS Handshake** завершується.
|
||||
|
||||
Після цього клієнт і сервер мають **зашифроване TCP-з’єднання** — саме це надає TLS. А потім вони можуть використовувати це з’єднання, щоб почати фактичну **HTTP-взаємодію**.
|
||||
|
||||
І це й є **HTTPS**: звичайний **HTTP** всередині **захищеного TLS-з’єднання** замість чистого (незашифрованого) TCP-з’єднання.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Зверніть увагу, що шифрування відбувається на **рівні TCP**, а не на рівні HTTP.
|
||||
|
||||
///
|
||||
|
||||
### HTTPS-запит { #https-request }
|
||||
|
||||
Тепер, коли клієнт і сервер (зокрема браузер і TLS Termination Proxy) мають **зашифроване TCP-з’єднання**, вони можуть почати **HTTP-взаємодію**.
|
||||
|
||||
Тож клієнт надсилає **HTTPS-запит**. Це просто HTTP-запит через зашифроване TLS-з’єднання.
|
||||
|
||||
<img src="/img/deployment/https/https04.drawio.svg">
|
||||
|
||||
### Розшифрування запиту { #decrypt-the-request }
|
||||
|
||||
TLS Termination Proxy використовуватиме узгоджене шифрування, щоб **розшифрувати запит**, і передасть **звичайний (розшифрований) HTTP-запит** до процесу, який запускає застосунок (наприклад, процес із Uvicorn, що запускає застосунок FastAPI).
|
||||
|
||||
<img src="/img/deployment/https/https05.drawio.svg">
|
||||
|
||||
### HTTP-відповідь { #http-response }
|
||||
|
||||
Застосунок обробить запит і надішле **звичайну (незашифровану) HTTP-відповідь** до TLS Termination Proxy.
|
||||
|
||||
<img src="/img/deployment/https/https06.drawio.svg">
|
||||
|
||||
### HTTPS-відповідь { #https-response }
|
||||
|
||||
Далі TLS Termination Proxy **зашифрує відповідь**, використовуючи узгоджену раніше криптографію (що почалася із сертифіката для `someapp.example.com`), і відправить її назад у браузер.
|
||||
|
||||
Потім браузер перевірить, що відповідь дійсна й зашифрована правильним криптографічним ключем тощо. Після цього він **розшифрує відповідь** і обробить її.
|
||||
|
||||
<img src="/img/deployment/https/https07.drawio.svg">
|
||||
|
||||
Клієнт (браузер) знатиме, що відповідь надходить від правильного сервера, бо використовується криптографія, яку вони узгодили раніше за допомогою **HTTPS-сертифіката**.
|
||||
|
||||
### Кілька застосунків { #multiple-applications }
|
||||
|
||||
На тому самому сервері (або серверах) може бути **кілька застосунків**, наприклад інші API-програми або база даних.
|
||||
|
||||
Лише один процес може обробляти конкретні IP і порт (у нашому прикладі — TLS Termination Proxy), але інші застосунки/процеси також можуть працювати на сервері(ах), доки вони не намагаються використати ту саму **комбінацію публічної IP-адреси та порту**.
|
||||
|
||||
<img src="/img/deployment/https/https08.drawio.svg">
|
||||
|
||||
Таким чином TLS Termination Proxy може обробляти HTTPS і сертифікати для **кількох доменів**, для кількох застосунків, а потім передавати запити до правильного застосунку в кожному випадку.
|
||||
|
||||
### Поновлення сертифікатів { #certificate-renewal }
|
||||
|
||||
У певний момент у майбутньому кожен сертифікат **закінчиться** (приблизно через 3 місяці після отримання).
|
||||
|
||||
І тоді інша програма (у деяких випадках це інша програма, у деяких — це може бути той самий TLS Termination Proxy) зв’яжеться з Let's Encrypt і поновить сертифікат(и).
|
||||
|
||||
<img src="/img/deployment/https/https.drawio.svg">
|
||||
|
||||
**TLS-сертифікати** **прив’язані до доменного імені**, а не до IP-адреси.
|
||||
|
||||
Тому, щоб поновити сертифікати, програма поновлення має **довести** центру сертифікації (Let's Encrypt), що вона справді **«володіє» і контролює цей домен**.
|
||||
|
||||
Для цього, а також щоб задовольнити потреби різних застосунків, є кілька способів. Популярні варіанти:
|
||||
|
||||
* **Змінити деякі DNS-записи**.
|
||||
* Для цього програма поновлення має підтримувати API DNS-провайдера, тож залежно від провайдера DNS це може бути або не бути можливим.
|
||||
* **Запустити як сервер** (принаймні на час процесу отримання сертифіката) на публічній IP-адресі, пов’язаній із доменом.
|
||||
* Як ми казали вище, лише один процес може слухати конкретні IP і порт.
|
||||
* Це одна з причин, чому дуже корисно, коли той самий TLS Termination Proxy також відповідає за процес поновлення сертифікатів.
|
||||
* Інакше вам, можливо, доведеться тимчасово зупинити TLS Termination Proxy, запустити програму поновлення, щоб отримати сертифікати, потім налаштувати їх у TLS Termination Proxy і перезапустити TLS Termination Proxy. Це неідеально, бо ваші застосунки не будуть доступні в той час, коли TLS Termination Proxy вимкнений.
|
||||
|
||||
Увесь цей процес поновлення, паралельно з обслуговуванням застосунку, — одна з головних причин, чому варто мати **окрему систему для обробки HTTPS** із TLS Termination Proxy, замість того щоб використовувати TLS-сертифікати безпосередньо в application server (наприклад, Uvicorn).
|
||||
|
||||
## Проксійовані forwarded headers { #proxy-forwarded-headers }
|
||||
|
||||
Коли ви використовуєте проксі для обробки HTTPS, ваш **application server** (наприклад Uvicorn через FastAPI CLI) нічого не знає про процес HTTPS і спілкується звичайним HTTP із **TLS Termination Proxy**.
|
||||
|
||||
Цей **proxy** зазвичай «на льоту» встановлює деякі HTTP-заголовки перед тим, як передати запит до **application server**, щоб дати application server знати, що запит **проксійовано** через проксі.
|
||||
|
||||
/// note | Технічні деталі
|
||||
|
||||
Заголовки проксі:
|
||||
|
||||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For" class="external-link" target="_blank">X-Forwarded-For</a>
|
||||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto" class="external-link" target="_blank">X-Forwarded-Proto</a>
|
||||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Host" class="external-link" target="_blank">X-Forwarded-Host</a>
|
||||
|
||||
///
|
||||
|
||||
Попри це, оскільки **application server** не знає, що він стоїть за довіреним **proxy**, за замовчуванням він не довірятиме цим заголовкам.
|
||||
|
||||
Але ви можете налаштувати **application server** так, щоб він довіряв заголовкам *forwarded*, які надсилає **proxy**. Якщо ви використовуєте FastAPI CLI, ви можете використати *CLI Option* `--forwarded-allow-ips`, щоб вказати, з яких IP він має довіряти цим заголовкам *forwarded*.
|
||||
|
||||
Наприклад, якщо **application server** отримує запити лише від довіреного **proxy**, ви можете встановити `--forwarded-allow-ips="*"`, щоб довіряти всім вхідним IP, адже він отримуватиме запити лише з IP, який використовує **proxy**.
|
||||
|
||||
Так застосунок зможе знати свою власну публічну URL-адресу, чи використовує він HTTPS, домен тощо.
|
||||
|
||||
Це буде корисно, наприклад, щоб коректно обробляти перенаправлення.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Докладніше дивіться в документації: [За проксі — увімкнення Proxy Forwarded Headers](../advanced/behind-a-proxy.md#enable-proxy-forwarded-headers){.internal-link target=_blank}
|
||||
|
||||
///
|
||||
|
||||
## Підсумок { #recap }
|
||||
|
||||
Наявність **HTTPS** дуже важлива і в більшості випадків є **критичною**. Переважно зусилля, які вам як розробнику потрібно докласти щодо HTTPS, зводяться до **розуміння цих концепцій** і того, як вони працюють.
|
||||
|
||||
Але щойно ви знаєте базову інформацію про **HTTPS для розробників**, ви можете легко поєднувати й налаштовувати різні інструменти, щоб керувати всім у простий спосіб.
|
||||
|
||||
У наступних розділах я покажу кілька конкретних прикладів, як налаштувати **HTTPS** для застосунків **FastAPI**. 🔒
|
||||
23
docs/uk/docs/deployment/index.md
Normal file
23
docs/uk/docs/deployment/index.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Розгортання { #deployment }
|
||||
|
||||
Розгортання застосунку **FastAPI** є відносно простим.
|
||||
|
||||
## Що означає розгортання { #what-does-deployment-mean }
|
||||
|
||||
**Розгорнути** застосунок означає виконати необхідні кроки, щоб зробити його **доступним для користувачів**.
|
||||
|
||||
Для **web API** це зазвичай передбачає розміщення його на **віддаленій машині** з **серверною програмою**, яка забезпечує хорошу продуктивність, стабільність тощо, щоб ваші **користувачі** могли ефективно **отримувати доступ** до застосунку без перерв або проблем.
|
||||
|
||||
Це відрізняється від етапів **розробки**, коли ви постійно змінюєте код, ламаєте його й виправляєте, зупиняєте та перезапускаєте сервер розробки тощо.
|
||||
|
||||
## Стратегії розгортання { #deployment-strategies }
|
||||
|
||||
Є кілька способів зробити це залежно від вашого конкретного сценарію використання та інструментів, які ви застосовуєте.
|
||||
|
||||
Ви можете **розгорнути сервер** самостійно, використовуючи комбінацію інструментів, можете скористатися **хмарним сервісом**, який виконує частину роботи за вас, або іншими можливими варіантами.
|
||||
|
||||
Наприклад, ми, команда FastAPI, створили <a href="https://fastapicloud.com" class="external-link" target="_blank">**FastAPI Cloud**</a>, щоб зробити розгортання застосунків FastAPI у хмарі максимально зручним і «streamlined», з таким самим developer experience, як і під час роботи з FastAPI.
|
||||
|
||||
Я покажу вам деякі основні концепції, які вам, імовірно, варто мати на увазі під час розгортання застосунку **FastAPI** (хоча більшість із цього стосується будь-якого іншого типу вебзастосунку).
|
||||
|
||||
У наступних розділах ви побачите більше деталей, які варто врахувати, і деякі техніки, як це робити. ✨
|
||||
157
docs/uk/docs/deployment/manually.md
Normal file
157
docs/uk/docs/deployment/manually.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Запуск сервера вручну { #run-a-server-manually }
|
||||
|
||||
## Використайте команду `fastapi run` { #use-the-fastapi-run-command }
|
||||
|
||||
Коротко: використовуйте `fastapi run`, щоб запустити вашу FastAPI-застосунок:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ <font color="#4E9A06">fastapi</font> run <u style="text-decoration-style:solid">main.py</u>
|
||||
|
||||
<span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting production server 🚀
|
||||
|
||||
Searching for package file structure from directories
|
||||
with <font color="#3465A4">__init__.py</font> files
|
||||
Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font>
|
||||
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> module </font></span> 🐍 main.py
|
||||
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> code </font></span> Importing the FastAPI app object from the module with
|
||||
the following code:
|
||||
|
||||
<u style="text-decoration-style:solid">from </u><u style="text-decoration-style:solid"><b>main</b></u><u style="text-decoration-style:solid"> import </u><u style="text-decoration-style:solid"><b>app</b></u>
|
||||
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> app </font></span> Using import string: <font color="#3465A4">main:app</font>
|
||||
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Server started at <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000</u></font>
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Documentation at <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000/docs</u></font>
|
||||
|
||||
Logs:
|
||||
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>2306215</b></font><b>]</b>
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup.
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete.
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000</u></font> <b>(</b>Press CTRL+C
|
||||
to quit<b>)</b>
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Це працюватиме в більшості випадків. 😎
|
||||
|
||||
Ви можете використовувати цю команду, наприклад, щоб запустити ваш **FastAPI** застосунок у контейнері, на сервері тощо.
|
||||
|
||||
## ASGI-сервери { #asgi-servers }
|
||||
|
||||
Розгляньмо деталі трохи глибше.
|
||||
|
||||
FastAPI використовує стандарт для побудови Python web framework-ів і серверів під назвою <abbr title="Asynchronous Server Gateway Interface - Асинхронний інтерфейс шлюзу сервера">ASGI</abbr>. FastAPI — це ASGI web framework.
|
||||
|
||||
Головне, що вам потрібно, щоб запустити застосунок **FastAPI** (або будь-який інший ASGI-застосунок) на віддаленій серверній машині — це програма ASGI-сервера, наприклад **Uvicorn**. Саме її `fastapi` використовує за замовчуванням.
|
||||
|
||||
Є кілька альтернатив, зокрема:
|
||||
|
||||
* <a href="https://www.uvicorn.dev/" class="external-link" target="_blank">Uvicorn</a>: високопродуктивний ASGI-сервер.
|
||||
* <a href="https://hypercorn.readthedocs.io/" class="external-link" target="_blank">Hypercorn</a>: ASGI-сервер, сумісний з HTTP/2 і Trio, серед інших можливостей.
|
||||
* <a href="https://github.com/django/daphne" class="external-link" target="_blank">Daphne</a>: ASGI-сервер, створений для Django Channels.
|
||||
* <a href="https://github.com/emmett-framework/granian" class="external-link" target="_blank">Granian</a>: Rust HTTP-сервер для Python-застосунків.
|
||||
* <a href="https://unit.nginx.org/howto/fastapi/" class="external-link" target="_blank">NGINX Unit</a>: NGINX Unit — легковагове та універсальне середовище виконання web-застосунків.
|
||||
|
||||
## Серверна машина та серверна програма { #server-machine-and-server-program }
|
||||
|
||||
Є невеликий нюанс із назвами, який варто пам’ятати. 💡
|
||||
|
||||
Слово «**server**» часто використовують і для віддаленого/хмарного комп’ютера (фізичної або віртуальної машини), і для програми, що працює на цій машині (наприклад, Uvicorn).
|
||||
|
||||
Просто майте на увазі: коли загалом читаєте «server», це може означати одну з цих двох речей.
|
||||
|
||||
Коли йдеться про віддалену машину, її часто називають **server**, а також **machine**, **VM** (virtual machine), **node**. Усе це означає певний тип віддаленої машини, зазвичай під Linux, на якій ви запускаєте програми.
|
||||
|
||||
## Встановіть серверну програму { #install-the-server-program }
|
||||
|
||||
Коли ви встановлюєте FastAPI, разом із ним встановлюється production server — Uvicorn, і ви можете запустити його командою `fastapi run`.
|
||||
|
||||
Але ви також можете встановити ASGI-сервер вручну.
|
||||
|
||||
Переконайтеся, що ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, і тоді можете встановити серверний застосунок.
|
||||
|
||||
Наприклад, щоб встановити Uvicorn:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ pip install "uvicorn[standard]"
|
||||
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Подібний процес застосовується і для будь-якої іншої програми ASGI-сервера.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Додаючи `standard`, Uvicorn встановить і використовуватиме деякі рекомендовані додаткові залежності.
|
||||
|
||||
Зокрема `uvloop` — високопродуктивну повністю сумісну заміну для `asyncio`, що дає значний приріст продуктивності паралельного виконання.
|
||||
|
||||
Коли ви встановлюєте FastAPI командою на кшталт `pip install "fastapi[standard]"`, ви також уже отримуєте `uvicorn[standard]`.
|
||||
|
||||
///
|
||||
|
||||
## Запустіть серверну програму { #run-the-server-program }
|
||||
|
||||
Якщо ви встановили ASGI-сервер вручну, зазвичай потрібно передати рядок імпорту (import string) у спеціальному форматі, щоб він імпортував ваш FastAPI-застосунок:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ uvicorn main:app --host 0.0.0.0 --port 80
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
/// note | Примітка
|
||||
|
||||
Команда `uvicorn main:app` означає:
|
||||
|
||||
* `main`: файл `main.py` (Python «module»).
|
||||
* `app`: об’єкт, створений всередині `main.py` рядком `app = FastAPI()`.
|
||||
|
||||
Це еквівалентно:
|
||||
|
||||
```Python
|
||||
from main import app
|
||||
```
|
||||
|
||||
///
|
||||
|
||||
Кожна альтернативна програма ASGI-сервера матиме подібну команду, докладніше дивіться у відповідній документації.
|
||||
|
||||
/// warning | Попередження
|
||||
|
||||
Uvicorn та інші сервери підтримують опцію `--reload`, яка корисна під час розробки.
|
||||
|
||||
Опція `--reload` споживає значно більше ресурсів, є менш стабільною тощо.
|
||||
|
||||
Вона дуже допомагає під час **розробки**, але **не слід** використовувати її в **production**.
|
||||
|
||||
///
|
||||
|
||||
## Концепції деплою { #deployment-concepts }
|
||||
|
||||
Ці приклади запускають серверну програму (наприклад, Uvicorn), стартуючи **один процес**, що слухає всі IP-адреси (`0.0.0.0`) на наперед визначеному порту (наприклад, `80`).
|
||||
|
||||
Це базова ідея. Але, ймовірно, вам також потрібно буде подбати про додаткові речі, як-от:
|
||||
|
||||
* Безпека — HTTPS
|
||||
* Запуск при старті системи
|
||||
* Перезапуски
|
||||
* Реплікація (кількість запущених процесів)
|
||||
* Пам’ять
|
||||
* Попередні кроки перед запуском
|
||||
|
||||
У наступних розділах я розповім більше про кожне з цих понять: як про них думати та наведу конкретні приклади зі стратегіями, як із цим працювати. 🚀
|
||||
139
docs/uk/docs/deployment/server-workers.md
Normal file
139
docs/uk/docs/deployment/server-workers.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Воркери сервера — Uvicorn із воркерами { #server-workers-uvicorn-with-workers }
|
||||
|
||||
Повернімося до розглянутих раніше концепцій розгортання:
|
||||
|
||||
* Безпека — HTTPS
|
||||
* Запуск під час старту системи
|
||||
* Перезапуски
|
||||
* **Реплікація (кількість запущених процесів)**
|
||||
* Пам’ять
|
||||
* Попередні кроки перед запуском
|
||||
|
||||
До цього моменту, з усіма навчальними матеріалами в документації, ви, ймовірно, запускали **серверну програму**, наприклад за допомогою команди `fastapi`, яка запускає Uvicorn, і працювали в **одному процесі**.
|
||||
|
||||
Під час розгортання застосунків вам, імовірно, захочеться мати певну **реплікацію процесів**, щоб використати переваги **кількох ядер** і мати змогу обробляти більше запитів.
|
||||
|
||||
Як ви бачили в попередньому розділі про [Концепції розгортання](concepts.md){.internal-link target=_blank}, існує кілька стратегій, які можна застосувати.
|
||||
|
||||
Тут я покажу, як використовувати **Uvicorn** із **воркер-процесами**, використовуючи команду `fastapi` або безпосередньо команду `uvicorn`.
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Якщо ви використовуєте контейнери, наприклад Docker або Kubernetes, я розповім про це докладніше в наступному розділі: [FastAPI у контейнерах — Docker](docker.md){.internal-link target=_blank}.
|
||||
|
||||
Зокрема, під час запуску в **Kubernetes** вам, імовірно, **не** варто використовувати воркери, а натомість запускати **один процес Uvicorn на контейнер**, але про це я розповім пізніше в тому розділі.
|
||||
|
||||
///
|
||||
|
||||
## Кілька воркерів { #multiple-workers }
|
||||
|
||||
Ви можете запустити кілька воркерів за допомогою опції командного рядка `--workers`:
|
||||
|
||||
//// tab | `fastapi`
|
||||
|
||||
Якщо ви використовуєте команду `fastapi`:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ <font color="#4E9A06">fastapi</font> run --workers 4 <u style="text-decoration-style:solid">main.py</u>
|
||||
|
||||
<span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting production server 🚀
|
||||
|
||||
Searching for package file structure from directories with
|
||||
<font color="#3465A4">__init__.py</font> files
|
||||
Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font>
|
||||
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> module </font></span> 🐍 main.py
|
||||
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> code </font></span> Importing the FastAPI app object from the module with the
|
||||
following code:
|
||||
|
||||
<u style="text-decoration-style:solid">from </u><u style="text-decoration-style:solid"><b>main</b></u><u style="text-decoration-style:solid"> import </u><u style="text-decoration-style:solid"><b>app</b></u>
|
||||
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> app </font></span> Using import string: <font color="#3465A4">main:app</font>
|
||||
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Server started at <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000</u></font>
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Documentation at <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000/docs</u></font>
|
||||
|
||||
Logs:
|
||||
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000</u></font> <b>(</b>Press CTRL+C to
|
||||
quit<b>)</b>
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started parent process <b>[</b><font color="#34E2E2"><b>27365</b></font><b>]</b>
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>27368</b></font><b>]</b>
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>27369</b></font><b>]</b>
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>27370</b></font><b>]</b>
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>27367</b></font><b>]</b>
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup.
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup.
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup.
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup.
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete.
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete.
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete.
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete.
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
////
|
||||
|
||||
//// tab | `uvicorn`
|
||||
|
||||
Якщо ви надаєте перевагу використанню команди `uvicorn` безпосередньо:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4
|
||||
<font color="#A6E22E">INFO</font>: Uvicorn running on <b>http://0.0.0.0:8080</b> (Press CTRL+C to quit)
|
||||
<font color="#A6E22E">INFO</font>: Started parent process [<font color="#A1EFE4"><b>27365</b></font>]
|
||||
<font color="#A6E22E">INFO</font>: Started server process [<font color="#A1EFE4">27368</font>]
|
||||
<font color="#A6E22E">INFO</font>: Waiting for application startup.
|
||||
<font color="#A6E22E">INFO</font>: Application startup complete.
|
||||
<font color="#A6E22E">INFO</font>: Started server process [<font color="#A1EFE4">27369</font>]
|
||||
<font color="#A6E22E">INFO</font>: Waiting for application startup.
|
||||
<font color="#A6E22E">INFO</font>: Application startup complete.
|
||||
<font color="#A6E22E">INFO</font>: Started server process [<font color="#A1EFE4">27370</font>]
|
||||
<font color="#A6E22E">INFO</font>: Waiting for application startup.
|
||||
<font color="#A6E22E">INFO</font>: Application startup complete.
|
||||
<font color="#A6E22E">INFO</font>: Started server process [<font color="#A1EFE4">27367</font>]
|
||||
<font color="#A6E22E">INFO</font>: Waiting for application startup.
|
||||
<font color="#A6E22E">INFO</font>: Application startup complete.
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
////
|
||||
|
||||
Єдина нова опція тут — `--workers`, яка наказує Uvicorn запустити 4 воркер-процеси.
|
||||
|
||||
Також ви можете бачити, що він показує **PID** кожного процесу: `27365` для батьківського процесу (це **менеджер процесів**) і по одному для кожного воркер-процесу: `27368`, `27369`, `27370` та `27367`.
|
||||
|
||||
## Концепції розгортання { #deployment-concepts }
|
||||
|
||||
Тут ви побачили, як використовувати кілька **воркерів**, щоб **паралелізувати** виконання застосунку, використати переваги **кількох ядер** CPU та мати змогу обслуговувати **більше запитів**.
|
||||
|
||||
Із наведеного вище списку концепцій розгортання використання воркерів здебільшого допоможе з частиною **реплікації**, і трохи — з **перезапусками**, але про решту все одно потрібно подбати:
|
||||
|
||||
* **Безпека — HTTPS**
|
||||
* **Запуск під час старту системи**
|
||||
* ***Перезапуски***
|
||||
* Реплікація (кількість запущених процесів)
|
||||
* **Пам’ять**
|
||||
* **Попередні кроки перед запуском**
|
||||
|
||||
## Контейнери та Docker { #containers-and-docker }
|
||||
|
||||
У наступному розділі про [FastAPI у контейнерах — Docker](docker.md){.internal-link target=_blank} я поясню деякі стратегії, які ви можете використати, щоб опрацювати інші **концепції розгортання**.
|
||||
|
||||
Я покажу вам, як **зібрати власний image з нуля**, щоб запускати один процес Uvicorn. Це простий процес і, ймовірно, саме те, що вам потрібно при використанні розподіленої системи керування контейнерами на кшталт **Kubernetes**.
|
||||
|
||||
## Підсумок { #recap }
|
||||
|
||||
Ви можете використовувати кілька воркер-процесів за допомогою CLI-опції `--workers` з командами `fastapi` або `uvicorn`, щоб скористатися перевагами **багатоядерних CPU** та запускати **кілька процесів паралельно**.
|
||||
|
||||
Ви можете застосувати ці інструменти та ідеї, якщо налаштовуєте **власну систему розгортання**, водночас самостійно дбаючи про інші концепції розгортання.
|
||||
|
||||
Перегляньте наступний розділ, щоб дізнатися про **FastAPI** з контейнерами (наприклад, Docker і Kubernetes). Ви побачите, що ці інструменти також мають прості способи розв’язати інші **концепції розгортання**. ✨
|
||||
93
docs/uk/docs/deployment/versions.md
Normal file
93
docs/uk/docs/deployment/versions.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Про версії FastAPI { #about-fastapi-versions }
|
||||
|
||||
**FastAPI** вже використовується в продакшені в багатьох застосунках і системах. А покриття тестами підтримується на рівні 100%. Водночас розробка все ще рухається швидко.
|
||||
|
||||
Нові можливості додаються часто, баги виправляються регулярно, а код і надалі безперервно поліпшується.
|
||||
|
||||
Саме тому поточні версії все ще `0.x.x` — це відображає те, що кожна версія потенційно може містити несумісні зміни. Це відповідає правилам <a href="https://semver.org/" class="external-link" target="_blank">Semantic Versioning</a>.
|
||||
|
||||
Ви можете створювати продакшен-застосунки з **FastAPI** уже зараз (і, ймовірно, робите це вже певний час) — потрібно лише переконатися, що ви використовуєте версію, яка коректно працює з рештою вашого коду.
|
||||
|
||||
## Зафіксуйте версію `fastapi` { #pin-your-fastapi-version }
|
||||
|
||||
Перше, що вам слід зробити — «зафіксувати» версію **FastAPI**, яку ви використовуєте, на конкретній останній версії, про яку ви знаєте, що вона коректно працює для вашого застосунку.
|
||||
|
||||
Наприклад, припустімо, ви використовуєте у своєму застосунку версію `0.112.0`.
|
||||
|
||||
Якщо ви використовуєте файл `requirements.txt`, ви можете вказати версію так:
|
||||
|
||||
```txt
|
||||
fastapi[standard]==0.112.0
|
||||
```
|
||||
|
||||
це означатиме, що ви використовуватимете рівно версію `0.112.0`.
|
||||
|
||||
Або ви також можете зафіксувати її так:
|
||||
|
||||
```txt
|
||||
fastapi[standard]>=0.112.0,<0.113.0
|
||||
```
|
||||
|
||||
це означатиме, що ви використовуватимете версії `0.112.0` або вище, але нижче `0.113.0`. Наприклад, версія `0.112.2` також буде прийнятною.
|
||||
|
||||
Якщо ви використовуєте будь-який інший інструмент для керування встановленнями, наприклад `uv`, Poetry, Pipenv або інші, усі вони мають спосіб, яким ви можете визначати конкретні версії для ваших пакетів.
|
||||
|
||||
## Доступні версії { #available-versions }
|
||||
|
||||
Ви можете переглянути доступні версії (наприклад, щоб перевірити, яка зараз остання) у [Нотатках про релізи](../release-notes.md){.internal-link target=_blank}.
|
||||
|
||||
## Про версіонування { #about-versions }
|
||||
|
||||
Згідно з правилами Semantic Versioning, будь-яка версія нижче `1.0.0` потенційно може додавати несумісні зміни.
|
||||
|
||||
FastAPI також дотримується правила, що будь-яка зміна версії «PATCH» призначена для виправлення багів і змін без порушення сумісності.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
«PATCH» — це останнє число, наприклад у `0.2.3` PATCH-версія дорівнює `3`.
|
||||
|
||||
///
|
||||
|
||||
Отже, ви маєте змогу фіксувати версію, наприклад так:
|
||||
|
||||
```txt
|
||||
fastapi>=0.45.0,<0.46.0
|
||||
```
|
||||
|
||||
Несумісні зміни та нові можливості додаються у версіях «MINOR».
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
«MINOR» — це число посередині, наприклад у `0.2.3` MINOR-версія дорівнює `2`.
|
||||
|
||||
///
|
||||
|
||||
## Оновлення версій FastAPI { #upgrading-the-fastapi-versions }
|
||||
|
||||
Вам слід додати тести для вашого застосунку.
|
||||
|
||||
З **FastAPI** це дуже просто (завдяки Starlette). Перегляньте документацію: [Тестування](../tutorial/testing.md){.internal-link target=_blank}
|
||||
|
||||
Після того як у вас будуть тести, ви можете оновити версію **FastAPI** до новішої та переконатися, що весь ваш код працює коректно, запустивши тести.
|
||||
|
||||
Якщо все працює, або після внесення потрібних змін, і всі тести проходять, тоді ви можете зафіксувати `fastapi` на цій новій актуальній версії.
|
||||
|
||||
## Про Starlette { #about-starlette }
|
||||
|
||||
Вам не слід фіксувати версію `starlette`.
|
||||
|
||||
Різні версії **FastAPI** використовуватимуть певну новішу версію Starlette.
|
||||
|
||||
Тож ви можете просто дозволити **FastAPI** використовувати правильну версію Starlette.
|
||||
|
||||
## Про Pydantic { #about-pydantic }
|
||||
|
||||
Pydantic включає тести для **FastAPI** до власного набору тестів, тож нові версії Pydantic (вище `1.0.0`) завжди сумісні з FastAPI.
|
||||
|
||||
Ви можете зафіксувати Pydantic на будь-якій версії вище `1.0.0`, яка вам підходить.
|
||||
|
||||
Наприклад:
|
||||
|
||||
```txt
|
||||
pydantic>=2.7.0,<3.0.0
|
||||
```
|
||||
298
docs/uk/docs/environment-variables.md
Normal file
298
docs/uk/docs/environment-variables.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# Змінні середовища { #environment-variables }
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Якщо ви вже знаєте, що таке «змінні середовища», і як ними користуватися, можете пропустити цей розділ.
|
||||
|
||||
///
|
||||
|
||||
Змінна середовища (також відома як «**env var**») — це змінна, що існує **поза** Python-кодом, в **операційній системі**, і може бути прочитана вашим Python-кодом (або також іншими програмами).
|
||||
|
||||
Змінні середовища можуть бути корисними для керування **налаштуваннями** застосунку, як частина **встановлення** Python тощо.
|
||||
|
||||
## Створення і використання env var { #create-and-use-env-vars }
|
||||
|
||||
Ви можете **створювати** та використовувати змінні середовища в **оболонці (терміналі)**, без потреби в Python:
|
||||
|
||||
//// tab | Linux, macOS, Windows Bash
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// You could create an env var MY_NAME with
|
||||
$ export MY_NAME="Wade Wilson"
|
||||
|
||||
// Then you could use it with other programs, like
|
||||
$ echo "Hello $MY_NAME"
|
||||
|
||||
Hello Wade Wilson
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
////
|
||||
|
||||
//// tab | Windows PowerShell
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// Create an env var MY_NAME
|
||||
$ $Env:MY_NAME = "Wade Wilson"
|
||||
|
||||
// Use it with other programs, like
|
||||
$ echo "Hello $Env:MY_NAME"
|
||||
|
||||
Hello Wade Wilson
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
////
|
||||
|
||||
## Читання env var у Python { #read-env-vars-in-python }
|
||||
|
||||
Ви також можете створювати змінні середовища **поза** Python, у терміналі (або будь-яким іншим способом), а потім **читати їх у Python**.
|
||||
|
||||
Наприклад, у вас може бути файл `main.py` з таким вмістом:
|
||||
|
||||
```Python hl_lines="3"
|
||||
import os
|
||||
|
||||
name = os.getenv("MY_NAME", "World")
|
||||
print(f"Hello {name} from Python")
|
||||
```
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Другий аргумент у <a href="https://docs.python.org/3.8/library/os.html#os.getenv" class="external-link" target="_blank">`os.getenv()`</a> — це значення за замовчуванням, яке слід повернути.
|
||||
|
||||
Якщо його не передати, за замовчуванням буде `None`; тут ми задаємо `"World"` як значення за замовчуванням.
|
||||
|
||||
///
|
||||
|
||||
Після цього ви можете викликати цю Python-програму:
|
||||
|
||||
//// tab | Linux, macOS, Windows Bash
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// Here we don't set the env var yet
|
||||
$ python main.py
|
||||
|
||||
// As we didn't set the env var, we get the default value
|
||||
|
||||
Hello World from Python
|
||||
|
||||
// But if we create an environment variable first
|
||||
$ export MY_NAME="Wade Wilson"
|
||||
|
||||
// And then call the program again
|
||||
$ python main.py
|
||||
|
||||
// Now it can read the environment variable
|
||||
|
||||
Hello Wade Wilson from Python
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
////
|
||||
|
||||
//// tab | Windows PowerShell
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// Here we don't set the env var yet
|
||||
$ python main.py
|
||||
|
||||
// As we didn't set the env var, we get the default value
|
||||
|
||||
Hello World from Python
|
||||
|
||||
// But if we create an environment variable first
|
||||
$ $Env:MY_NAME = "Wade Wilson"
|
||||
|
||||
// And then call the program again
|
||||
$ python main.py
|
||||
|
||||
// Now it can read the environment variable
|
||||
|
||||
Hello Wade Wilson from Python
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
////
|
||||
|
||||
Оскільки змінні середовища можна задавати поза кодом, але їх може читати код, і їх не потрібно зберігати (комітити в `git`) разом з іншими файлами, їх часто використовують для конфігурації або **налаштувань**.
|
||||
|
||||
Ви також можете створити змінну середовища лише для **конкретного запуску програми** — вона буде доступна тільки цій програмі й лише на час її виконання.
|
||||
|
||||
Для цього задайте її прямо перед запуском програми, в тому самому рядку:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// Create an env var MY_NAME in line for this program call
|
||||
$ MY_NAME="Wade Wilson" python main.py
|
||||
|
||||
// Now it can read the environment variable
|
||||
|
||||
Hello Wade Wilson from Python
|
||||
|
||||
// The env var no longer exists afterwards
|
||||
$ python main.py
|
||||
|
||||
Hello World from Python
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Докладніше про це можна прочитати тут: <a href="https://12factor.net/config" class="external-link" target="_blank">The Twelve-Factor App: Config</a>.
|
||||
|
||||
///
|
||||
|
||||
## Типи та валідація { #types-and-validation }
|
||||
|
||||
Ці змінні середовища можуть зберігати лише **текстові рядки**, оскільки вони є зовнішніми відносно Python і мають бути сумісними з іншими програмами та рештою системи (і навіть з різними операційними системами, як-от Linux, Windows, macOS).
|
||||
|
||||
Це означає, що **будь-яке значення**, прочитане в Python зі змінної середовища, **буде `str`**, а будь-яке перетворення в інший тип або будь-яка валідація мають виконуватися в коді.
|
||||
|
||||
Більше про використання змінних середовища для керування **налаштуваннями застосунку** ви дізнаєтеся в розділі [Розширений посібник користувача — Налаштування та змінні середовища](./advanced/settings.md){.internal-link target=_blank}.
|
||||
|
||||
## Змінна середовища `PATH` { #path-environment-variable }
|
||||
|
||||
Існує **особлива** змінна середовища **`PATH`**, яку використовують операційні системи (Linux, macOS, Windows), щоб знаходити програми для запуску.
|
||||
|
||||
Значення змінної `PATH` — це довгий рядок, що складається з директорій, розділених двокрапкою `:` у Linux і macOS та крапкою з комою `;` у Windows.
|
||||
|
||||
Наприклад, змінна середовища `PATH` може виглядати так:
|
||||
|
||||
//// tab | Linux, macOS
|
||||
|
||||
```plaintext
|
||||
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
|
||||
```
|
||||
|
||||
Це означає, що система має шукати програми в директоріях:
|
||||
|
||||
* `/usr/local/bin`
|
||||
* `/usr/bin`
|
||||
* `/bin`
|
||||
* `/usr/sbin`
|
||||
* `/sbin`
|
||||
|
||||
////
|
||||
|
||||
//// tab | Windows
|
||||
|
||||
```plaintext
|
||||
C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32
|
||||
```
|
||||
|
||||
Це означає, що система має шукати програми в директоріях:
|
||||
|
||||
* `C:\Program Files\Python312\Scripts`
|
||||
* `C:\Program Files\Python312`
|
||||
* `C:\Windows\System32`
|
||||
|
||||
////
|
||||
|
||||
Коли ви вводите **команду** в терміналі, операційна система **шукає** програму в **кожній із директорій**, перелічених у змінній середовища `PATH`.
|
||||
|
||||
Наприклад, коли ви вводите `python` у терміналі, операційна система шукає програму з назвою `python` у **першій директорії** цього списку.
|
||||
|
||||
Якщо вона її знаходить — **використовує**. Інакше продовжує пошук в **інших директоріях**.
|
||||
|
||||
### Встановлення Python і оновлення `PATH` { #installing-python-and-updating-the-path }
|
||||
|
||||
Під час встановлення Python вас можуть запитати, чи потрібно оновити змінну середовища `PATH`.
|
||||
|
||||
//// tab | Linux, macOS
|
||||
|
||||
Припустімо, ви встановлюєте Python, і він опиняється в директорії `/opt/custompython/bin`.
|
||||
|
||||
Якщо ви погодитеся оновити змінну середовища `PATH`, тоді інсталятор додасть `/opt/custompython/bin` до змінної `PATH`.
|
||||
|
||||
Це може виглядати так:
|
||||
|
||||
```plaintext
|
||||
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin
|
||||
```
|
||||
|
||||
Таким чином, коли ви введете `python` у терміналі, система знайде програму Python у `/opt/custompython/bin` (останній директорії) і використає саме її.
|
||||
|
||||
////
|
||||
|
||||
//// tab | Windows
|
||||
|
||||
Припустімо, ви встановлюєте Python, і він опиняється в директорії `C:\opt\custompython\bin`.
|
||||
|
||||
Якщо ви погодитеся оновити змінну середовища `PATH`, тоді інсталятор додасть `C:\opt\custompython\bin` до змінної `PATH`.
|
||||
|
||||
```plaintext
|
||||
C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin
|
||||
```
|
||||
|
||||
Таким чином, коли ви введете `python` у терміналі, система знайде програму Python у `C:\opt\custompython\bin` (останній директорії) і використає саме її.
|
||||
|
||||
////
|
||||
|
||||
Отже, якщо ви введете:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ python
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
//// tab | Linux, macOS
|
||||
|
||||
Система **знайде** програму `python` у `/opt/custompython/bin` і запустить її.
|
||||
|
||||
Це буде приблизно еквівалентно введенню:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ /opt/custompython/bin/python
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
////
|
||||
|
||||
//// tab | Windows
|
||||
|
||||
Система **знайде** програму `python` у `C:\opt\custompython\bin\python` і запустить її.
|
||||
|
||||
Це буде приблизно еквівалентно введенню:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ C:\opt\custompython\bin\python
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
////
|
||||
|
||||
Ця інформація буде корисною під час вивчення [Віртуальних середовищ](virtual-environments.md){.internal-link target=_blank}.
|
||||
|
||||
## Висновок { #conclusion }
|
||||
|
||||
Тепер у вас має бути базове розуміння того, що таке **змінні середовища** і як ними користуватися в Python.
|
||||
|
||||
Також ви можете прочитати більше про них у статті <a href="https://en.wikipedia.org/wiki/Environment_variable" class="external-link" target="_blank">Wikipedia про Environment Variable</a>.
|
||||
|
||||
У багатьох випадках не дуже очевидно, як змінні середовища можуть бути корисними й застосовними одразу. Але під час розробки вони з’являються в багатьох різних сценаріях, тож корисно знати про них.
|
||||
|
||||
Наприклад, вам знадобиться ця інформація в наступному розділі про [Віртуальні середовища](virtual-environments.md).
|
||||
256
docs/uk/docs/help-fastapi.md
Normal file
256
docs/uk/docs/help-fastapi.md
Normal file
@@ -0,0 +1,256 @@
|
||||
# Допомога FastAPI — Отримати допомогу { #help-fastapi-get-help }
|
||||
|
||||
Вам подобається **FastAPI**?
|
||||
|
||||
Хотіли б ви допомогти FastAPI, іншим користувачам і автору?
|
||||
|
||||
Або ви хотіли б отримати допомогу з **FastAPI**?
|
||||
|
||||
Є дуже прості способи допомогти (деякі з них потребують лише одного-двох кліків).
|
||||
|
||||
Також є кілька способів отримати допомогу.
|
||||
|
||||
## Підписатися на розсилку { #subscribe-to-the-newsletter }
|
||||
|
||||
Ви можете підписатися на (нечасту) розсилку [**FastAPI and friends**](newsletter.md){.internal-link target=_blank}, щоб бути в курсі:
|
||||
|
||||
* Новин про FastAPI та друзів 🚀
|
||||
* Гайдів 📝
|
||||
* Функцій ✨
|
||||
* Несумісних змін 🚨
|
||||
* Порад і хитрощів ✅
|
||||
|
||||
## Підписатися на FastAPI в X (Twitter) { #follow-fastapi-on-x-twitter }
|
||||
|
||||
<a href="https://x.com/fastapi" class="external-link" target="_blank">Підпишіться на @fastapi в **X (Twitter)**</a>, щоб отримувати найсвіжіші новини про **FastAPI**. 🐦
|
||||
|
||||
## Поставити зірку **FastAPI** у GitHub { #star-fastapi-in-github }
|
||||
|
||||
Ви можете поставити FastAPI «зірку» в GitHub (натиснувши кнопку із зіркою у правому верхньому куті): <a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">https://github.com/fastapi/fastapi</a>. ⭐️
|
||||
|
||||
Додавши зірку, іншим користувачам буде легше знайти проєкт і побачити, що він уже був корисним для інших.
|
||||
|
||||
## Стежити за релізами репозиторію GitHub { #watch-the-github-repository-for-releases }
|
||||
|
||||
Ви можете «watch» FastAPI в GitHub (натиснувши кнопку «watch» у правому верхньому куті): <a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">https://github.com/fastapi/fastapi</a>. 👀
|
||||
|
||||
Там ви можете вибрати «Releases only».
|
||||
|
||||
Після цього ви отримуватимете сповіщення (на електронну пошту) щоразу, коли виходитиме новий реліз (нова версія) **FastAPI** з виправленнями помилок і новими можливостями.
|
||||
|
||||
## Зв’язатися з автором { #connect-with-the-author }
|
||||
|
||||
Ви можете зв’язатися з <a href="https://tiangolo.com" class="external-link" target="_blank">мною (Sebastián Ramírez / `tiangolo`)</a>, автором.
|
||||
|
||||
Ви можете:
|
||||
|
||||
* <a href="https://github.com/tiangolo" class="external-link" target="_blank">Підписатися на мене в **GitHub**</a>.
|
||||
* Подивитися інші Open Source-проєкти, які я створив і які можуть вам допомогти.
|
||||
* Підписатися, щоб бачити, коли я створюю новий Open Source-проєкт.
|
||||
* <a href="https://x.com/tiangolo" class="external-link" target="_blank">Підписатися на мене в **X (Twitter)**</a> або <a href="https://fosstodon.org/@tiangolo" class="external-link" target="_blank">Mastodon</a>.
|
||||
* Розповісти, як ви використовуєте FastAPI (мені дуже приємно це чути).
|
||||
* Дізнаватися, коли я роблю оголошення або випускаю нові інструменти.
|
||||
* Ви також можете <a href="https://x.com/fastapi" class="external-link" target="_blank">підписатися на @fastapi в X (Twitter)</a> (це окремий акаунт).
|
||||
* <a href="https://www.linkedin.com/in/tiangolo/" class="external-link" target="_blank">Підписатися на мене в **LinkedIn**</a>.
|
||||
* Дізнаватися, коли я роблю оголошення або випускаю нові інструменти (хоча частіше я використовую X (Twitter) 🤷♂).
|
||||
* Читати, що я пишу (або підписатися на мене) на <a href="https://dev.to/tiangolo" class="external-link" target="_blank">**Dev.to**</a> або <a href="https://medium.com/@tiangolo" class="external-link" target="_blank">**Medium**</a>.
|
||||
* Читати інші ідеї, статті, а також про інструменти, які я створив.
|
||||
* Підписатися, щоб читати, коли я публікую щось нове.
|
||||
|
||||
## Твітнути про **FastAPI** { #tweet-about-fastapi }
|
||||
|
||||
<a href="https://x.com/compose/tweet?text=I'm loving @fastapi because... https://github.com/fastapi/fastapi" class="external-link" target="_blank">Твітніть про **FastAPI**</a> і дайте мені та іншим знати, чому він вам подобається. 🎉
|
||||
|
||||
Мені приємно чути про те, як використовують **FastAPI**, що саме вам у ньому сподобалося, у якому проєкті/компанії ви його застосовуєте тощо.
|
||||
|
||||
## Проголосувати за FastAPI { #vote-for-fastapi }
|
||||
|
||||
* <a href="https://www.slant.co/options/34241/~fastapi-review" class="external-link" target="_blank">Проголосуйте за **FastAPI** у Slant</a>.
|
||||
* <a href="https://alternativeto.net/software/fastapi/about/" class="external-link" target="_blank">Проголосуйте за **FastAPI** в AlternativeTo</a>.
|
||||
* <a href="https://stackshare.io/pypi-fastapi" class="external-link" target="_blank">Вкажіть, що ви використовуєте **FastAPI** на StackShare</a>.
|
||||
|
||||
## Допомагати іншим із питаннями в GitHub { #help-others-with-questions-in-github }
|
||||
|
||||
Ви можете спробувати допомогти іншим із їхніми питаннями тут:
|
||||
|
||||
* <a href="https://github.com/fastapi/fastapi/discussions/categories/questions?discussions_q=category%3AQuestions+is%3Aunanswered" class="external-link" target="_blank">GitHub Discussions</a>
|
||||
* <a href="https://github.com/fastapi/fastapi/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Aquestion+-label%3Aanswered+" class="external-link" target="_blank">GitHub Issues</a>
|
||||
|
||||
У багатьох випадках ви вже можете знати відповідь на ці питання. 🤓
|
||||
|
||||
Якщо ви багато допомагаєте людям із їхніми питаннями, ви станете офіційним [FastAPI Expert](fastapi-people.md#fastapi-experts){.internal-link target=_blank}. 🎉
|
||||
|
||||
Просто пам’ятайте: найважливіше — намагатися бути доброзичливими. Люди приходять зі своїми розчаруваннями і часто формулюють питання не найкращим чином, але намагайтеся, наскільки можете, бути ввічливими. 🤗
|
||||
|
||||
Ідея в тому, щоб спільнота **FastAPI** була дружньою та відкритою. Водночас не приймайте цькування або зневажливу поведінку щодо інших. Ми маємо піклуватися одне про одного.
|
||||
|
||||
---
|
||||
|
||||
Ось як допомагати іншим із питаннями (у discussions або issues):
|
||||
|
||||
### Зрозуміти питання { #understand-the-question }
|
||||
|
||||
* Перевірте, чи ви розумієте, яка **мета** та сценарій використання в людини, що питає.
|
||||
|
||||
* Далі перевірте, чи питання (переважна більшість — це питання) сформульоване **чітко**.
|
||||
|
||||
* У багатьох випадках поставлене питання стосується уявного рішення користувача, але може існувати **краще**. Якщо ви краще зрозумієте проблему та сценарій використання, ви зможете запропонувати кращий **альтернативний варіант**.
|
||||
|
||||
* Якщо ви не розумієте питання, попросіть більше **деталей**.
|
||||
|
||||
### Відтворити проблему { #reproduce-the-problem }
|
||||
|
||||
У більшості випадків і для більшості питань є щось, пов’язане з **початковим кодом** людини.
|
||||
|
||||
Часто люди копіюють лише фрагмент коду, але цього недостатньо, щоб **відтворити проблему**.
|
||||
|
||||
* Ви можете попросити надати <a href="https://stackoverflow.com/help/minimal-reproducible-example" class="external-link" target="_blank">мінімальний відтворюваний приклад</a>, який ви зможете **скопіювати й вставити** та запустити локально, щоб побачити ту саму помилку або поведінку, або щоб краще зрозуміти їхній сценарій використання.
|
||||
|
||||
* Якщо ви почуваєтеся надто щедрими, можете спробувати **створити приклад** самостійно, лише на основі опису проблеми. Але майте на увазі: це може зайняти багато часу, і краще спершу попросити уточнити проблему.
|
||||
|
||||
### Запропонувати рішення { #suggest-solutions }
|
||||
|
||||
* Після того як ви змогли зрозуміти питання, ви можете дати можливу **відповідь**.
|
||||
|
||||
* У багатьох випадках краще зрозуміти їхню **першопричину або сценарій використання**, адже може існувати кращий спосіб вирішити задачу, ніж те, що вони намагаються зробити.
|
||||
|
||||
### Попросити закрити { #ask-to-close }
|
||||
|
||||
Якщо вам відповіли, дуже ймовірно, що ви вже розв’язали їхню проблему — вітаю, **ви герой**! 🦸
|
||||
|
||||
* Тепер, якщо це справді розв’язало проблему, ви можете попросити їх:
|
||||
|
||||
* у GitHub Discussions: позначити коментар як **answer**.
|
||||
* у GitHub Issues: **закрити** issue.
|
||||
|
||||
## Стежити за репозиторієм GitHub { #watch-the-github-repository }
|
||||
|
||||
Ви можете «watch» FastAPI в GitHub (натиснувши кнопку «watch» у правому верхньому куті): <a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">https://github.com/fastapi/fastapi</a>. 👀
|
||||
|
||||
Якщо вибрати «Watching» замість «Releases only», ви отримуватимете сповіщення, коли хтось створює новий issue або питання. Також можна вказати, що ви хочете отримувати сповіщення лише про нові issues, або discussions, або PR тощо.
|
||||
|
||||
Тоді ви зможете спробувати допомогти їм розв’язати ці питання.
|
||||
|
||||
## Ставити питання { #ask-questions }
|
||||
|
||||
Ви можете <a href="https://github.com/fastapi/fastapi/discussions/new?category=questions" class="external-link" target="_blank">створити нове питання</a> у репозиторії GitHub, наприклад щоб:
|
||||
|
||||
* Поставити **питання** або запитати про **проблему**.
|
||||
* Запропонувати нову **функцію**.
|
||||
|
||||
**Примітка**: якщо ви це зробите, я також попрошу вас допомагати іншим. 😉
|
||||
|
||||
## Рецензувати Pull Requests { #review-pull-requests }
|
||||
|
||||
Ви можете допомогти мені рецензувати pull requests від інших.
|
||||
|
||||
Знову ж, будь ласка, намагайтеся бути доброзичливими. 🤗
|
||||
|
||||
---
|
||||
|
||||
Ось що варто пам’ятати і як рецензувати pull request:
|
||||
|
||||
### Зрозуміти проблему { #understand-the-problem }
|
||||
|
||||
* Спочатку переконайтеся, що ви **розумієте проблему**, яку pull request намагається розв’язати. Можливо, є довше обговорення в GitHub Discussion або issue.
|
||||
|
||||
* Також цілком можливо, що pull request насправді не потрібен, бо проблему можна розв’язати **іншим способом**. Тоді ви можете запропонувати або запитати про це.
|
||||
|
||||
### Не турбуйтеся про стиль { #dont-worry-about-style }
|
||||
|
||||
* Не хвилюйтеся надто щодо таких речей, як стиль повідомлень комітів — я зроблю squash і merge, відредагувавши коміт вручну.
|
||||
|
||||
* Також не турбуйтеся про правила стилю — уже є автоматизовані інструменти, що це перевіряють.
|
||||
|
||||
А якщо потрібні ще якісь зміни стилю або узгодженості, я попрошу про це напряму або додам коміти поверх із потрібними змінами.
|
||||
|
||||
### Перевірити код { #check-the-code }
|
||||
|
||||
* Перевірте й прочитайте код: чи він має сенс, **запустіть його локально** і подивіться, чи він справді розв’язує проблему.
|
||||
|
||||
* Потім залиште **коментар**, що ви це зробили — так я знатиму, що ви справді це перевірили.
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
На жаль, я не можу просто довіряти PR, у яких є лише кілька схвалень.
|
||||
|
||||
Кілька разів траплялося, що є PR із 3, 5 або більше схваленнями, ймовірно тому, що опис виглядає привабливо, але коли я перевіряю PR, вони насправді зламані, містять баг або не розв’язують проблему, яку заявляють. 😅
|
||||
|
||||
Тому дуже важливо, щоб ви справді прочитали й запустили код і повідомили мені в коментарях, що ви це зробили. 🤓
|
||||
|
||||
///
|
||||
|
||||
* Якщо PR можна якось спростити, ви можете про це попросити, але не варто бути надто прискіпливими: може бути багато суб’єктивних поглядів (і в мене теж 🙈), тож краще зосередитися на основному.
|
||||
|
||||
### Тести { #tests }
|
||||
|
||||
* Допоможіть мені перевірити, що PR має **тести**.
|
||||
|
||||
* Перевірте, що тести **падають** до PR. 🚨
|
||||
|
||||
* Потім перевірте, що тести **проходять** після PR. ✅
|
||||
|
||||
* У багатьох PR немає тестів — ви можете **нагадати** додати тести або навіть **запропонувати** кілька тестів самі. Це одна з речей, що забирає найбільше часу, і тут ви можете дуже допомогти.
|
||||
|
||||
* Також прокоментуйте, що саме ви перевірили — так я знатиму, що ви це перевірили. 🤓
|
||||
|
||||
## Створити Pull Request { #create-a-pull-request }
|
||||
|
||||
Ви можете [зробити внесок](contributing.md){.internal-link target=_blank} у вихідний код через Pull Requests, наприклад:
|
||||
|
||||
* Виправити друкарську помилку, яку ви знайшли в документації.
|
||||
* Поділитися статтею, відео або подкастом, який ви створили або знайшли про FastAPI, <a href="https://github.com/fastapi/fastapi/edit/master/docs/en/data/external_links.yml" class="external-link" target="_blank">відредагувавши цей файл</a>.
|
||||
* Переконайтеся, що ви додаєте ваше посилання на початок відповідного розділу.
|
||||
* Допомогти [перекласти документацію](contributing.md#translations){.internal-link target=_blank} вашою мовою.
|
||||
* Ви також можете допомогти рецензувати переклади, створені іншими.
|
||||
* Запропонувати нові розділи документації.
|
||||
* Виправити наявний issue/баг.
|
||||
* Обов’язково додайте тести.
|
||||
* Додати нову функцію.
|
||||
* Обов’язково додайте тести.
|
||||
* Обов’язково додайте документацію, якщо це доречно.
|
||||
|
||||
## Допомагати підтримувати FastAPI { #help-maintain-fastapi }
|
||||
|
||||
Допоможіть мені підтримувати **FastAPI**! 🤓
|
||||
|
||||
Роботи дуже багато, і більшість із неї можете зробити **ВИ**.
|
||||
|
||||
Основні завдання, які ви можете виконувати вже зараз:
|
||||
|
||||
* [Допомагати іншим із питаннями в GitHub](#help-others-with-questions-in-github){.internal-link target=_blank} (див. розділ вище).
|
||||
* [Рецензувати Pull Requests](#review-pull-requests){.internal-link target=_blank} (див. розділ вище).
|
||||
|
||||
Саме ці два завдання **забирають найбільше часу**. Це основна робота з підтримки FastAPI.
|
||||
|
||||
Якщо ви допоможете мені з цим, **ви допоможете мені підтримувати FastAPI** і зробите так, щоб він **розвивався швидше та краще**. 🚀
|
||||
|
||||
## Приєднатися до чату { #join-the-chat }
|
||||
|
||||
Приєднуйтеся до 👥 <a href="https://discord.gg/VQjSZaeJmf" class="external-link" target="_blank">чат-сервера Discord</a> 👥 і поспілкуйтеся з іншими в спільноті FastAPI.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Для питань ставте їх у <a href="https://github.com/fastapi/fastapi/discussions/new?category=questions" class="external-link" target="_blank">GitHub Discussions</a> — шанс отримати допомогу від [FastAPI Experts](fastapi-people.md#fastapi-experts){.internal-link target=_blank} там значно вищий.
|
||||
|
||||
Використовуйте чат лише для інших загальних розмов.
|
||||
|
||||
///
|
||||
|
||||
### Не використовуйте чат для питань { #dont-use-the-chat-for-questions }
|
||||
|
||||
Майте на увазі, що оскільки чати дозволяють більш «вільну розмову», легко ставити питання, які надто загальні й складніші для відповіді — тож ви можете не отримати відповіді.
|
||||
|
||||
У GitHub шаблон допоможе вам сформулювати правильне питання так, щоб ви могли легше отримати хорошу відповідь або навіть розв’язати проблему самостійно ще до того, як запитаєте. А в GitHub я можу гарантувати, що завжди відповім на все, навіть якщо це займе деякий час. Я не можу особисто робити це в чат-системах. 😅
|
||||
|
||||
Розмови в чат-системах також не так легко шукати, як у GitHub, тож питання й відповіді можуть загубитися в потоці. І лише ті, що в GitHub, враховуються, щоб стати [FastAPI Expert](fastapi-people.md#fastapi-experts){.internal-link target=_blank}, тож найімовірніше, у GitHub ви отримаєте більше уваги.
|
||||
|
||||
З іншого боку, у чат-системах є тисячі користувачів, тож дуже ймовірно, що ви знайдете там когось для спілкування майже завжди. 😄
|
||||
|
||||
## Спонсорувати автора { #sponsor-the-author }
|
||||
|
||||
Якщо ваш **продукт/компанія** залежить від **FastAPI** або пов’язаний із ним і ви хочете охопити його користувачів, ви можете спонсорувати автора (мене) через <a href="https://github.com/sponsors/tiangolo" class="external-link" target="_blank">GitHub sponsors</a>. Залежно від рівня, ви можете отримати додаткові переваги, наприклад бейдж у документації. 🎁
|
||||
|
||||
---
|
||||
|
||||
Дякую! 🚀
|
||||
79
docs/uk/docs/history-design-future.md
Normal file
79
docs/uk/docs/history-design-future.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Історія, дизайн і майбутнє { #history-design-and-future }
|
||||
|
||||
Певний час тому <a href="https://github.com/fastapi/fastapi/issues/3#issuecomment-454956920" class="external-link" target="_blank">один користувач **FastAPI** запитав</a>:
|
||||
|
||||
> Яка історія цього проєкту? Здається, що він з’явився нізвідки й за кілька тижнів став чудовим [...]
|
||||
|
||||
Ось трохи цієї історії.
|
||||
|
||||
## Альтернативи { #alternatives }
|
||||
|
||||
Протягом кількох років я створював API зі складними вимогами (Machine Learning, розподілені системи, асинхронні задачі, NoSQL бази даних тощо), керуючи кількома командами розробників.
|
||||
|
||||
У межах цього мені потрібно було дослідити, протестувати й використовувати багато альтернатив.
|
||||
|
||||
Історія **FastAPI** значною мірою — це історія його попередників.
|
||||
|
||||
Як сказано в розділі [Альтернативи](alternatives.md){.internal-link target=_blank}:
|
||||
|
||||
<blockquote markdown="1">
|
||||
|
||||
**FastAPI** не існував би без попередньої роботи інших людей.
|
||||
|
||||
До цього було створено багато інструментів, які допомогли надихнути його появу.
|
||||
|
||||
Я уникав створення нового framework протягом кількох років. Спочатку я намагався реалізувати всі можливості, які охоплює **FastAPI**, використовуючи багато різних frameworks, plug-ins та інструментів.
|
||||
|
||||
Але в певний момент не залишилося іншого варіанту, окрім як створити щось, що надає всі ці можливості: взяти найкращі ідеї з попередніх інструментів і поєднати їх найкращим можливим способом, використовуючи можливості мови, яких раніше навіть не було (type hints у Python 3.6+).
|
||||
|
||||
</blockquote>
|
||||
|
||||
## Дослідження { #investigation }
|
||||
|
||||
Використовуючи всі попередні альтернативи, я мав можливість повчитися на кожній із них, взяти ідеї та поєднати їх найкращим способом, який я зміг знайти для себе та команд розробників, з якими працював.
|
||||
|
||||
Наприклад, було очевидно, що в ідеалі все має базуватися на стандартних Python type hints.
|
||||
|
||||
Також найкращим підходом було використання вже наявних стандартів.
|
||||
|
||||
Тож ще до того, як почати писати код **FastAPI**, я витратив кілька місяців на вивчення специфікацій OpenAPI, JSON Schema, OAuth2 тощо. Щоб зрозуміти їхні взаємозв’язки, перетини та відмінності.
|
||||
|
||||
## Дизайн { #design }
|
||||
|
||||
Потім я витратив певний час на проєктування «API» для розробника, яке я хотів мати як користувач (як розробник, що використовує FastAPI).
|
||||
|
||||
Я протестував кілька ідей у найпопулярніших Python-редакторах: PyCharm, VS Code, редакторах на базі Jedi.
|
||||
|
||||
Згідно з останнім <a href="https://www.jetbrains.com/research/python-developers-survey-2018/#development-tools" class="external-link" target="_blank">Python Developer Survey</a>, який охоплює близько 80% користувачів.
|
||||
|
||||
Це означає, що **FastAPI** спеціально тестували в редакторах, якими користуються 80% Python-розробників. А оскільки більшість інших редакторів зазвичай працюють подібно, усі його переваги мають працювати практично в усіх редакторах.
|
||||
|
||||
Так я зміг знайти найкращі способи максимально зменшити дублювання коду, мати автодоповнення всюди, перевірки типів і помилок тощо.
|
||||
|
||||
Усе це — так, щоб забезпечити найкращий досвід розробки для всіх розробників.
|
||||
|
||||
## Вимоги { #requirements }
|
||||
|
||||
Після тестування кількох альтернатив я вирішив, що використовуватиму <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">**Pydantic**</a> через його переваги.
|
||||
|
||||
Потім я зробив внесок у його розвиток, щоб забезпечити повну відповідність JSON Schema, підтримати різні способи визначення декларацій обмежень і покращити підтримку редакторів (перевірки типів, автодоповнення) на основі тестів у кількох редакторах.
|
||||
|
||||
Під час розробки я також зробив внесок у <a href="https://www.starlette.dev/" class="external-link" target="_blank">**Starlette**</a> — іншу ключову вимогу.
|
||||
|
||||
## Розробка { #development }
|
||||
|
||||
На момент, коли я почав створювати сам **FastAPI**, більшість частин уже були на місці: дизайн визначено, вимоги та інструменти готові, а знання про стандарти й специфікації були чіткими та свіжими.
|
||||
|
||||
## Майбутнє { #future }
|
||||
|
||||
На цьому етапі вже зрозуміло, що **FastAPI** з його ідеями є корисним для багатьох людей.
|
||||
|
||||
Його обирають замість попередніх альтернатив, бо він краще підходить для багатьох сценаріїв використання.
|
||||
|
||||
Багато розробників і команд уже залежать від **FastAPI** у своїх проєктах (включно зі мною та моєю командою).
|
||||
|
||||
Але попереду ще багато покращень і нових можливостей.
|
||||
|
||||
**FastAPI** має чудове майбутнє.
|
||||
|
||||
І [ваша допомога](help-fastapi.md){.internal-link target=_blank} буде дуже цінною.
|
||||
17
docs/uk/docs/how-to/authentication-error-status-code.md
Normal file
17
docs/uk/docs/how-to/authentication-error-status-code.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Використання старих кодів стану 403 для помилок автентифікації { #use-old-403-authentication-error-status-codes }
|
||||
|
||||
До версії FastAPI `0.122.0`, коли вбудовані утиліти безпеки повертали клієнту помилку після невдалої автентифікації, вони використовували код стану HTTP `403 Forbidden`.
|
||||
|
||||
Починаючи з FastAPI `0.122.0`, вони використовують більш доречний код стану HTTP `401 Unauthorized` і повертають у відповіді коректний заголовок `WWW-Authenticate`, дотримуючись специфікацій HTTP, <a href="https://datatracker.ietf.org/doc/html/rfc7235#section-3.1" class="external-link" target="_blank">RFC 7235</a>, <a href="https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized" class="external-link" target="_blank">RFC 9110</a>.
|
||||
|
||||
Але якщо з певної причини ваші клієнти залежать від старої поведінки, ви можете повернути її, перевизначивши метод `make_not_authenticated_error` у ваших класах безпеки.
|
||||
|
||||
Наприклад, ви можете створити підклас `HTTPBearer`, який повертає помилку `403 Forbidden` замість типової `401 Unauthorized`:
|
||||
|
||||
{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *}
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Зверніть увагу: функція повертає екземпляр винятку, а не піднімає його. Підняття виконується в іншій частині внутрішнього коду.
|
||||
|
||||
///
|
||||
56
docs/uk/docs/how-to/conditional-openapi.md
Normal file
56
docs/uk/docs/how-to/conditional-openapi.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Умовний OpenAPI { #conditional-openapi }
|
||||
|
||||
За потреби ви можете використати налаштування та змінні середовища, щоб умовно конфігурувати OpenAPI залежно від середовища, і навіть повністю його вимкнути.
|
||||
|
||||
## Про безпеку, API та документацію { #about-security-apis-and-docs }
|
||||
|
||||
Приховування інтерфейсів документації у production *не повинно* бути способом захисту вашого API.
|
||||
|
||||
Це не додає жодної додаткової безпеки для вашого API — *операції шляху* усе одно будуть доступні там, де вони є.
|
||||
|
||||
Якщо у вашому коді є вразливість безпеки, вона все одно існуватиме.
|
||||
|
||||
Приховування документації лише ускладнює розуміння того, як взаємодіяти з вашим API, і може ускладнити вам налагодження в production. Це можна розглядати просто як форму <a href="https://en.wikipedia.org/wiki/Security_through_obscurity" class="external-link" target="_blank">безпеки через неочевидність</a>.
|
||||
|
||||
Якщо ви хочете захистити свій API, є кілька кращих речей, які ви можете зробити, наприклад:
|
||||
|
||||
* Переконайтеся, що у вас є чітко визначені моделі Pydantic для тіл запитів і відповідей.
|
||||
* Налаштуйте всі необхідні дозволи та ролі за допомогою залежностей.
|
||||
* Ніколи не зберігайте паролі у відкритому вигляді, лише хеші паролів.
|
||||
* Реалізуйте та використовуйте добре відомі криптографічні інструменти, як-от pwdlib і JWT tokens тощо.
|
||||
* Додайте більш деталізований контроль доступу за допомогою OAuth2 scopes там, де це потрібно.
|
||||
* ...тощо.
|
||||
|
||||
Втім, у вас може бути дуже специфічний випадок використання, коли вам справді потрібно вимкнути документацію API для певного середовища (наприклад, для production) або залежно від конфігурацій зі змінних середовища.
|
||||
|
||||
## Умовний OpenAPI з налаштувань і змінних середовища { #conditional-openapi-from-settings-and-env-vars }
|
||||
|
||||
Ви можете легко використати ті самі налаштування Pydantic, щоб налаштувати згенерований OpenAPI та UI документації.
|
||||
|
||||
Наприклад:
|
||||
|
||||
{* ../../docs_src/conditional_openapi/tutorial001_py39.py hl[6,11] *}
|
||||
|
||||
Тут ми оголошуємо налаштування `openapi_url` з тим самим значенням за замовчуванням `"/openapi.json"`.
|
||||
|
||||
А потім використовуємо його під час створення застосунку `FastAPI`.
|
||||
|
||||
Далі ви можете вимкнути OpenAPI (включно з UI документації), встановивши змінну середовища `OPENAPI_URL` в порожній рядок, ось так:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ OPENAPI_URL= uvicorn main:app
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Тоді, якщо ви перейдете за URL `/openapi.json`, `/docs` або `/redoc`, ви просто отримаєте помилку `404 Not Found`, наприклад:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"detail": "Not Found"
|
||||
}
|
||||
```
|
||||
70
docs/uk/docs/how-to/configure-swagger-ui.md
Normal file
70
docs/uk/docs/how-to/configure-swagger-ui.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Налаштування Swagger UI { #configure-swagger-ui }
|
||||
|
||||
Ви можете налаштувати деякі додаткові <a href="https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/" class="external-link" target="_blank">параметри Swagger UI</a>.
|
||||
|
||||
Щоб налаштувати їх, передайте аргумент `swagger_ui_parameters` під час створення об’єкта застосунку `FastAPI()` або у функцію `get_swagger_ui_html()`.
|
||||
|
||||
`swagger_ui_parameters` приймає словник із конфігураціями, які передаються безпосередньо в Swagger UI.
|
||||
|
||||
FastAPI перетворює конфігурації на **JSON**, щоб зробити їх сумісними з JavaScript, адже саме це потрібно Swagger UI.
|
||||
|
||||
## Вимкнення підсвічування синтаксису { #disable-syntax-highlighting }
|
||||
|
||||
Наприклад, ви можете вимкнути підсвічування синтаксису в Swagger UI.
|
||||
|
||||
Без зміни налаштувань підсвічування синтаксису увімкнено за замовчуванням:
|
||||
|
||||
<img src="/img/tutorial/extending-openapi/image02.png">
|
||||
|
||||
Але ви можете вимкнути його, встановивши `syntaxHighlight` у `False`:
|
||||
|
||||
{* ../../docs_src/configure_swagger_ui/tutorial001_py39.py hl[3] *}
|
||||
|
||||
...і тоді Swagger UI більше не показуватиме підсвічування синтаксису:
|
||||
|
||||
<img src="/img/tutorial/extending-openapi/image03.png">
|
||||
|
||||
## Зміна теми { #change-the-theme }
|
||||
|
||||
Так само ви можете встановити тему підсвічування синтаксису ключем `"syntaxHighlight.theme"` (зверніть увагу, що посередині є крапка):
|
||||
|
||||
{* ../../docs_src/configure_swagger_ui/tutorial002_py39.py hl[3] *}
|
||||
|
||||
Ця конфігурація змінить колірну тему підсвічування синтаксису:
|
||||
|
||||
<img src="/img/tutorial/extending-openapi/image04.png">
|
||||
|
||||
## Зміна параметрів Swagger UI за замовчуванням { #change-default-swagger-ui-parameters }
|
||||
|
||||
FastAPI містить деякі стандартні параметри конфігурації, придатні для більшості випадків використання.
|
||||
|
||||
Він включає такі конфігурації за замовчуванням:
|
||||
|
||||
{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *}
|
||||
|
||||
Ви можете перевизначити будь-яку з них, встановивши інше значення в аргументі `swagger_ui_parameters`.
|
||||
|
||||
Наприклад, щоб вимкнути `deepLinking`, ви можете передати такі налаштування в `swagger_ui_parameters`:
|
||||
|
||||
{* ../../docs_src/configure_swagger_ui/tutorial003_py39.py hl[3] *}
|
||||
|
||||
## Інші параметри Swagger UI { #other-swagger-ui-parameters }
|
||||
|
||||
Щоб переглянути всі інші можливі конфігурації, які ви можете використати, прочитайте офіційну <a href="https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/" class="external-link" target="_blank">документацію з параметрів Swagger UI</a>.
|
||||
|
||||
## Налаштування лише для JavaScript { #javascript-only-settings }
|
||||
|
||||
Swagger UI також дозволяє інші конфігурації у вигляді об’єктів, що є **лише для JavaScript** (наприклад, функції JavaScript).
|
||||
|
||||
FastAPI також включає ці налаштування `presets`, що є лише для JavaScript:
|
||||
|
||||
```JavaScript
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
]
|
||||
```
|
||||
|
||||
Це об’єкти **JavaScript**, а не рядки, тож ви не можете передати їх напряму з Python-коду.
|
||||
|
||||
Якщо вам потрібно використати конфігурації лише для JavaScript, подібні до цих, ви можете застосувати один із методів вище: перевизначити всі операції шляху Swagger UI та вручну написати потрібний вам JavaScript.
|
||||
185
docs/uk/docs/how-to/custom-docs-ui-assets.md
Normal file
185
docs/uk/docs/how-to/custom-docs-ui-assets.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# Власні статичні ресурси для UI документації (самостійне хостингування) { #custom-docs-ui-static-assets-self-hosting }
|
||||
|
||||
Документація API використовує **Swagger UI** і **ReDoc**, і кожному з них потрібні деякі файли JavaScript і CSS.
|
||||
|
||||
За замовчуванням ці файли віддаються через <abbr title="Content Delivery Network - Мережа доставки контенту: сервіс, зазвичай складений із кількох серверів, що надає статичні файли, як-от JavaScript і CSS. Його часто використовують, щоб віддавати ці файли із сервера, ближчого до клієнта, покращуючи продуктивність.">CDN</abbr>.
|
||||
|
||||
Але це можна налаштувати: ви можете вказати конкретний CDN або віддавати файли самостійно.
|
||||
|
||||
## Власний CDN для JavaScript і CSS { #custom-cdn-for-javascript-and-css }
|
||||
|
||||
Припустімо, ви хочете використовувати інший <abbr title="Content Delivery Network - Мережа доставки контенту">CDN</abbr>, наприклад `https://unpkg.com/`.
|
||||
|
||||
Це може бути корисно, наприклад, якщо ви живете в країні, яка обмежує доступ до деяких URL.
|
||||
|
||||
### Вимкнути автоматичну документацію { #disable-the-automatic-docs }
|
||||
|
||||
Перший крок — вимкнути автоматичну документацію, адже за замовчуванням вона використовує стандартний CDN.
|
||||
|
||||
Щоб вимкнути її, встановіть її URL у `None` під час створення застосунку `FastAPI`:
|
||||
|
||||
{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[8] *}
|
||||
|
||||
### Додати власну документацію { #include-the-custom-docs }
|
||||
|
||||
Тепер ви можете створити *операції шляху* для власної документації.
|
||||
|
||||
Ви можете повторно використати внутрішні функції FastAPI для створення HTML-сторінок документації та передати їм потрібні аргументи:
|
||||
|
||||
* `openapi_url`: URL, звідки HTML-сторінка документації може отримати схему OpenAPI для вашого API. Тут можна використати атрибут `app.openapi_url`.
|
||||
* `title`: заголовок вашого API.
|
||||
* `oauth2_redirect_url`: тут можна використати `app.swagger_ui_oauth2_redirect_url`, щоб застосувати значення за замовчуванням.
|
||||
* `swagger_js_url`: URL, звідки HTML для документації Swagger UI може отримати файл **JavaScript**. Це URL вашого власного CDN.
|
||||
* `swagger_css_url`: URL, звідки HTML для документації Swagger UI може отримати файл **CSS**. Це URL вашого власного CDN.
|
||||
|
||||
І так само для ReDoc...
|
||||
|
||||
{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[2:6,11:19,22:24,27:33] *}
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
*Операція шляху* для `swagger_ui_redirect` — це допоміжний обробник для випадку, коли ви використовуєте OAuth2.
|
||||
|
||||
Якщо ви інтегруєте ваш API з OAuth2-провайдером, ви зможете пройти автентифікацію, повернутися до документації API з отриманими обліковими даними та взаємодіяти з ним, використовуючи справжню OAuth2-автентифікацію.
|
||||
|
||||
Swagger UI зробить це «за лаштунками» для вас, але для цього йому потрібен цей допоміжний «redirect».
|
||||
|
||||
///
|
||||
|
||||
### Створити *операцію шляху* для перевірки { #create-a-path-operation-to-test-it }
|
||||
|
||||
Тепер, щоб мати змогу перевірити, що все працює, створіть *операцію шляху*:
|
||||
|
||||
{* ../../docs_src/custom_docs_ui/tutorial001_py39.py hl[36:38] *}
|
||||
|
||||
### Перевірка { #test-it }
|
||||
|
||||
Тепер ви маєте змогу перейти до документації за адресою <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> і перезавантажити сторінку — вона завантажить ці ресурси з нового CDN.
|
||||
|
||||
## Самостійне хостингування JavaScript і CSS для документації { #self-hosting-javascript-and-css-for-docs }
|
||||
|
||||
Самостійне хостингування JavaScript і CSS може бути корисним, наприклад, якщо ваш застосунок має продовжувати працювати навіть офлайн — без доступу до відкритого Інтернету — або в локальній мережі.
|
||||
|
||||
Тут ви побачите, як віддавати ці файли самостійно, у тому ж застосунку FastAPI, і налаштувати документацію так, щоб вона їх використовувала.
|
||||
|
||||
### Структура файлів проєкту { #project-file-structure }
|
||||
|
||||
Припустімо, структура файлів вашого проєкту виглядає так:
|
||||
|
||||
```
|
||||
.
|
||||
├── app
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py
|
||||
```
|
||||
|
||||
Тепер створіть директорію для зберігання цих статичних файлів.
|
||||
|
||||
Нова структура файлів може виглядати так:
|
||||
|
||||
```
|
||||
.
|
||||
├── app
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py
|
||||
└── static/
|
||||
```
|
||||
|
||||
### Завантажити файли { #download-the-files }
|
||||
|
||||
Завантажте статичні файли, потрібні для документації, і покладіть їх у директорію `static/`.
|
||||
|
||||
Ймовірно, ви можете натиснути правою кнопкою миші на кожне посилання та вибрати щось на кшталт «Save link as...».
|
||||
|
||||
**Swagger UI** використовує файли:
|
||||
|
||||
* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js" class="external-link" target="_blank">`swagger-ui-bundle.js`</a>
|
||||
* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css" class="external-link" target="_blank">`swagger-ui.css`</a>
|
||||
|
||||
А **ReDoc** використовує файл:
|
||||
|
||||
* <a href="https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js" class="external-link" target="_blank">`redoc.standalone.js`</a>
|
||||
|
||||
Після цього структура файлів може виглядати так:
|
||||
|
||||
```
|
||||
.
|
||||
├── app
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py
|
||||
└── static
|
||||
├── redoc.standalone.js
|
||||
├── swagger-ui-bundle.js
|
||||
└── swagger-ui.css
|
||||
```
|
||||
|
||||
### Віддавати статичні файли { #serve-the-static-files }
|
||||
|
||||
* Імпортуйте `StaticFiles`.
|
||||
* «Змонтуйте» екземпляр `StaticFiles()` на конкретному шляху.
|
||||
|
||||
{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[7,11] *}
|
||||
|
||||
### Перевірити статичні файли { #test-the-static-files }
|
||||
|
||||
Запустіть ваш застосунок і перейдіть за адресою <a href="http://127.0.0.1:8000/static/redoc.standalone.js" class="external-link" target="_blank">http://127.0.0.1:8000/static/redoc.standalone.js</a>.
|
||||
|
||||
Ви маєте побачити дуже довгий JavaScript-файл для **ReDoc**.
|
||||
|
||||
Він може починатися приблизно так:
|
||||
|
||||
```JavaScript
|
||||
/*! For license information please see redoc.standalone.js.LICENSE.txt */
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("null")):
|
||||
...
|
||||
```
|
||||
|
||||
Це підтверджує, що ви можете віддавати статичні файли зі свого застосунку і що ви розмістили статичні файли для документації в правильному місці.
|
||||
|
||||
Тепер ми можемо налаштувати застосунок так, щоб він використовував ці статичні файли для документації.
|
||||
|
||||
### Вимкнути автоматичну документацію для статичних файлів { #disable-the-automatic-docs-for-static-files }
|
||||
|
||||
Так само, як і при використанні власного CDN, перший крок — вимкнути автоматичну документацію, адже за замовчуванням вона використовує CDN.
|
||||
|
||||
Щоб вимкнути її, встановіть її URL у `None` під час створення застосунку `FastAPI`:
|
||||
|
||||
{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[9] *}
|
||||
|
||||
### Додати власну документацію для статичних файлів { #include-the-custom-docs-for-static-files }
|
||||
|
||||
І так само, як із власним CDN, тепер ви можете створити *операції шляху* для власної документації.
|
||||
|
||||
Знову ж таки, ви можете повторно використати внутрішні функції FastAPI для створення HTML-сторінок документації та передати їм потрібні аргументи:
|
||||
|
||||
* `openapi_url`: URL, звідки HTML-сторінка документації може отримати схему OpenAPI для вашого API. Тут можна використати атрибут `app.openapi_url`.
|
||||
* `title`: заголовок вашого API.
|
||||
* `oauth2_redirect_url`: тут можна використати `app.swagger_ui_oauth2_redirect_url`, щоб застосувати значення за замовчуванням.
|
||||
* `swagger_js_url`: URL, звідки HTML для документації Swagger UI може отримати файл **JavaScript**. **Тепер його віддає ваш власний застосунок**.
|
||||
* `swagger_css_url`: URL, звідки HTML для документації Swagger UI може отримати файл **CSS**. **Тепер його віддає ваш власний застосунок**.
|
||||
|
||||
І так само для ReDoc...
|
||||
|
||||
{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[2:6,14:22,25:27,30:36] *}
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
*Операція шляху* для `swagger_ui_redirect` — це допоміжний обробник для випадку, коли ви використовуєте OAuth2.
|
||||
|
||||
Якщо ви інтегруєте ваш API з OAuth2-провайдером, ви зможете пройти автентифікацію, повернутися до документації API з отриманими обліковими даними та взаємодіяти з ним, використовуючи справжню OAuth2-автентифікацію.
|
||||
|
||||
Swagger UI зробить це «за лаштунками» для вас, але для цього йому потрібен цей допоміжний «redirect».
|
||||
|
||||
///
|
||||
|
||||
### Створити *операцію шляху* для перевірки статичних файлів { #create-a-path-operation-to-test-static-files }
|
||||
|
||||
Тепер, щоб мати змогу перевірити, що все працює, створіть *операцію шляху*:
|
||||
|
||||
{* ../../docs_src/custom_docs_ui/tutorial002_py39.py hl[39:41] *}
|
||||
|
||||
### Перевірити UI зі статичними файлами { #test-static-files-ui }
|
||||
|
||||
Тепер ви маєте змогу від’єднати WiFi, перейти до документації за адресою <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> і перезавантажити сторінку.
|
||||
|
||||
І навіть без Інтернету ви зможете бачити документацію вашого API та взаємодіяти з ним.
|
||||
109
docs/uk/docs/how-to/custom-request-and-route.md
Normal file
109
docs/uk/docs/how-to/custom-request-and-route.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Власні класи Request і APIRoute { #custom-request-and-apiroute-class }
|
||||
|
||||
У деяких випадках ви можете захотіти перевизначити логіку, яку використовують класи `Request` і `APIRoute`.
|
||||
|
||||
Зокрема, це може бути хорошою альтернативою логіці в middleware.
|
||||
|
||||
Наприклад, якщо ви хочете прочитати або змінити тіло запиту до того, як його обробить ваш застосунок.
|
||||
|
||||
/// danger | Обережно
|
||||
|
||||
Це «просунута» можливість.
|
||||
|
||||
Якщо ви лише починаєте працювати з **FastAPI**, вам, імовірно, варто пропустити цей розділ.
|
||||
|
||||
///
|
||||
|
||||
## Випадки використання { #use-cases }
|
||||
|
||||
Деякі випадки використання:
|
||||
|
||||
* Перетворення не-JSON тіл запитів на JSON (наприклад, <a href="https://msgpack.org/index.html" class="external-link" target="_blank">`msgpack`</a>).
|
||||
* Розпаковування gzip-стиснених тіл запитів.
|
||||
* Автоматичне логування всіх тіл запитів.
|
||||
|
||||
## Обробка власних кодувань тіла запиту { #handling-custom-request-body-encodings }
|
||||
|
||||
Розгляньмо, як використати власний підклас `Request`, щоб розпаковувати gzip-запити.
|
||||
|
||||
А також підклас `APIRoute`, щоб використовувати цей власний клас запиту.
|
||||
|
||||
### Створіть власний клас `GzipRequest` { #create-a-custom-gziprequest-class }
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Це навчальний приклад, щоб продемонструвати, як це працює. Якщо вам потрібна підтримка Gzip, ви можете використати наданий [`GzipMiddleware`](../advanced/middleware.md#gzipmiddleware){.internal-link target=_blank}.
|
||||
|
||||
///
|
||||
|
||||
Спочатку створюємо клас `GzipRequest`, який перевизначить метод `Request.body()`, щоб розпаковувати тіло за наявності відповідного заголовка.
|
||||
|
||||
Якщо в заголовку немає `gzip`, він не намагатиметься розпаковувати тіло.
|
||||
|
||||
Так один і той самий клас маршруту може обробляти як gzip-стиснені, так і нестиснені запити.
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *}
|
||||
|
||||
### Створіть власний клас `GzipRoute` { #create-a-custom-gziproute-class }
|
||||
|
||||
Далі створюємо власний підклас `fastapi.routing.APIRoute`, який використовуватиме `GzipRequest`.
|
||||
|
||||
Цього разу він перевизначить метод `APIRoute.get_route_handler()`.
|
||||
|
||||
Цей метод повертає функцію. І саме ця функція отримуватиме запит і повертатиме відповідь.
|
||||
|
||||
Тут ми використовуємо її, щоб створити `GzipRequest` з початкового запиту.
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[19:27] *}
|
||||
|
||||
/// note | Технічні деталі
|
||||
|
||||
`Request` має атрибут `request.scope` — це просто Python-`dict`, що містить метадані, пов’язані із запитом.
|
||||
|
||||
`Request` також має `request.receive` — це функція для «отримання» тіла запиту.
|
||||
|
||||
`dict` `scope` і функція `receive` обидва є частиною специфікації ASGI.
|
||||
|
||||
І саме ці дві речі — `scope` та `receive` — потрібні, щоб створити новий екземпляр `Request`.
|
||||
|
||||
Щоб дізнатися більше про `Request`, перегляньте <a href="https://www.starlette.dev/requests/" class="external-link" target="_blank">документацію Starlette про Requests</a>.
|
||||
|
||||
///
|
||||
|
||||
Єдина відмінність у роботі функції, яку повертає `GzipRequest.get_route_handler`, — це перетворення `Request` на `GzipRequest`.
|
||||
|
||||
Завдяки цьому наш `GzipRequest` подбає про розпаковування даних (за потреби) перед тим, як передати їх у наші *операції шляху*.
|
||||
|
||||
Після цього вся логіка обробки залишається тією самою.
|
||||
|
||||
Але через наші зміни в `GzipRequest.body` тіло запиту буде автоматично розпаковане, коли **FastAPI** завантажуватиме його за потреби.
|
||||
|
||||
## Доступ до тіла запиту в обробнику винятків { #accessing-the-request-body-in-an-exception-handler }
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Щоб розв’язати цю саму задачу, ймовірно, значно простіше використати `body` у власному обробнику для `RequestValidationError` ([Обробка помилок](../tutorial/handling-errors.md#use-the-requestvalidationerror-body){.internal-link target=_blank}).
|
||||
|
||||
Але цей приклад усе одно коректний і показує, як взаємодіяти з внутрішніми компонентами.
|
||||
|
||||
///
|
||||
|
||||
Ми також можемо використати цей самий підхід, щоб отримати доступ до тіла запиту в обробнику винятків.
|
||||
|
||||
Усе, що потрібно зробити, — обробляти запит усередині блоку `try`/`except`:
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[14,16] *}
|
||||
|
||||
Якщо станеться виняток, екземпляр `Request` усе ще буде в області видимості, тож ми зможемо прочитати та використати тіло запиту під час обробки помилки:
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[17:19] *}
|
||||
|
||||
## Власний клас `APIRoute` у router { #custom-apiroute-class-in-a-router }
|
||||
|
||||
Ви також можете встановити параметр `route_class` для `APIRouter`:
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[26] *}
|
||||
|
||||
У цьому прикладі *операції шляху* під `router` використовуватимуть власний клас `TimedRoute` і матимуть додатковий заголовок `X-Response-Time` у відповіді з часом, який знадобився для генерування відповіді:
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[13:20] *}
|
||||
80
docs/uk/docs/how-to/extending-openapi.md
Normal file
80
docs/uk/docs/how-to/extending-openapi.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Розширення OpenAPI { #extending-openapi }
|
||||
|
||||
У деяких випадках вам може знадобитися змінити згенеровану схему OpenAPI.
|
||||
|
||||
У цьому розділі ви побачите, як це зробити.
|
||||
|
||||
## Звичайний процес { #the-normal-process }
|
||||
|
||||
Звичайний (типовий) процес виглядає так.
|
||||
|
||||
Застосунок `FastAPI` (екземпляр) має метод `.openapi()`, який має повертати схему OpenAPI.
|
||||
|
||||
Під час створення об’єкта застосунку реєструється *операція шляху* для `/openapi.json` (або для того, що ви вкажете в `openapi_url`).
|
||||
|
||||
Вона просто повертає JSON-відповідь з результатом методу `.openapi()` застосунку.
|
||||
|
||||
Типово метод `.openapi()` перевіряє властивість `.openapi_schema`, щоб з’ясувати, чи містить вона дані, і повертає їх.
|
||||
|
||||
Якщо даних немає, він генерує їх за допомогою утилітарної функції `fastapi.openapi.utils.get_openapi`.
|
||||
|
||||
І ця функція `get_openapi()` отримує як параметри:
|
||||
|
||||
* `title`: заголовок OpenAPI, який показується в документації.
|
||||
* `version`: версія вашого API, напр. `2.5.0`.
|
||||
* `openapi_version`: версія специфікації OpenAPI, що використовується. Типово — найновіша: `3.1.0`.
|
||||
* `summary`: короткий підсумок API.
|
||||
* `description`: опис вашого API; може містити markdown і буде показаний у документації.
|
||||
* `routes`: список маршрутів; кожен з них — це зареєстрована *операція шляху*. Їх беруть з `app.routes`.
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Параметр `summary` доступний в OpenAPI 3.1.0 і вище, підтримується у FastAPI 0.99.0 і вище.
|
||||
|
||||
///
|
||||
|
||||
## Перевизначення типових значень { #overriding-the-defaults }
|
||||
|
||||
Використовуючи наведені вище відомості, ви можете застосувати ту саму утилітарну функцію, щоб згенерувати схему OpenAPI і перевизначити кожну потрібну вам частину.
|
||||
|
||||
Наприклад, додаймо <a href="https://github.com/Rebilly/ReDoc/blob/master/docs/redoc-vendor-extensions.md#x-logo" class="external-link" target="_blank">розширення OpenAPI для ReDoc, щоб включити власний логотип</a>.
|
||||
|
||||
### Звичайний **FastAPI** { #normal-fastapi }
|
||||
|
||||
Спочатку напишіть увесь ваш застосунок **FastAPI** як зазвичай:
|
||||
|
||||
{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[1,4,7:9] *}
|
||||
|
||||
### Згенеруйте схему OpenAPI { #generate-the-openapi-schema }
|
||||
|
||||
Потім використайте ту саму утилітарну функцію, щоб згенерувати схему OpenAPI всередині функції `custom_openapi()`:
|
||||
|
||||
{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[2,15:21] *}
|
||||
|
||||
### Змініть схему OpenAPI { #modify-the-openapi-schema }
|
||||
|
||||
Тепер ви можете додати розширення ReDoc, додавши власний `x-logo` до «об’єкта» `info` у схемі OpenAPI:
|
||||
|
||||
{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[22:24] *}
|
||||
|
||||
### Кешуйте схему OpenAPI { #cache-the-openapi-schema }
|
||||
|
||||
Ви можете використати властивість `.openapi_schema` як «кеш», щоб зберігати згенеровану схему.
|
||||
|
||||
Так ваш застосунок не муситиме генерувати схему щоразу, коли користувач відкриває вашу документацію API.
|
||||
|
||||
Її буде згенеровано лише один раз, а потім для наступних запитів використовуватиметься та сама закешована схема.
|
||||
|
||||
{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[13:14,25:26] *}
|
||||
|
||||
### Перевизначте метод { #override-the-method }
|
||||
|
||||
Тепер ви можете замінити метод `.openapi()` вашою новою функцією.
|
||||
|
||||
{* ../../docs_src/extending_openapi/tutorial001_py39.py hl[29] *}
|
||||
|
||||
### Перевірте { #check-it }
|
||||
|
||||
Після переходу за посиланням <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> ви побачите, що використовується ваш власний логотип (у цьому прикладі — логотип **FastAPI**):
|
||||
|
||||
<img src="/img/tutorial/extending-openapi/image01.png">
|
||||
39
docs/uk/docs/how-to/general.md
Normal file
39
docs/uk/docs/how-to/general.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Загальне — Як зробити — Рецепти { #general-how-to-recipes }
|
||||
|
||||
Ось кілька посилань на інші місця в документації для загальних або частих запитань.
|
||||
|
||||
## Фільтрування даних — Безпека { #filter-data-security }
|
||||
|
||||
Щоб гарантувати, що ви не повернете більше даних, ніж слід, прочитайте документацію: [Підручник — Модель відповіді — Тип повернення](../tutorial/response-model.md){.internal-link target=_blank}.
|
||||
|
||||
## Теги документації — OpenAPI { #documentation-tags-openapi }
|
||||
|
||||
Щоб додати теги до ваших *операцій шляху* та згрупувати їх в інтерфейсі документації, прочитайте документацію: [Підручник — Налаштування операцій шляху — Теги](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank}.
|
||||
|
||||
## Підсумок і опис документації — OpenAPI { #documentation-summary-and-description-openapi }
|
||||
|
||||
Щоб додати підсумок і опис до ваших *операцій шляху* та показувати їх в інтерфейсі документації, прочитайте документацію: [Підручник — Налаштування операцій шляху — Підсумок і опис](../tutorial/path-operation-configuration.md#summary-and-description){.internal-link target=_blank}.
|
||||
|
||||
## Опис відповіді в документації — OpenAPI { #documentation-response-description-openapi }
|
||||
|
||||
Щоб визначити опис відповіді, який показується в інтерфейсі документації, прочитайте документацію: [Підручник — Налаштування операцій шляху — Опис відповіді](../tutorial/path-operation-configuration.md#response-description){.internal-link target=_blank}.
|
||||
|
||||
## Позначення *операції шляху* як застарілої — OpenAPI { #documentation-deprecate-a-path-operation-openapi }
|
||||
|
||||
Щоб позначити *операцію шляху* як застарілу та показувати це в інтерфейсі документації, прочитайте документацію: [Підручник — Налаштування операцій шляху — Deprecation](../tutorial/path-operation-configuration.md#deprecate-a-path-operation){.internal-link target=_blank}.
|
||||
|
||||
## Перетворення будь-яких даних на JSON-сумісні { #convert-any-data-to-json-compatible }
|
||||
|
||||
Щоб перетворити будь-які дані на JSON-сумісні, прочитайте документацію: [Підручник — JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}.
|
||||
|
||||
## Метадані OpenAPI — Документація { #openapi-metadata-docs }
|
||||
|
||||
Щоб додати метадані до вашої схеми OpenAPI, включно з ліцензією, версією, контактами тощо, прочитайте документацію: [Підручник — Метадані та URL-адреси документації](../tutorial/metadata.md){.internal-link target=_blank}.
|
||||
|
||||
## Користувацька URL-адреса OpenAPI { #openapi-custom-url }
|
||||
|
||||
Щоб налаштувати URL-адресу OpenAPI (або прибрати її), прочитайте документацію: [Підручник — Метадані та URL-адреси документації](../tutorial/metadata.md#openapi-url){.internal-link target=_blank}.
|
||||
|
||||
## URL-адреси документації OpenAPI { #openapi-docs-urls }
|
||||
|
||||
Щоб оновити URL-адреси, які використовуються для автоматично згенерованих інтерфейсів документації, прочитайте документацію: [Підручник — Метадані та URL-адреси документації](../tutorial/metadata.md#docs-urls){.internal-link target=_blank}.
|
||||
60
docs/uk/docs/how-to/graphql.md
Normal file
60
docs/uk/docs/how-to/graphql.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# GraphQL { #graphql }
|
||||
|
||||
Оскільки **FastAPI** базується на стандарті **ASGI**, дуже легко інтегрувати будь-яку бібліотеку **GraphQL**, яка також сумісна з ASGI.
|
||||
|
||||
Ви можете поєднувати звичайні FastAPI *операції шляху* з GraphQL в одному застосунку.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
**GraphQL** розв’язує деякі дуже специфічні випадки використання.
|
||||
|
||||
Він має **переваги** та **недоліки** порівняно зі звичними **web API**.
|
||||
|
||||
Переконайтеся, що ви оцінили, чи **переваги** для вашого випадку використання компенсують **недоліки**. 🤓
|
||||
|
||||
///
|
||||
|
||||
## Бібліотеки GraphQL { #graphql-libraries }
|
||||
|
||||
Ось деякі бібліотеки **GraphQL**, які мають підтримку **ASGI**. Ви можете використовувати їх із **FastAPI**:
|
||||
|
||||
* <a href="https://strawberry.rocks/" class="external-link" target="_blank">Strawberry</a> 🍓
|
||||
* З <a href="https://strawberry.rocks/docs/integrations/fastapi" class="external-link" target="_blank">документацією для FastAPI</a>
|
||||
* <a href="https://ariadnegraphql.org/" class="external-link" target="_blank">Ariadne</a>
|
||||
* З <a href="https://ariadnegraphql.org/docs/fastapi-integration" class="external-link" target="_blank">документацією для FastAPI</a>
|
||||
* <a href="https://tartiflette.io/" class="external-link" target="_blank">Tartiflette</a>
|
||||
* Із <a href="https://tartiflette.github.io/tartiflette-asgi/" class="external-link" target="_blank">Tartiflette ASGI</a> для забезпечення інтеграції з ASGI
|
||||
* <a href="https://graphene-python.org/" class="external-link" target="_blank">Graphene</a>
|
||||
* Із <a href="https://github.com/ciscorn/starlette-graphene3" class="external-link" target="_blank">starlette-graphene3</a>
|
||||
|
||||
## GraphQL зі Strawberry { #graphql-with-strawberry }
|
||||
|
||||
Якщо вам потрібно або ви хочете працювати з **GraphQL**, <a href="https://strawberry.rocks/" class="external-link" target="_blank">**Strawberry**</a> — **рекомендована** бібліотека, адже її дизайн найближчий до дизайну **FastAPI**: усе базується на **анотаціях типів**.
|
||||
|
||||
Залежно від вашого випадку використання, ви можете віддати перевагу іншій бібліотеці, але якби ви запитали мене, я б, імовірно, порадив вам спробувати **Strawberry**.
|
||||
|
||||
Ось невеликий приклад того, як ви можете інтегрувати Strawberry з FastAPI:
|
||||
|
||||
{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *}
|
||||
|
||||
Докладніше про Strawberry можна дізнатися в <a href="https://strawberry.rocks/" class="external-link" target="_blank">документації Strawberry</a>.
|
||||
|
||||
А також у документації про <a href="https://strawberry.rocks/docs/integrations/fastapi" class="external-link" target="_blank">Strawberry з FastAPI</a>.
|
||||
|
||||
## Старий `GraphQLApp` зі Starlette { #older-graphqlapp-from-starlette }
|
||||
|
||||
Попередні версії Starlette містили клас `GraphQLApp` для інтеграції з <a href="https://graphene-python.org/" class="external-link" target="_blank">Graphene</a>.
|
||||
|
||||
Його позначили застарілим у Starlette, але якщо у вас є код, який його використовував, ви можете легко **мігрувати** на <a href="https://github.com/ciscorn/starlette-graphene3" class="external-link" target="_blank">starlette-graphene3</a>, що покриває той самий випадок використання та має **майже ідентичний інтерфейс**.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Якщо вам потрібен GraphQL, я все одно рекомендую вам звернути увагу на <a href="https://strawberry.rocks/" class="external-link" target="_blank">Strawberry</a>, адже він базується на анотаціях типів, а не на власних класах і типах.
|
||||
|
||||
///
|
||||
|
||||
## Дізнатися більше { #learn-more }
|
||||
|
||||
Детальніше про **GraphQL** можна дізнатися в <a href="https://graphql.org/" class="external-link" target="_blank">офіційній документації GraphQL</a>.
|
||||
|
||||
Ви також можете прочитати більше про кожну з бібліотек, описаних вище, за наведеними посиланнями.
|
||||
13
docs/uk/docs/how-to/index.md
Normal file
13
docs/uk/docs/how-to/index.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Як зробити — Рецепти { #how-to-recipes }
|
||||
|
||||
Тут ви знайдете різні рецепти або інструкції «how to» для **кількох тем**.
|
||||
|
||||
Більшість цих ідей є більш-менш **незалежними**, і в більшості випадків вам потрібно буде вивчати їх лише тоді, коли вони безпосередньо стосуються **вашого проєкту**.
|
||||
|
||||
Якщо щось здається цікавим і корисним для вашого проєкту — перегляньте це, а інакше, ймовірно, можете просто пропустити.
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Якщо ви хочете **вивчати FastAPI** структуровано (рекомендовано), натомість читайте розділ [Підручник — Посібник користувача](../tutorial/index.md){.internal-link target=_blank} главу за главою.
|
||||
|
||||
///
|
||||
135
docs/uk/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md
Normal file
135
docs/uk/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Міграція з Pydantic v1 на Pydantic v2 { #migrate-from-pydantic-v1-to-pydantic-v2 }
|
||||
|
||||
Якщо у вас старий застосунок FastAPI, можливо, ви використовуєте Pydantic версії 1.
|
||||
|
||||
FastAPI версії 0.100.0 підтримував або Pydantic v1, або v2. Він використовував ту версію, яку ви встановили.
|
||||
|
||||
FastAPI версії 0.119.0 представив часткову підтримку Pydantic v1 усередині Pydantic v2 (як `pydantic.v1`), щоб полегшити міграцію на v2.
|
||||
|
||||
FastAPI 0.126.0 припинив підтримку Pydantic v1, водночас ще деякий час підтримуючи `pydantic.v1`.
|
||||
|
||||
/// warning | Попередження
|
||||
|
||||
Команда Pydantic припинила підтримку Pydantic v1 для найновіших версій Python, починаючи з **Python 3.14**.
|
||||
|
||||
Це також стосується `pydantic.v1`, який більше не підтримується в Python 3.14 і вище.
|
||||
|
||||
Якщо ви хочете використовувати найновіші можливості Python, вам потрібно переконатися, що ви використовуєте Pydantic v2.
|
||||
|
||||
///
|
||||
|
||||
Якщо у вас старий застосунок FastAPI з Pydantic v1, тут я покажу, як мігрувати на Pydantic v2, і **можливості у FastAPI 0.119.0**, що допомагають виконати поступову міграцію.
|
||||
|
||||
## Офіційний посібник { #official-guide }
|
||||
|
||||
Pydantic має офіційний <a href="https://docs.pydantic.dev/latest/migration/" class="external-link" target="_blank">посібник з міграції</a> з v1 на v2.
|
||||
|
||||
Він також містить інформацію про те, що змінилося, як валідації тепер є коректнішими та суворішими, можливі застереження тощо.
|
||||
|
||||
Ви можете прочитати його, щоб краще зрозуміти, що змінилося.
|
||||
|
||||
## Тести { #tests }
|
||||
|
||||
Переконайтеся, що у вас є [тести](../tutorial/testing.md){.internal-link target=_blank} для вашого застосунку та ви запускаєте їх у безперервній інтеграції (CI).
|
||||
|
||||
Так ви зможете виконати оновлення й переконатися, що все й далі працює як очікується.
|
||||
|
||||
## `bump-pydantic` { #bump-pydantic }
|
||||
|
||||
У багатьох випадках, коли ви використовуєте звичайні моделі Pydantic без налаштувань, ви зможете автоматизувати більшість процесу міграції з Pydantic v1 на Pydantic v2.
|
||||
|
||||
Ви можете використати <a href="https://github.com/pydantic/bump-pydantic" class="external-link" target="_blank">`bump-pydantic`</a> від тієї ж команди Pydantic.
|
||||
|
||||
Цей інструмент допоможе вам автоматично змінити більшість коду, який потрібно змінити.
|
||||
|
||||
Після цього ви можете запустити тести й перевірити, чи все працює. Якщо так — готово. 😎
|
||||
|
||||
## Pydantic v1 у v2 { #pydantic-v1-in-v2 }
|
||||
|
||||
Pydantic v2 включає все з Pydantic v1 як підмодуль `pydantic.v1`. Але це більше не підтримується у версіях вище Python 3.13.
|
||||
|
||||
Це означає, що ви можете встановити найновішу версію Pydantic v2, імпортувати та використовувати старі компоненти Pydantic v1 з цього підмодуля — так, ніби у вас встановлено старий Pydantic v1.
|
||||
|
||||
{* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *}
|
||||
|
||||
### Підтримка FastAPI для Pydantic v1 у v2 { #fastapi-support-for-pydantic-v1-in-v2 }
|
||||
|
||||
Починаючи з FastAPI 0.119.0, також є часткова підтримка Pydantic v1 усередині Pydantic v2, щоб полегшити міграцію на v2.
|
||||
|
||||
Тож ви могли б оновити Pydantic до найновішої версії 2, змінити імпорти, щоб використовувати підмодуль `pydantic.v1`, і в багатьох випадках це просто працюватиме.
|
||||
|
||||
{* ../../docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py hl[2,5,15] *}
|
||||
|
||||
/// warning | Попередження
|
||||
|
||||
Майте на увазі: оскільки команда Pydantic більше не підтримує Pydantic v1 у нових версіях Python, починаючи з Python 3.14, використання `pydantic.v1` також не підтримується в Python 3.14 і вище.
|
||||
|
||||
///
|
||||
|
||||
### Pydantic v1 і v2 в одному застосунку { #pydantic-v1-and-v2-on-the-same-app }
|
||||
|
||||
Pydantic **не підтримує** модель Pydantic v2, у якій власні поля визначені як моделі Pydantic v1, або навпаки.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "❌ Not Supported"
|
||||
direction TB
|
||||
subgraph V2["Pydantic v2 Model"]
|
||||
V1Field["Pydantic v1 Model"]
|
||||
end
|
||||
subgraph V1["Pydantic v1 Model"]
|
||||
V2Field["Pydantic v2 Model"]
|
||||
end
|
||||
end
|
||||
|
||||
style V2 fill:#f9fff3
|
||||
style V1 fill:#fff6f0
|
||||
style V1Field fill:#fff6f0
|
||||
style V2Field fill:#f9fff3
|
||||
```
|
||||
|
||||
...але ви можете мати розділені моделі, використовуючи Pydantic v1 і v2 в одному застосунку.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "✅ Supported"
|
||||
direction TB
|
||||
subgraph V2["Pydantic v2 Model"]
|
||||
V2Field["Pydantic v2 Model"]
|
||||
end
|
||||
subgraph V1["Pydantic v1 Model"]
|
||||
V1Field["Pydantic v1 Model"]
|
||||
end
|
||||
end
|
||||
|
||||
style V2 fill:#f9fff3
|
||||
style V1 fill:#fff6f0
|
||||
style V1Field fill:#fff6f0
|
||||
style V2Field fill:#f9fff3
|
||||
```
|
||||
|
||||
У деяких випадках навіть можливо мати і моделі Pydantic v1, і v2 в одній **операції шляху** у вашому застосунку FastAPI:
|
||||
|
||||
{* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *}
|
||||
|
||||
У наведеному вище прикладі вхідна модель — це модель Pydantic v1, а вихідна модель (визначена в `response_model=ItemV2`) — це модель Pydantic v2.
|
||||
|
||||
### Параметри Pydantic v1 { #pydantic-v1-parameters }
|
||||
|
||||
Якщо вам потрібно використовувати деякі специфічні для FastAPI інструменти для параметрів, як-от `Body`, `Query`, `Form` тощо, з моделями Pydantic v1, ви можете імпортувати їх із `fastapi.temp_pydantic_v1_params`, доки завершуєте міграцію на Pydantic v2:
|
||||
|
||||
{* ../../docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py hl[4,18] *}
|
||||
|
||||
### Міграція поетапно { #migrate-in-steps }
|
||||
|
||||
/// tip | Порада
|
||||
|
||||
Спочатку спробуйте `bump-pydantic`: якщо ваші тести проходять і це працює, тоді ви завершили все однією командою. ✨
|
||||
|
||||
///
|
||||
|
||||
Якщо `bump-pydantic` не підходить для вашого випадку, ви можете використати підтримку одночасно моделей Pydantic v1 і v2 в одному застосунку, щоб виконувати міграцію на Pydantic v2 поступово.
|
||||
|
||||
Спочатку ви можете оновити Pydantic до найновішої версії 2 і змінити імпорти, щоб використовувати `pydantic.v1` для всіх ваших моделей.
|
||||
|
||||
Далі ви можете почати мігрувати ваші моделі з Pydantic v1 на v2 групами, крок за кроком. 🚶
|
||||
102
docs/uk/docs/how-to/separate-openapi-schemas.md
Normal file
102
docs/uk/docs/how-to/separate-openapi-schemas.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Окремі схеми OpenAPI для введення та виведення чи ні { #separate-openapi-schemas-for-input-and-output-or-not }
|
||||
|
||||
Починаючи з виходу **Pydantic v2**, згенерований OpenAPI став трохи точнішим і **правильнішим**, ніж раніше. 😎
|
||||
|
||||
Фактично, у деяких випадках для однієї й тієї ж моделі Pydantic в OpenAPI може бути навіть **дві JSON Schema** — для введення та для виведення — залежно від того, чи мають поля **значення за замовчуванням**.
|
||||
|
||||
Розгляньмо, як це працює і як це змінити, якщо вам потрібно.
|
||||
|
||||
## Моделі Pydantic для введення та виведення { #pydantic-models-for-input-and-output }
|
||||
|
||||
Припустімо, у вас є модель Pydantic зі значеннями за замовчуванням, як-от ця:
|
||||
|
||||
{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py ln[1:7] hl[7] *}
|
||||
|
||||
### Модель для введення { #model-for-input }
|
||||
|
||||
Якщо ви використовуєте цю модель як вхідні дані, як тут:
|
||||
|
||||
{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py ln[1:15] hl[14] *}
|
||||
|
||||
...тоді поле `description` **не буде обов’язковим**. Тому що воно має значення за замовчуванням `None`.
|
||||
|
||||
### Вхідна модель у документації { #input-model-in-docs }
|
||||
|
||||
Ви можете підтвердити це в документації: поле `description` не має **червоної зірочки**, тобто не позначене як обов’язкове:
|
||||
|
||||
<div class="screenshot">
|
||||
<img src="/img/tutorial/separate-openapi-schemas/image01.png">
|
||||
</div>
|
||||
|
||||
### Модель для виведення { #model-for-output }
|
||||
|
||||
Але якщо ви використовуєте ту саму модель як вихідні дані, як тут:
|
||||
|
||||
{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py hl[19] *}
|
||||
|
||||
...тоді через те, що `description` має значення за замовчуванням, навіть якщо ви **не повернете нічого** для цього поля, воно все одно матиме **значення за замовчуванням**.
|
||||
|
||||
### Модель для даних відповіді { #model-for-output-response-data }
|
||||
|
||||
Якщо взаємодіяти з документацією і перевірити відповідь, то навіть попри те, що код не додав нічого в одне з полів `description`, JSON-відповідь містить значення за замовчуванням (`null`):
|
||||
|
||||
<div class="screenshot">
|
||||
<img src="/img/tutorial/separate-openapi-schemas/image02.png">
|
||||
</div>
|
||||
|
||||
Це означає, що поле **завжди матиме значення** — просто інколи це значення може бути `None` (або `null` у JSON).
|
||||
|
||||
Отже, клієнтам, які використовують ваш API, не потрібно перевіряти, чи існує значення — вони можуть **припускати, що поле завжди буде присутнім**, але в деяких випадках матиме значення за замовчуванням `None`.
|
||||
|
||||
Спосіб описати це в OpenAPI — позначити поле як **обов’язкове**, адже воно завжди буде присутнім.
|
||||
|
||||
Через це JSON Schema для моделі може відрізнятися залежно від того, чи використовується вона для **введення чи виведення**:
|
||||
|
||||
* для **введення** `description` **не є обов’язковим**
|
||||
* для **виведення** воно **є обов’язковим** (і може бути `None`, або в термінах JSON — `null`)
|
||||
|
||||
### Модель для виведення в документації { #model-for-output-in-docs }
|
||||
|
||||
Ви також можете перевірити модель для виведення в документації: **обидва** поля `name` і `description` позначені як **обов’язкові** **червоною зірочкою**:
|
||||
|
||||
<div class="screenshot">
|
||||
<img src="/img/tutorial/separate-openapi-schemas/image03.png">
|
||||
</div>
|
||||
|
||||
### Моделі для введення та виведення в документації { #model-for-input-and-output-in-docs }
|
||||
|
||||
А якщо переглянути всі доступні Schemas (JSON Schemas) в OpenAPI, ви побачите, що їх дві: `Item-Input` і `Item-Output`.
|
||||
|
||||
Для `Item-Input` поле `description` **не є обов’язковим**, воно не має червоної зірочки.
|
||||
|
||||
Але для `Item-Output` поле `description` **є обов’язковим**, воно має червону зірочку.
|
||||
|
||||
<div class="screenshot">
|
||||
<img src="/img/tutorial/separate-openapi-schemas/image04.png">
|
||||
</div>
|
||||
|
||||
Завдяки цій можливості **Pydantic v2** документація вашого API стає більш **точною**, і якщо у вас є автозгенеровані клієнти та SDK, вони теж будуть точнішими — із кращим **досвідом розробника** та узгодженістю. 🎉
|
||||
|
||||
## Не розділяти схеми { #do-not-separate-schemas }
|
||||
|
||||
Тепер, у деяких випадках ви можете хотіти мати **однакову схему для введення та виведення**.
|
||||
|
||||
Ймовірно, основний сценарій — коли у вас уже є автозгенерований код клієнта/SDK, і ви поки що не хочете оновлювати весь цей автозгенерований код клієнта/SDK. Імовірно, ви захочете зробити це згодом, але, можливо, не зараз.
|
||||
|
||||
У такому разі ви можете вимкнути цю можливість у **FastAPI** параметром `separate_input_output_schemas=False`.
|
||||
|
||||
/// info | Інформація
|
||||
|
||||
Підтримку `separate_input_output_schemas` було додано у FastAPI `0.102.0`. 🤓
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/separate_openapi_schemas/tutorial002_py310.py hl[10] *}
|
||||
|
||||
### Однакова схема для моделей введення та виведення в документації { #same-schema-for-input-and-output-models-in-docs }
|
||||
|
||||
І тепер для моделі буде одна єдина схема і для введення, і для виведення — лише `Item`, і в ній поле `description` буде **необов’язковим**:
|
||||
|
||||
<div class="screenshot">
|
||||
<img src="/img/tutorial/separate-openapi-schemas/image05.png">
|
||||
</div>
|
||||
7
docs/uk/docs/how-to/testing-database.md
Normal file
7
docs/uk/docs/how-to/testing-database.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Тестування бази даних { #testing-a-database }
|
||||
|
||||
Ви можете вивчити бази даних, SQL і SQLModel у <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">документації SQLModel</a>. 🤓
|
||||
|
||||
Є міні-<a href="https://sqlmodel.tiangolo.com/tutorial/fastapi/" class="external-link" target="_blank">підручник з використання SQLModel із FastAPI</a>. ✨
|
||||
|
||||
У цьому підручнику є розділ про <a href="https://sqlmodel.tiangolo.com/tutorial/fastapi/tests/" class="external-link" target="_blank">тестування SQL баз даних</a>. 😎
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user